What happened
Complete-TaskWorktree takes its merge target from the base_branch recorded in
.bot/.control/worktree-map.json when the task worktree was created, and never reconciles it with the
branch the operator's main checkout is actually on (Dotbot.Worktree.psm1:1666):
$baseBranch = $entry.base_branch ?? (Resolve-MainBranch -ProjectRoot $ProjectRoot -BotRoot $BotRoot)
When the recorded value differs from the checked-out branch, one of two things happens — and which one
depends on whether anything happens to block git checkout:
Case A — nothing blocks it: the task's work is silently committed to the trunk.
success : True
failure_kind :
message : Squash-merged to master and cleaned up
master 18255dc -> f5a8fd7 feat: RM-B0 probe task [task:t_RMB0te]
apps/web/src/rm-b0-marker.ts | 1 +
workflow/plan-feature-RMB0 unchanged at 18255dc, file absent
HEAD workflow/plan-feature-RMB0 -> master (left there)
The commit landed on master while the operator was on workflow/plan-feature-RMB0. No warning, no
non-zero status. The branch the operator was on received nothing. The working copy was left on
master.
Case B — something blocks the checkout: the run halts with an unexplained error.
failure_kind : exception
message : Error during merge: Failed to checkout master in <repo> (currently on: workflow/plan-feature-RMB1)
at Assert-OnBaseBranch, ...\Dotbot.Worktree.psm1: line 245
at Complete-TaskWorktree, ...\Dotbot.Worktree.psm1: line 1754
The most reachable blocker is Dotbot's own pre-merge stash. Dotbot.Worktree.psm1:1744-1748 excludes
.bot/workspace/decisions/ from the stash:
git -C $ProjectRoot stash push -u -m "dotbot-pre-merge-$TaskId" -- `
'.' ':!.bot/workspace/tasks/' ':!.bot/workspace/decisions/'
…but that tree is tracked (init-project.ps1:230-245 seeds .gitkeep files into
decisions/{accepted,deprecated,proposed,superseded}; .bot/.gitignore does not cover it), it is
committed by this very function at :1880, and unlike .bot/workspace/tasks/ — scrubbed at
:1738-1739 — it is never scrubbed. So a dirty decisions file survives the stash and blocks the
checkout the stash exists to enable. :1750-1752 states that purpose explicitly.
Once it fails, recovery is impossible without manual surgery: New-TaskWorktree:1429-1432 writes a map
entry only if (-not $lockedMap.ContainsKey($TaskId)), and
Invoke-WorkflowProcess.ps1:130-139 early-returns when the worktree path still exists. base_branch
is therefore never refreshed, so answering the escalation's "A — investigate and retry" replays the
identical failure indefinitely.
What you expected
A task's work should be merged into the branch the run is actually building on, and the operator's
working copy should not be moved off their branch or written to without intent.
Concretely:
- If the recorded
base_branch no longer matches the run's actual base, Dotbot should reconcile it or
refuse with a message naming both branches — not silently commit to the trunk.
- The pre-merge stash must include every tracked path it needs to move away from, or scrub
.bot/workspace/decisions/ the way it already scrubs .bot/workspace/tasks/.
- A retry after a merge failure should re-resolve
base_branch rather than reuse the stale value.
Steps to reproduce
Deterministic, no AI provider needed. Tested on Windows 11 / pwsh 7.6.4 / git 2.54.0.
$env:DOTBOT_HOME = '<dotbot checkout>'
$repo = 'C:\tmp\repro'; $bot = "$repo\.bot"
# 1. a repo with a trunk, dotbot initialised and committed
git init -b master $repo; cd $repo
git config user.email a@b.c; git config user.name t
'x' > README.md; git add -A; git commit -qm init
& $env:DOTBOT_HOME\bin\dotbot.ps1 init -y
git add -A; git commit -qm 'chore: dotbot init'
Import-Module $env:DOTBOT_HOME\src\runtime\Modules\Dotbot.Core\Dotbot.Core.psm1 -Force -DisableNameChecking
Import-Module $env:DOTBOT_HOME\src\runtime\Modules\Dotbot.Settings\Dotbot.Settings.psd1 -Force -DisableNameChecking
Import-Module $env:DOTBOT_HOME\src\runtime\Modules\Dotbot.Worktree\Dotbot.Worktree.psd1 -Force -DisableNameChecking
New-Item -ItemType Directory -Force "$bot\.control" | Out-Null
# 2. a task worktree whose recorded base_branch is master, with a real change committed
$wt = New-TaskWorktree -TaskId t_probe -TaskName 'probe' -ProjectRoot $repo -BotRoot $bot -BaseBranch master
'must not land on master' > "$($wt.worktree_path)\marker.txt"
git -C $wt.worktree_path add -A; git -C $wt.worktree_path commit -qm 'feat: marker'
# 3. the operator moves the main checkout to another branch (as a workflow run does)
git -C $repo branch workflow/plan-feature-TEST master
git -C $repo checkout -q workflow/plan-feature-TEST
# ---- CASE A: clean tree -> silent merge into master ----
Complete-TaskWorktree -TaskId t_probe -ProjectRoot $repo -BotRoot $bot -SkipRemotePush
# -> success=True, "Squash-merged to master and cleaned up"
git -C $repo log --oneline -1 master # the task commit is on master
git -C $repo rev-parse --abbrev-ref HEAD # master — moved off the operator's branch
# ---- CASE B: add the blocker -> the reported exception ----
# repeat steps 2-3 with a fresh task id, then before completing:
$dec = "$repo\.bot\workspace\decisions\proposed\dec-001.md"
'original' > $dec; git -C $repo add -f $dec; git -C $repo commit -qm 'docs: dec-001'
git -C $repo checkout -q workflow/plan-feature-TEST
'differs here' > $dec; git -C $repo add -f $dec; git -C $repo commit -qm 'docs: dec-001 differs'
'dirty and uncommitted' > $dec # the blocker
Complete-TaskWorktree -TaskId t_probe2 -ProjectRoot $repo -BotRoot $bot -SkipRemotePush
# -> failure_kind=exception, "Failed to checkout master in <repo> (currently on: workflow/plan-feature-TEST)"
The blocker mechanism in isolation, with no Dotbot at all:
git stash push -u -m t -- '.' ':!.bot/workspace/tasks/' ':!.bot/workspace/decisions/'
# No local changes to save <- the only dirty file is the one it excludes
git checkout master
# error: Your local changes to the following files would be overwritten by checkout:
# .bot/workspace/decisions/proposed/dec-001.md
Environment
OS: Windows 11 Pro 10.0.26200 | dotbot v4.0.2 (main @ 7c95b466)
pwsh 7.6.4 | git 2.54.0.windows.1 | node 24.16.0 | dotnet SDK 10.0.302
DOTBOT_HOME set explicitly; %APPDATA%\dotbot\user-settings.json absent
git config: init.defaultBranch=master, core.autocrlf=true
AI provider: not involved — reproduced through direct Dotbot.Worktree calls and raw git
Target repo: ~1129 files, tracked .bot/workspace, single remote
Severity
high
Logs / screenshots
Reported by a user on a real project, matching Case B exactly — same failure_kind, same message
template, same line numbers:
Merge failure kind: exception
Message: Error during merge: Failed to checkout master in
C:\Users\<user>\source\repos\apms-performanceevaluation (currently on: workflow/plan-feature-TAjy)
Detail: Failed to checkout master in ... (currently on: workflow/plan-feature-TAjy)
at Assert-OnBaseBranch, ...\Dotbot.Worktree\Dotbot.Worktree.psm1: line 245
at Complete-TaskWorktree, ...\Dotbot.Worktree\Dotbot.Worktree.psm1: line 1754
at <ScriptBlock>, ...\src\runtime\Scripts\Invoke-WorkflowProcess.ps1: line 2289
at <ScriptBlock>, ...\src\runtime\Scripts\Invoke-DotbotProcess.ps1: line 442
Worktree preserved at: C:\...\worktrees\apms-performanceevaluation\task-t_Hd7Qhc-scan-relevant-codebase
State left behind after Case B (a retry cannot succeed):
HEAD : workflow/plan-feature-RMB1 <- operator's branch not restored
worktree preserved : True
task branch preserved : YES
dirty file : M .bot/workspace/decisions/proposed/dec-001-example.md
worktree-map entry : base_branch STILL "master"
leaked stashes : 0
Workaround that does work (verified end to end): set the base branch explicitly in
.bot/.control/settings.json:
{ "git": { "base_branch": "feature/my-work" } }
New-TaskWorktree -> map base_branch = "feature/my-work"
Complete-TaskWorktree -> "Squash-merged to feature/my-work and cleaned up"
master unchanged; feature/my-work advanced
What happened
Complete-TaskWorktreetakes its merge target from thebase_branchrecorded in.bot/.control/worktree-map.jsonwhen the task worktree was created, and never reconciles it with thebranch the operator's main checkout is actually on (
Dotbot.Worktree.psm1:1666):When the recorded value differs from the checked-out branch, one of two things happens — and which one
depends on whether anything happens to block
git checkout:Case A — nothing blocks it: the task's work is silently committed to the trunk.
The commit landed on
masterwhile the operator was onworkflow/plan-feature-RMB0. No warning, nonon-zero status. The branch the operator was on received nothing. The working copy was left on
master.Case B — something blocks the checkout: the run halts with an unexplained error.
The most reachable blocker is Dotbot's own pre-merge stash.
Dotbot.Worktree.psm1:1744-1748excludes.bot/workspace/decisions/from the stash:…but that tree is tracked (
init-project.ps1:230-245seeds.gitkeepfiles intodecisions/{accepted,deprecated,proposed,superseded};.bot/.gitignoredoes not cover it), it iscommitted by this very function at
:1880, and unlike.bot/workspace/tasks/— scrubbed at:1738-1739— it is never scrubbed. So a dirty decisions file survives the stash and blocks thecheckout the stash exists to enable.
:1750-1752states that purpose explicitly.Once it fails, recovery is impossible without manual surgery:
New-TaskWorktree:1429-1432writes a mapentry only
if (-not $lockedMap.ContainsKey($TaskId)), andInvoke-WorkflowProcess.ps1:130-139early-returns when the worktree path still exists.base_branchis therefore never refreshed, so answering the escalation's "A — investigate and retry" replays the
identical failure indefinitely.
What you expected
A task's work should be merged into the branch the run is actually building on, and the operator's
working copy should not be moved off their branch or written to without intent.
Concretely:
base_branchno longer matches the run's actual base, Dotbot should reconcile it orrefuse with a message naming both branches — not silently commit to the trunk.
.bot/workspace/decisions/the way it already scrubs.bot/workspace/tasks/.base_branchrather than reuse the stale value.Steps to reproduce
Deterministic, no AI provider needed. Tested on Windows 11 / pwsh 7.6.4 / git 2.54.0.
The blocker mechanism in isolation, with no Dotbot at all:
Environment
Severity
high
Logs / screenshots
Reported by a user on a real project, matching Case B exactly — same
failure_kind, same messagetemplate, same line numbers:
State left behind after Case B (a retry cannot succeed):
Workaround that does work (verified end to end): set the base branch explicitly in
.bot/.control/settings.json:{ "git": { "base_branch": "feature/my-work" } }