What happened
Two related lifecycle problems, both about work and branches that outlive — or fail to outlive — a run.
(1) A failed task's committed work is discarded and is not in the reflog.
With quality_gate.enabled = true and a deliberately failing test_command, a real agent run created
its file, committed it, then called task_set_status(done). The gate correctly refused:
mcp__dotbot__task_set_status: MCP error -32603: Tool execution failed: validation error:
Hook 'enter-done' aborted the transition: Verify '05-verify-quality.ps1' failed:
Quality gate failed: test (exit 1)
Task ended failed. Then the worktree and task branch were deleted, taking the commit with them:
qg-probe.ts present on:
master : False
develop : False
feature/qa-work : False
workflow/fast-prompt-duJ1 : False (integration branch — correct for a failure)
worktrees preserved : 0
task branch : gone
Recoverability:
git reflog --all | grep qg-probe -> not in reflog (branch deleted, its reflog went too)
git stash list -> (empty)
git fsck --lost-found -> 8 dangling commits, one of which is the work:
55f3b6b feat: add QG_PROBE constant for quality gate probe
So the code exists only as a dangling git object, outside the reflog, and the next git gc destroys
it. The quality gate's entire purpose is to catch code that fails tests — and the failure path then
throws that code away, so the developer cannot inspect or fix it and must pay for the work again.
The inconsistency is notable: the merge-failure path deliberately preserves everything and says so
("Worktree preserved at: …"), while the task-failure path deletes it. The two paths disagree about
whether in-flight work is worth keeping.
(2) Every run leaves a workflow/* branch that the supplied reaper cannot see.
Three successive smoke-test runs on one project:
local workflow/* : 3 remote workflow/* : 3
workflow/smoke-test-Ikka ahead=0 sha=af3b80c
workflow/smoke-test-MhFg ahead=0 sha=af3b80c
workflow/smoke-test-a09g ahead=0 sha=af3b80c
git diff --stat master..workflow/smoke-test-a09g -> (empty)
All three are byte-identical to master — the workflow's only output is gitignored, so the
squash-merge replayed an empty patch. Each was still pushed to the remote and announced:
Integration branch workflow/smoke-test-a09g pushed to <origin>. Open a PR into master.
A PR from any of them would be empty. Branches are produced on the failure path too — a run that
completed zero tasks still pushed one (see #682's logs).
dotbot prune-branches cannot clean them with any documented invocation:
| Attempt |
Result |
prune-branches --dry-run |
✓ No branches match the prune criteria. (window 30d) |
prune-branches --dry-run --include-remote |
✓ No branches match the prune criteria. |
prune-branches --older-than 0d --dry-run |
A parameter cannot be found that matches parameter name 'older-than'. exit 0 |
prune-branches -OlderThan 0d --dry-run |
✓ No branches match… (skipped: they have upstreams) |
prune-branches -OlderThan 0d --include-remote --dry-run |
3 candidates ✓ |
Two defaults conspire. Get-PrunableBranches:514 skips any branch with has_remote_ref unless
-IncludeRemote, and _Get-RepoBranchRecords:566 derives that from %(upstream:short) — so pushing
the branch is what makes it invisible to the default prune. Widening the 30-day window then requires
-OlderThan, whose documented kebab-case form is unbindable (see BUG-006). Only the undocumented
PowerShell-native -OlderThan works.
A related correctness detail: _Get-RepoBranchRecords:547 measures age from the branch tip's
%(committerdate), not from when the branch was created. An empty integration branch inherits the
trunk's tip date, so on a repo whose trunk is older than 30 days brand-new branches are immediately
prunable, and on an actively committed trunk they never are. Branch age is not what is measured.
What you expected
- Work an agent has committed should survive a task failure — preserve the task branch (as the
merge-failure path already does), or at minimum name the recoverable commit in the failure message.
For a quality-gate failure specifically, keeping the branch is the whole point: someone has to fix
the code.
- A completed run should reap its own integration branch when it is empty, or the default
dotbot prune-branches should be able to see the branches Dotbot itself creates — without requiring
an undocumented parameter spelling plus a non-default flag.
Steps to reproduce
(1) Discarded work on a failed task (needs an AI provider):
$env:DOTBOT_HOME = '<dotbot checkout>'
# init a repo + dotbot init + `workflow add fast-prompt` as usual, then:
@'
{ "workflow": "fast-prompt",
"quality_gate": { "enabled": true,
"test_command": "pwsh -NoProfile -Command \"exit 3\"", "lint_command": null } }
'@ | Set-Content .bot\.control\settings.json
# launch a code task over HTTP (the CLI cannot pass a prompt):
& $env:DOTBOT_HOME\bin\dotbot.ps1 go # note the port from .bot\.control\ui-port
Invoke-RestMethod -Method Post -Uri "http://localhost:<port>/api/workflows/fast-prompt/run" `
-ContentType 'application/json' -Headers @{'X-Dotbot-Request'='1'} `
-Body '{"prompt":"Create apps/web/src/lib/qg-probe.ts exporting QG_PROBE = \"x\". Nothing else."}'
# after it settles: task status = failed, and the committed file is on no branch
git for-each-ref --format='%(refname:short)' refs/heads/ |
ForEach-Object { "$_ : $(git cat-file -e "${_}:apps/web/src/lib/qg-probe.ts" 2>$null; $LASTEXITCODE -eq 0)" }
git reflog --all | Select-String qg-probe # -> nothing
git fsck --lost-found | Select-String 'dangling commit' # -> the work is here only
(2) Unreapable branch debris (no AI provider needed):
& $env:DOTBOT_HOME\bin\dotbot.ps1 workflow add smoke-test -y
1..3 | ForEach-Object { & $env:DOTBOT_HOME\bin\dotbot.ps1 run smoke-test --watch }
git branch --list 'workflow/*' # -> 3 branches
git branch -r --list 'origin/workflow/*' # -> 3 on the remote too
git rev-list --count master..workflow/smoke-test-XXXX # -> 0, they are empty
& $env:DOTBOT_HOME\bin\dotbot.ps1 prune-branches --dry-run # -> none
& $env:DOTBOT_HOME\bin\dotbot.ps1 prune-branches --older-than 0d --dry-run # -> binding error, exit 0
& $env:DOTBOT_HOME\bin\dotbot.ps1 prune-branches -OlderThan 0d --include-remote --dry-run # -> finally lists them
Environment
OS: Windows 11 Pro 10.0.26200 | dotbot v4.0.2 (main @ 7c95b466)
pwsh 7.6.4 | git 2.54.0.windows.1 | claude CLI 2.1.222 (models: shipped defaults, all resolve)
DOTBOT_HOME set explicitly; %APPDATA%\dotbot\user-settings.json absent
Target repo: 1129 files, single remote (local bare origin), master default
Severity
high
Logs / screenshots
Cosmetic defect visible in the prune output that finally works — branch name and timestamp are
concatenated with no separator:
CANDIDATES
────────────────────────────────────────────
• workflow/smoke-test-Ikka08/05/2026 15:37:47 [has remote]
• workflow/smoke-test-MhFg08/05/2026 15:37:47 [has remote]
• workflow/smoke-test-a09g08/05/2026 15:37:47 [has remote]
Reap command that does work today, for anyone accumulating debris now:
dotbot prune-branches -OlderThan 0d --include-remote --dry-run # review first
dotbot prune-branches -OlderThan 0d --include-remote -y
What happened
Two related lifecycle problems, both about work and branches that outlive — or fail to outlive — a run.
(1) A failed task's committed work is discarded and is not in the reflog.
With
quality_gate.enabled = trueand a deliberately failingtest_command, a real agent run createdits file, committed it, then called
task_set_status(done). The gate correctly refused:Task ended
failed. Then the worktree and task branch were deleted, taking the commit with them:Recoverability:
So the code exists only as a dangling git object, outside the reflog, and the next
git gcdestroysit. The quality gate's entire purpose is to catch code that fails tests — and the failure path then
throws that code away, so the developer cannot inspect or fix it and must pay for the work again.
The inconsistency is notable: the merge-failure path deliberately preserves everything and says so
("Worktree preserved at: …"), while the task-failure path deletes it. The two paths disagree about
whether in-flight work is worth keeping.
(2) Every run leaves a
workflow/*branch that the supplied reaper cannot see.Three successive
smoke-testruns on one project:All three are byte-identical to
master— the workflow's only output is gitignored, so thesquash-merge replayed an empty patch. Each was still pushed to the remote and announced:
A PR from any of them would be empty. Branches are produced on the failure path too — a run that
completed zero tasks still pushed one (see #682's logs).
dotbot prune-branchescannot clean them with any documented invocation:prune-branches --dry-run✓ No branches match the prune criteria.(window30d)prune-branches --dry-run --include-remote✓ No branches match the prune criteria.prune-branches --older-than 0d --dry-runA parameter cannot be found that matches parameter name 'older-than'.exit 0prune-branches -OlderThan 0d --dry-run✓ No branches match…(skipped: they have upstreams)prune-branches -OlderThan 0d --include-remote --dry-runTwo defaults conspire.
Get-PrunableBranches:514skips any branch withhas_remote_refunless-IncludeRemote, and_Get-RepoBranchRecords:566derives that from%(upstream:short)— so pushingthe branch is what makes it invisible to the default prune. Widening the 30-day window then requires
-OlderThan, whose documented kebab-case form is unbindable (see BUG-006). Only the undocumentedPowerShell-native
-OlderThanworks.A related correctness detail:
_Get-RepoBranchRecords:547measures age from the branch tip's%(committerdate), not from when the branch was created. An empty integration branch inherits thetrunk's tip date, so on a repo whose trunk is older than 30 days brand-new branches are immediately
prunable, and on an actively committed trunk they never are. Branch age is not what is measured.
What you expected
merge-failure path already does), or at minimum name the recoverable commit in the failure message.
For a quality-gate failure specifically, keeping the branch is the whole point: someone has to fix
the code.
dotbot prune-branchesshould be able to see the branches Dotbot itself creates — without requiringan undocumented parameter spelling plus a non-default flag.
Steps to reproduce
(1) Discarded work on a failed task (needs an AI provider):
(2) Unreapable branch debris (no AI provider needed):
Environment
Severity
high
Logs / screenshots
Cosmetic defect visible in the prune output that finally works — branch name and timestamp are
concatenated with no separator:
Reap command that does work today, for anyone accumulating debris now: