Skip to content

worktree detect / review-scope resolve base_branch from the wrong repo checkout in multi-worktree setups #170

Description

@moonplkr

Summary

pilot worktree detect --json <slug> and pilot review-scope --slug <slug> --json return a base_branch/base_ref that does not match either (a) the base_branch recorded in the session's ~/.pilot/sessions/<id>/worktree.json, or (b) the actual default branch (main) the worktree was created from. Instead, base_branch tracks whatever branch happens to be checked out live, at query time, in some other reference checkout — reproduced consistently across multiple queries in one session.

Pilot Shell version: v9.14.0

Setup

Multiple parallel Claude Code sessions, each in its own git worktree under a shared repo (.worktrees/terminal-1 .. terminal-4), plus the bare repo root checkout. One session ran pilot worktree create --json <slug> to spin up a nested spec worktree for /spec isolation.

Repro

  1. pilot worktree create --json <slug> — creates the worktree, writes ~/.pilot/sessions/<session-id>/worktree.json with base_branch set to whatever branch happened to be checked out in the calling worktree at creation time (already a separate issue — ideally this should resolve to the repo's actual default branch, e.g. via git symbolic-ref refs/remotes/origin/HEAD, not "whatever HEAD currently is").
  2. Some time later (other terminals/sessions have since moved their own checkouts to unrelated branches), run:
    pilot worktree detect --json <slug>
    pilot review-scope --slug <slug> --json
  3. base_branch / base_ref comes back as a branch name belonging to a completely unrelated, unmerged, no-PR branch — not the value stored in worktree.json, not main.

Evidence it's not reading the stored state

// ~/.pilot/sessions/<id>/worktree.json (edited by hand)
{"worktree_path": "...", "branch": "spec/<slug>", "base_branch": "main", "plan_slug": "<slug>", ...}
$ pilot review-scope --slug <slug> --json
{"mode": "worktree", "base_ref": "fix/some-unrelated-branch", "diff_range": "fix/some-unrelated-branch...spec/<slug>", ...}

Editing base_branch in the JSON to "main" had zero effect on the tool's output — it kept returning the same wrong branch across repeated invocations, even after the edit. This rules out a stale-cache-of-the-stored-value explanation; the value is being recomputed live from somewhere else on every call.

Cross-checked against every git checkout in play:

Directory Checked-out branch Matches reported base_branch?
the worktree itself (worktree_path) spec/<slug> no
the session's project_root (a different worktree, e.g. .worktrees/terminal-2) fix/4469-... no
the bare repo's root checkout (top-level clone, outside any .worktrees/* dir) fix/some-unrelated-branch yes, exact match

So base_branch tracks the root/top-level checkout's current branch, not worktree.json's project_root, and not the actual default branch. In a setup where multiple sessions share one clone (each in its own linked worktree) and the root checkout is left on whatever some other, unrelated session's work happens to be, every review-scope/worktree detect call in every other worktree silently inherits that unrelated branch as its diff base.

Impact

  • pilot review-scope --slug X --json returns a diff_range that includes hundreds of files from an unrelated branch — completely unusable for the changes-review / code-review workflows that treat this as authoritative ("never derive the range by hand").
  • pilot worktree sync (squash-merge) presumably uses the same resolution — if so, this risks squash-merging a worktree branch against the wrong base.
  • The failure is silent: no error, no warning, just a wrong (but well-formed) JSON response. It only surfaces if someone manually cross-checks with git diff origin/main...HEAD.

Suggested fix

base_branch resolution should not depend on the live state of any git checkout at query time. Two independent things to fix:

  1. At worktree create time, resolve the true default branch (git symbolic-ref refs/remotes/origin/HEAD, falling back to main/master probing) instead of capturing whatever the calling worktree's HEAD happens to be, and persist that.
  2. At detect/review-scope query time, read base_branch from the stored worktree.json for that session/slug rather than re-deriving it from any checkout's current branch.

Happy to share the full worktree.json / command transcripts from the repro session if useful.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions