Skip to content

fix(worktree): branch from main to prevent cross-session contamination#322

Merged
dimakis merged 5 commits into
mainfrom
fix/worktree-start-point
May 17, 2026
Merged

fix(worktree): branch from main to prevent cross-session contamination#322
dimakis merged 5 commits into
mainfrom
fix/worktree-start-point

Conversation

@dimakis
Copy link
Copy Markdown
Owner

@dimakis dimakis commented May 14, 2026

Summary

  • createWorktree() and createWorktreeAsync() called git 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 commits
  • Fix: add startPoint option (default 'main') as the final argument to git worktree add
  • When falling back to an existing branch, reset it to startPoint before attaching

Context

Discovered while investigating mgmt PR #105, where a design doc PR inherited 7 unrelated jira_process commits from a prior session. The root cause: worktree creation branched from the repo's current HEAD (a stale session branch) instead of main.

Test plan

  • New test: worktree branches from main, not HEAD (verifies the bug fix)
  • New test: custom startPoint option respected
  • New test: existing branch reset to startPoint on fallback
  • All 35 worktree tests pass
  • Lint + format pass (pre-commit hooks)

🤖 Generated with Claude Code

@dimakis
Copy link
Copy Markdown
Owner Author

dimakis commented May 14, 2026

Centaur Review

Found 4 issue(s) (3 warning).

server/worktree.ts

Good fix for a real cross-session contamination bug. The hardcoded 'main' default is the main concern — it breaks portability for repos not using main as default branch. Tests need git init -b main to be reliable, and the sync createWorktree variant lacks test coverage for the new behavior.

  • 🟡 unsafe_assumptions (L74): Hardcoded 'main' as default startPoint. Repos using master or another default branch will fail with fatal: not a valid object name: 'main'. Secondary repos configured in .mitzo.json repos are particularly at risk since they're external. Consider detecting the default branch dynamically (e.g. git symbolic-ref refs/remotes/origin/HEAD) or reading from .mitzo.json per-repo config. [fixable]
  • 🔵 regressions (L79): The 'reuse existing worktree' early return (lines 79-87 sync, 145-151 async) does not reset the branch to startPoint. If a worktree from a prior run still exists and is valid but points to contaminated commits, it will be reused as-is, leaving the contamination bug unfixed for that path. This is likely acceptable since session IDs include random hex making collisions improbable, but worth documenting.

server/__tests__/worktree.test.ts

Good fix for a real cross-session contamination bug. The hardcoded 'main' default is the main concern — it breaks portability for repos not using main as default branch. Tests need git init -b main to be reliable, and the sync createWorktree variant lacks test coverage for the new behavior.

  • 🟡 missing_tests: The sync createWorktree function received the same startPoint changes as createWorktreeAsync, but has no corresponding tests. The sync version is the one used in production by createSessionWorktrees (worktree.ts:272) and app.ts:375. The async variant's tests don't cover the sync code path. [fixable]
  • 🟡 bugs (L127): Test beforeEach uses git init without -b main, so the default branch name depends on the system's init.defaultBranch git config. On systems where it defaults to master, all three new tests will fail because the startPoint default resolves 'main' which won't exist. Use git init -b main to make tests self-contained. [fixable]

dimakis added a commit that referenced this pull request May 16, 2026
… 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>
Copy link
Copy Markdown
Owner Author

@dimakis dimakis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 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]
  • 🟡 regressions (L49): 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]
  • 🔵 unsafe_assumptions (L38): 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]

Comment thread server/worktree.ts Outdated
{ 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();
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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]

Comment thread server/worktree.ts Outdated
} catch {
// No origin or symbolic-ref not set — fall back
}
return 'main';
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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]

Comment thread server/worktree.ts Outdated
*/
export function detectDefaultBranch(repoPath: string): string {
try {
const ref = execFileSync(
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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]

@dimakis
Copy link
Copy Markdown
Owner Author

dimakis commented May 16, 2026

/review

dimakis added a commit that referenced this pull request May 16, 2026
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>
dimakis and others added 4 commits May 17, 2026 17:56
…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>
@dimakis dimakis force-pushed the fix/worktree-start-point branch from db3e380 to 26313ee Compare May 17, 2026 16:56
@dimakis
Copy link
Copy Markdown
Owner Author

dimakis commented May 17, 2026

/review

@dimakis dimakis added the openai Trigger Codex review label May 17, 2026
Copy link
Copy Markdown
Owner Author

@dimakis dimakis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 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]

Comment thread server/worktree.ts
* 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 {
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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>
Copy link
Copy Markdown
Owner Author

@dimakis dimakis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 -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]

Comment thread server/worktree.ts
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], {
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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]

@dimakis dimakis merged commit 07b5fff into main May 17, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

openai Trigger Codex review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant