What happened
Two defects in one 33-line function (Dotbot.Worktree.psm1:217-249). They are filed together because
they share a cause — the function's parameter list is missing what it needs — and a fix touches the
same lines.
(1) git's explanation is thrown away (:243).
if ($currentBranch -ne $BranchName) {
git -C $ProjectRoot checkout $BranchName 2>&1 | Out-Null # <- stderr discarded
if ($LASTEXITCODE -ne 0) {
throw "Failed to checkout $BranchName in $ProjectRoot (currently on: $currentBranch)"
}
}
Demonstrated with a dirty tracked file that blocks the checkout:
Assert-OnBaseBranch -ProjectRoot <repo> -BranchName master
THROWN: Failed to checkout master in <repo> (currently on: workflow/plan-feature-RMB1)
mentions 'local changes' : False
mentions the blocking file : False
mentions 'overwritten' : False
git had already said precisely what was wrong:
error: Your local changes to the following files would be overwritten by checkout:
.bot/workspace/decisions/proposed/dec-001-example.md
Please commit your changes or stash them before you switch branches.
Aborting
Because the thrown string is what Complete-TaskWorktree puts into failure_detail, and what the
runner then shows the operator in the needs-input escalation, every possible blocker produces the
same unactionable message — a dirty tracked file, a branch already checked out in a second linked
worktree (fatal: '<b>' is already used by worktree at ...), or a Windows file lock
(error: unable to unlink ...: Permission denied). The operator cannot tell which, so the recommended
"A — investigate and retry" has nothing to investigate.
(2) The function cannot see git.base_branch, so failure paths abandon it (:225-231).
param(
[Parameter(Mandatory)][string]$ProjectRoot,
[string]$BranchName # <- no -BotRoot parameter exists
)
if (-not $BranchName) {
$BranchName = Resolve-MainBranch -ProjectRoot $ProjectRoot # <- no -BotRoot passed
}
Resolve-DotbotBaseBranch only consults git.base_branch when a -BotRoot is supplied
(:162), so this fallback can never honour the setting:
Resolve-DotbotBaseBranch -ProjectRoot <r> -BotRoot <r>\.bot -> feature/qa-work
Resolve-DotbotBaseBranch -ProjectRoot <r> -> master <- config invisible
Resolve-MainBranch -ProjectRoot <r> -> master
Three call sites in the workflow runner omit -BranchName, and all three sit in failure /
terminal-non-done / skipped cleanup paths — Invoke-WorkflowProcess.ps1:1774, :2390, :2413:
try { Assert-OnBaseBranch -ProjectRoot $projectRoot | Out-Null } catch { Write-BotLog -Level Warn ... }
So on a configured project, the happy path merges into the configured branch but any task failure
force-checks-out main/master, silently moving the operator's working copy off the branch they
configured:
configured git.base_branch : feature/qa-work
HEAD before : feature/qa-work
Assert-OnBaseBranch -ProjectRoot <repo> # exactly what line 1774 calls
returned : master
HEAD after : master
:2413 carries the comment # Re-assert base branch after failed-task cleanup (Fix: wrong-branch merge) — a fix for wrong-branch merges that itself ignores the configured base branch.
What you expected
- When
git checkout fails, the thrown message should carry git's stderr so the operator learns
why. The escalation shown to a human is only as useful as this string.
Assert-OnBaseBranch should accept -BotRoot and pass it through to Resolve-MainBranch, so its
no--BranchName fallback honours git.base_branch like every other resolution site. The three
runner call sites should pass the run's resolved base (or -BotRoot) rather than defaulting to
main/master.
Steps to reproduce
(1) Discarded stderr
$env:DOTBOT_HOME = '<dotbot checkout>'
$repo = 'C:\tmp\repro2'
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.Worktree\Dotbot.Worktree.psd1 -Force -DisableNameChecking
# a tracked file whose content differs between the two branches, dirty in the working tree
$f = "$repo\.bot\workspace\decisions\proposed\d.md"
'on master' > $f; git add -f $f; git commit -qm 'docs: d on master'
git checkout -q -b other master
'on other' > $f; git add -f $f; git commit -qm 'docs: d on other'
'dirty' > $f
try { Assert-OnBaseBranch -ProjectRoot $repo -BranchName master } catch { $_.Exception.Message }
# -> "Failed to checkout master in <repo> (currently on: other)" ... and nothing else
git checkout master # <- the reason Dotbot swallowed
(2) Config ignored on the fallback path
New-Item -ItemType Directory -Force "$repo\.bot\.control" | Out-Null
'{ "git": { "base_branch": "other" } }' | Set-Content "$repo\.bot\.control\settings.json"
Import-Module $env:DOTBOT_HOME\src\runtime\Modules\Dotbot.Settings\Dotbot.Settings.psd1 -Force -DisableNameChecking
git checkout -q other
Resolve-DotbotBaseBranch -ProjectRoot $repo -BotRoot "$repo\.bot" # -> other (honoured)
Assert-OnBaseBranch -ProjectRoot $repo # -> master (ignored)
git rev-parse --abbrev-ref HEAD # -> master (yanked)
Environment
OS: Windows 11 Pro 10.0.26200 | dotbot v4.0.2 (main @ 7c95b466)
pwsh 7.6.4 | git 2.54.0.windows.1
DOTBOT_HOME set explicitly; %APPDATA%\dotbot\user-settings.json absent
AI provider: not involved — reproduced through direct Dotbot.Worktree calls
Severity
medium
Logs / screenshots
The real-world consequence, from a user report — the entire diagnostic payload for a blocked run,
containing no root cause:
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.psm1: line 245
at Complete-TaskWorktree, ...\Dotbot.Worktree.psm1: line 1754
at <ScriptBlock>, ...\Invoke-WorkflowProcess.ps1: line 2289
at <ScriptBlock>, ...\Invoke-DotbotProcess.ps1: line 442
The reason, recoverable only by re-running the checkout by hand afterwards:
dirty working-tree paths: 1
~ .bot/workspace/decisions/proposed/dec-001-example.md
--- target 'master': 1 paths differ from HEAD; 1 of them are dirty ---
WOULD BLOCK git checkout master:
! .bot/workspace/decisions/proposed/dec-001-example.md
A minimal fix for (1) is to capture the output rather than discard it:
$checkoutOutput = git -C $ProjectRoot checkout $BranchName 2>&1 | Out-String
if ($LASTEXITCODE -ne 0) {
throw "Failed to checkout $BranchName in $ProjectRoot (currently on: $currentBranch): $($checkoutOutput.Trim())"
}
What happened
Two defects in one 33-line function (
Dotbot.Worktree.psm1:217-249). They are filed together becausethey share a cause — the function's parameter list is missing what it needs — and a fix touches the
same lines.
(1) git's explanation is thrown away (
:243).Demonstrated with a dirty tracked file that blocks the checkout:
git had already said precisely what was wrong:
Because the thrown string is what
Complete-TaskWorktreeputs intofailure_detail, and what therunner then shows the operator in the
needs-inputescalation, every possible blocker produces thesame unactionable message — a dirty tracked file, a branch already checked out in a second linked
worktree (
fatal: '<b>' is already used by worktree at ...), or a Windows file lock(
error: unable to unlink ...: Permission denied). The operator cannot tell which, so the recommended"A — investigate and retry" has nothing to investigate.
(2) The function cannot see
git.base_branch, so failure paths abandon it (:225-231).Resolve-DotbotBaseBranchonly consultsgit.base_branchwhen a-BotRootis supplied(
:162), so this fallback can never honour the setting:Three call sites in the workflow runner omit
-BranchName, and all three sit in failure /terminal-non-done / skipped cleanup paths —
Invoke-WorkflowProcess.ps1:1774,:2390,:2413:So on a configured project, the happy path merges into the configured branch but any task failure
force-checks-out
main/master, silently moving the operator's working copy off the branch theyconfigured:
:2413carries the comment# Re-assert base branch after failed-task cleanup (Fix: wrong-branch merge)— a fix for wrong-branch merges that itself ignores the configured base branch.What you expected
git checkoutfails, the thrown message should carry git's stderr so the operator learnswhy. The escalation shown to a human is only as useful as this string.
Assert-OnBaseBranchshould accept-BotRootand pass it through toResolve-MainBranch, so itsno-
-BranchNamefallback honoursgit.base_branchlike every other resolution site. The threerunner call sites should pass the run's resolved base (or
-BotRoot) rather than defaulting tomain/master.Steps to reproduce
(1) Discarded stderr
(2) Config ignored on the fallback path
Environment
Severity
medium
Logs / screenshots
The real-world consequence, from a user report — the entire diagnostic payload for a blocked run,
containing no root cause:
The reason, recoverable only by re-running the checkout by hand afterwards:
A minimal fix for (1) is to capture the output rather than discard it: