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
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").
- 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
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:
- 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.
- 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.
Summary
pilot worktree detect --json <slug>andpilot review-scope --slug <slug> --jsonreturn abase_branch/base_refthat does not match either (a) thebase_branchrecorded in the session's~/.pilot/sessions/<id>/worktree.json, or (b) the actual default branch (main) the worktree was created from. Instead,base_branchtracks 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 ranpilot worktree create --json <slug>to spin up a nested spec worktree for/specisolation.Repro
pilot worktree create --json <slug>— creates the worktree, writes~/.pilot/sessions/<session-id>/worktree.jsonwithbase_branchset 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. viagit symbolic-ref refs/remotes/origin/HEAD, not "whatever HEAD currently is").base_branch/base_refcomes back as a branch name belonging to a completely unrelated, unmerged, no-PR branch — not the value stored inworktree.json, notmain.Evidence it's not reading the stored state
Editing
base_branchin 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:
base_branch?worktree_path)spec/<slug>project_root(a different worktree, e.g..worktrees/terminal-2)fix/4469-....worktrees/*dir)fix/some-unrelated-branchSo
base_branchtracks the root/top-level checkout's current branch, notworktree.json'sproject_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, everyreview-scope/worktree detectcall in every other worktree silently inherits that unrelated branch as its diff base.Impact
pilot review-scope --slug X --jsonreturns adiff_rangethat 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.git diff origin/main...HEAD.Suggested fix
base_branchresolution should not depend on the live state of any git checkout at query time. Two independent things to fix:worktree createtime, resolve the true default branch (git symbolic-ref refs/remotes/origin/HEAD, falling back tomain/masterprobing) instead of capturing whatever the calling worktree's HEAD happens to be, and persist that.detect/review-scopequery time, readbase_branchfrom the storedworktree.jsonfor 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.