Skip to content

workspace worktree add silently checked out source branch instead of new branch (observed once, cannot reproduce) #286

@chubes4

Description

@chubes4

Symptom (observed once, not yet reproducible)

While running studio wp datamachine-code workspace worktree add wc-site-generator cherry-pick-finding-packet-fix --from=static/marbling-bench from a Studio agent shell on a workspace that already had ~474 worktrees registered, DMC reported success with branch metadata cherry-pick-finding-packet-fix (created), but the actual worktree on disk was checked out to the source branch (static/marbling-bench), not the requested new branch.

$ studio wp datamachine-code workspace worktree add wc-site-generator cherry-pick-finding-packet-fix --from=static/marbling-bench
Success: Worktree \"wc-site-generator@cherry-pick-finding-packet-fix\" added at /Users/.../wc-site-generator@cherry-pick-finding-packet-fix (branch fix/static-validation-finding-packets-regression).
Handle: wc-site-generator@cherry-pick-finding-packet-fix
Path:   /Users/.../wc-site-generator@cherry-pick-finding-packet-fix
Branch: cherry-pick-finding-packet-fix (created)

$ cd /Users/.../wc-site-generator@cherry-pick-finding-packet-fix
$ git status -sb
## static/marbling-bench...origin/static/marbling-bench [ahead 1]    <-- not the new branch
$ git branch --show-current
static/marbling-bench

Net effect: a subsequent git cherry-pick committed onto static/marbling-bench directly, and git push origin static/marbling-bench would have shipped that commit straight to whatever PR owns that branch — exactly the cross-session collision the <repo>@<slug> worktree convention exists to prevent.

In my case the resulting commit landed on PR #91's branch, which was actually convenient for my task, but it could just as easily have force-pushed an unrelated experiment onto someone else's open PR.

Cannot reproduce on rerun

I removed the worktree (and the deleted local branch) and reran the exact same command four times in a row. Every subsequent run produced the correct outcome (worktree on cherry-pick-finding-packet-fix, branch created). So this is either:

  • A race condition with another concurrent DMC mutation,
  • An interaction with the workspace state at that moment (~474 worktrees, disk-budget warning fired, fetch had succeeded, base was 10 commits behind upstream — under the 50-commit staleness threshold),
  • Or something I misread in the original output.

Filing this as observational rather than as a deterministic bug, but flagging the codepath that would be most likely to manifest the symptom.

Suspected codepath

inc/Workspace/Workspace.php::worktree_add_locked() (lines 1394-1407 at HEAD 6010125):

exec( sprintf( 'git -C %s show-ref --verify --quiet %s 2>&1', escapeshellarg( $primary_path ), escapeshellarg( 'refs/heads/' . $branch ) ), $_unused, $exists_local );
$created_branch = false;
$resolved_base  = null;

if ( 0 === $exists_local ) {
    $cmd = sprintf( 'worktree add %s %s', escapeshellarg( $wt_path ), escapeshellarg( $branch ) );
} else {
    $base           = $from && '' !== trim( $from ) ? trim( $from ) : $this->resolve_default_base( $primary_path );
    $resolved_base  = $base;
    $cmd            = sprintf( 'worktree add -b %s %s %s', escapeshellarg( $branch ), escapeshellarg( $wt_path ), escapeshellarg( $base ) );
    $created_branch = true;
}

If for any reason show-ref --verify --quiet refs/heads/<new-branch> returned exit 0 even though the new branch didn't actually exist (stale ref-cache, parallel ref churn, packed-refs interaction), DMC would dispatch the no--b form against the new-branch-name positional, which git would interpret as "check out existing branch with that name" — but if the branch actually doesn't exist, the command would fail. So that path doesn't fully explain my observation either.

A more defensive shape would be: regardless of which command DMC chose, after run_git, verify the worktree HEAD actually matches $branch before reporting success and writing inventory rows. Suggested check (uses git plumbing, no extra deps):

$head_check = $this->run_git( $wt_path, 'rev-parse --abbrev-ref HEAD' );
if ( ! is_wp_error( $head_check ) ) {
    $actual_branch = trim( (string) ( $head_check['output'] ?? '' ) );
    if ( $actual_branch !== $branch ) {
        $this->run_git( $primary_path, sprintf( 'worktree remove --force %s', escapeshellarg( $wt_path ) ) );
        return new \WP_Error(
            'worktree_branch_mismatch',
            sprintf( 'Worktree was created at %s but checked out branch \"%s\" instead of the requested \"%s\". Removed the half-cooked worktree; please retry.', $wt_path, $actual_branch, $branch ),
            array( 'status' => 500, 'expected' => $branch, 'actual' => $actual_branch )
        );
    }
}

That converts a silent collision (which I hit, but cannot reproduce) into a loud, recoverable error — matches the rest of the staleness-gate behavior in the same function, where a half-cooked worktree gets torn down rather than returned.

Repro environment

  • Host: macOS, Studio CLI driving DMC on intelligence-chubes4.
  • Workspace: /Users/chubes/Developer, ~474 worktrees registered (above the 100-warning threshold), disk-budget status warning.
  • Repo: chubes4/wc-site-generator, primary at static/spore-ledger-field-supply, source branch static/marbling-bench had a local checkout 10 commits behind origin/static/marbling-bench.
  • DMC HEAD: 6010125 fix: expose bounded cleanup-eligible apply path.
  • Studio agent shell, no concurrent CLI calls visible from my session — but I can't speak to background tasks the agent runtime might have triggered.

Happy to instrument or rerun under different conditions if there's a code path you want me to push on.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions