Skip to content

Assert-OnBaseBranch discards git's error and cannot read git.base_branch #680

Description

@EnmaJim

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

  1. 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.
  2. 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())"
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    Status
    Inbox

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions