fix(worktree): branch from main to prevent cross-session contamination#322
Conversation
Centaur ReviewFound 4 issue(s) (3 warning).
|
… coverage - Replace hardcoded 'main' default with detectDefaultBranch() that reads git symbolic-ref refs/remotes/origin/HEAD (falls back to 'main') - Fix git init calls in tests to use -b main for cross-platform reliability - Add full test suite for sync createWorktree (mirrors async coverage) - Add tests for detectDefaultBranch (no-origin fallback + origin detection) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 3 issue(s) (2 warning).
server/worktree.ts
Solid fix for a real cross-session contamination bug with thorough test coverage. Two issues worth addressing: split('/').pop() mishandles slashed branch names, and the hardcoded 'main' fallback can break repos using master without an origin.
- 🟡 bugs (L44):
ref.split('/').pop()incorrectly extracts branch names that contain slashes. If the default branch isrelease/stable, the symbolic-ref isrefs/remotes/origin/release/stableand.pop()returnsstableinstead ofrelease/stable. Useref.replace('refs/remotes/origin/', '')instead.[fixable] - 🟡 regressions (L49): Hardcoded fallback to
'main'regresses repos that usemaster(or another name) as default branch and have no origin configured. Previously, omittingstartPointcausedgit worktree addto branch from HEAD, which always works. Now it passes'main'as startPoint, which fails with a fatal error if the ref doesn't exist. Consider verifying the fallback ref exists (e.g.git rev-parse --verify main) and falling back toHEADif it doesn't.[fixable] - 🔵 unsafe_assumptions (L38):
detectDefaultBranchuses synchronousexecFileSyncbut is called from the asynccreateWorktreeAsyncpath (line 164), blocking the event loop. Consider an async variant or accepting that the git call is fast enough (~5ms) to not matter in practice.[fixable]
| { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: WORKTREE_GIT_TIMEOUT_MS }, | ||
| ).trim(); | ||
| // ref looks like "refs/remotes/origin/main" — extract the last segment | ||
| const branch = ref.split('/').pop(); |
There was a problem hiding this comment.
🟡 bugs: ref.split('/').pop() incorrectly extracts branch names that contain slashes. If the default branch is release/stable, the symbolic-ref is refs/remotes/origin/release/stable and .pop() returns stable instead of release/stable. Use ref.replace('refs/remotes/origin/', '') instead. [fixable]
| } catch { | ||
| // No origin or symbolic-ref not set — fall back | ||
| } | ||
| return 'main'; |
There was a problem hiding this comment.
🟡 regressions: Hardcoded fallback to 'main' regresses repos that use master (or another name) as default branch and have no origin configured. Previously, omitting startPoint caused git worktree add to branch from HEAD, which always works. Now it passes 'main' as startPoint, which fails with a fatal error if the ref doesn't exist. Consider verifying the fallback ref exists (e.g. git rev-parse --verify main) and falling back to HEAD if it doesn't. [fixable]
| */ | ||
| export function detectDefaultBranch(repoPath: string): string { | ||
| try { | ||
| const ref = execFileSync( |
There was a problem hiding this comment.
🔵 unsafe_assumptions: detectDefaultBranch uses synchronous execFileSync but is called from the async createWorktreeAsync path (line 164), blocking the event loop. Consider an async variant or accepting that the git call is fast enough (~5ms) to not matter in practice. [fixable]
|
/review |
Fixes from Centaur code review:
1. Fix branch name extraction for slashed branches (e.g., release/stable)
- Changed from .split('/').pop() to .replace('refs/remotes/origin/', '')
- Prevents incorrect extraction of "stable" instead of "release/stable"
2. Add fallback verification for repos without 'main' branch
- After no origin detected, verify 'main' exists before using it
- Fall back to 'HEAD' if 'main' doesn't exist (e.g., master-only repos)
3. Document intentional sync execFileSync usage
- Added comment explaining this runs during session setup, not hot path
- Blocking is acceptable for one-time worktree creation
4. Add comprehensive tests for new fallback logic
- Test slashed branch name handling (release/stable)
- Test HEAD fallback when main doesn't exist
- All 45 tests passing
Items 4 (git init -b main) and 5 (sync function tests) were already complete.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ontamination createWorktree() and createWorktreeAsync() used `git worktree add -b <branch> <path>` without a start-point argument. This caused new session branches to inherit whatever HEAD was in the base repo — often another session branch with unmerged commits. Fix: add startPoint option (default 'main') passed as the final argument to `git worktree add`. When falling back to an existing branch, reset it to startPoint before attaching the worktree. Discovered while investigating PR #105 (mgmt) where a design doc PR inherited 7 unrelated jira_process commits from a prior session. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… coverage - Replace hardcoded 'main' default with detectDefaultBranch() that reads git symbolic-ref refs/remotes/origin/HEAD (falls back to 'main') - Fix git init calls in tests to use -b main for cross-platform reliability - Add full test suite for sync createWorktree (mirrors async coverage) - Add tests for detectDefaultBranch (no-origin fallback + origin detection) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes from Centaur code review:
1. Fix branch name extraction for slashed branches (e.g., release/stable)
- Changed from .split('/').pop() to .replace('refs/remotes/origin/', '')
- Prevents incorrect extraction of "stable" instead of "release/stable"
2. Add fallback verification for repos without 'main' branch
- After no origin detected, verify 'main' exists before using it
- Fall back to 'HEAD' if 'main' doesn't exist (e.g., master-only repos)
3. Document intentional sync execFileSync usage
- Added comment explaining this runs during session setup, not hot path
- Blocking is acceptable for one-time worktree creation
4. Add comprehensive tests for new fallback logic
- Test slashed branch name handling (release/stable)
- Test HEAD fallback when main doesn't exist
- All 45 tests passing
Items 4 (git init -b main) and 5 (sync function tests) were already complete.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
db3e380 to
26313ee
Compare
|
/review |
dimakis
left a comment
There was a problem hiding this comment.
Codex Review
Found 1 issue(s) (1 warning).
server/worktree.ts
PR fixes the HEAD contamination bug and adds strong coverage, but default-branch detection may regress repos whose local base branch intentionally differs from origin/HEAD.
- 🟡 regressions (L39):
detectDefaultBranch()now prefersorigin/HEADover the local repo state, which changes behavior for repos whose local checked-out default branch intentionally differs from the remote default. In that case new session worktrees will suddenly branch from the remote default branch instead of the local base branch, potentially creating sessions from the wrong code line. Consider preferring an explicit local branch/ref when present (e.g. current repo default branch or configured branch) and only usingorigin/HEADas a fallback.[fixable]
| * NOTE: Uses execFileSync intentionally — called during session setup (not hot path). | ||
| * Worktree creation is a one-time initialization per session, so blocking is acceptable. | ||
| */ | ||
| export function detectDefaultBranch(repoPath: string): string { |
There was a problem hiding this comment.
🟡 regressions: detectDefaultBranch() now prefers origin/HEAD over the local repo state, which changes behavior for repos whose local checked-out default branch intentionally differs from the remote default. In that case new session worktrees will suddenly branch from the remote default branch instead of the local base branch, potentially creating sessions from the wrong code line. Consider preferring an explicit local branch/ref when present (e.g. current repo default branch or configured branch) and only using origin/HEAD as a fallback. [fixable]
Address Codex review: document why origin/HEAD is preferred over local HEAD for session worktrees — canonical default branch, not local state. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dimakis
left a comment
There was a problem hiding this comment.
Codex Review
Found 1 issue(s) (1 warning).
server/worktree.ts
PR fixes the HEAD contamination bug and adds strong coverage, but the new branch-reset fallback can fail when the branch is checked out in another worktree.
- 🟡 bugs (L148): The fallback path force-resets an existing session branch with
git branch -fbefore attaching it. If that branch is currently checked out in another existing worktree, Git rejects force-updating it (cannot force update the branch ... checked out at ...), socreateWorktree/createWorktreeAsyncwill now fail in a case that previously succeeded. This can happen if a stale branch remains attached elsewhere or concurrent session setup races on the same session id.[fixable]
| const message = err instanceof Error ? err.message : String(err); | ||
| if (message.includes('already exists')) { | ||
| // Branch exists but no worktree — reset it to startPoint before attaching | ||
| execFileSync('git', ['-C', baseRepo, 'branch', '-f', branch, startPoint], { |
There was a problem hiding this comment.
🟡 bugs: The fallback path force-resets an existing session branch with git branch -f before attaching it. If that branch is currently checked out in another existing worktree, Git rejects force-updating it (cannot force update the branch ... checked out at ...), so createWorktree/createWorktreeAsync will now fail in a case that previously succeeded. This can happen if a stale branch remains attached elsewhere or concurrent session setup races on the same session id. [fixable]
Summary
createWorktree()andcreateWorktreeAsync()calledgit worktree add -b <branch> <path>without a start-point, causing new sessions to inherit whatever HEAD was in the base repo — often another session branch with unmerged commitsstartPointoption (default'main') as the final argument togit worktree addstartPointbefore attachingContext
Discovered while investigating mgmt PR #105, where a design doc PR inherited 7 unrelated
jira_processcommits from a prior session. The root cause: worktree creation branched from the repo's current HEAD (a stale session branch) instead ofmain.Test plan
startPointoption respectedstartPointon fallback🤖 Generated with Claude Code