Skip to content

fix(worktrees): detect git D/F ref conflicts before create (STA-4762) - #15383

Open
Jinwoo-H wants to merge 3 commits into
mainfrom
jinwoo/sta-4762-branch-df-ref-conflict
Open

Jinwoo-H wants to merge 3 commits into
mainfrom
jinwoo/sta-4762-branch-df-ref-conflict

Conversation

@Jinwoo-H

@Jinwoo-H Jinwoo-H commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 4 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​406 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​406
Prod 6 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​240 $\color{#cf222e}{\Huge{\mathbf{−}}}$​12 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​228

Summary

STA-4746 is invalid — on current main and on the reported v1.4.184. This PR contains no production change, only the regression coverage that proves it and guards the contract.

The report claimed that in a paired/remote setup a folder workspace (ORCA_WORKTREE_ID=folder:<uuid>) makes the relay spawn its PTY in a fallback cwd, because "relay cwd resolution only consults ORCA_WORKTREE_ID, parses it as repoId::path, and never falls back to ORCA_WORKSPACE_ROOT."

That is not how the relay resolves cwd. PtyHandler.spawnAfterAdmission takes cwd from the spawn request (params.cwd); splitWorktreeId(worktreeId) feeds only the beginPtyCreation admission key. The client side already resolves folder:<uuid> through resolveTerminalStartupCwdForWorkspace → store.getFolderWorkspace(id).folderPath.

What actually produces the reported symptom

On every POSIX relay branch the host's login startup chain runs after the PTY is placed, so a remote startup file that cds wins after the shell already started in the workspace root. Two mechanisms get there:

  • zsh and other POSIX shells launch with POSIX_LOGIN_ARGS = ['-l'] (src/relay/pty-shell-launch.ts:12), so /etc/profile, /etc/profile.d/* and ~/.zprofile are sourced.
  • bash is normally wrapped (--rcfile, not -l), but the relay's own wrapper explicitly sources /etc/profile, then ~/.bash_profile / ~/.bash_login / ~/.profile (src/relay/pty-shell-overlay-wrappers.ts:63-70).

That is exactly the reporter's own evidence:

OLDPWD=/opt/tiger/workspace          # shell started here
PWD=/opt/tiger/rh2/rh2/init          # then cd'd away

OLDPWD is only ever set by cd. Phase B of the SSH spec reproduces that byte-shape by adding one /etc/profile.d entry to the container. Phase C shows the same cd moves a plain repoId::/path git worktree identically, so this mechanism is not folder-specific. That establishes a non-folder-specific cause for the reported signature; combined with the topology table below finding no folder-specific defect, it is why this closes as invalid.

Evidence (all on clean main @ 1b5cc00)

Topology Result
Headed paired desktop server + paired desktop client (primary) folder PTY lands on host folderPath; filesystem marker written into folderPath; git-worktree + host-local controls green
Headless orca serve paired host terminal.create(folder:<uuid>) → pwd == folderPath
Real Docker SSH relay (~/.orca-remote/relay-0.1.0+…) shell $PWD and remote /proc/<pid>/cwd both == folder path

Version oracle

The cwd-resolution logic is behaviourally identical at v1.4.184: src/shared/terminal-startup-cwd.ts and src/shared/workspace-scope.ts differ only by import-path moves (shared/worktree-id → shared/worktree/id, shared/types → shared/folder-workspace-types), and src/relay/pty-handler.ts still takes cwd from params.cwd. Those files carry unrelated changes since the tag — the equivalence claim is about the cwd path, not the whole diff. v1.4.184 also did not send worktreeId as a spawn param at all (added later in ssh-pty-spawn-request.ts for shell-history isolation), so the relay could not have derived cwd from it there either. Nothing was "already fixed".

Separately, on node-pty's POSIX implementation a nonexistent cwd makes the child exit 1 rather than silently inheriting the parent's cwd (node_modules/node-pty/src/unix/pty.cc), ruling out the "fell back to the container's CMD directory" mechanism as well. Windows uses a different spawn path and was not measured.

Why the proposed fix is not implemented

The proposed ORCA_WORKSPACE_ROOT fallback would change nothing for the reporter — cwd is already correct at spawn — and it would make the relay adopt an unvalidated client-supplied path for a request that named no working directory.

cwd is optional on the wire (PtySpawnOptions.cwd, src/main/providers/pty-provider-contract.ts:42), and the relay's default-cwd branch exists for requests that genuinely name none. But every relay-bound caller for a folder workspace resolves a path first via resolveTerminalStartupCwdForWorkspace; the one in-repo caller that can omit cwd (codex-detached-pane-restart.ts:210) is local-only and never reaches the relay. So the fallback has no relay caller to serve, and promoting an env var into it would only widen what the relay trusts.

Coverage added

  • src/relay/pty-handler-folder-workspace-cwd.test.ts — 5 deterministic cases: folder id honours request cwd, folder id with no ORCA_WORKSPACE_ROOT, worktree-id precedence unchanged, legacy/mixed-version client (env-only ORCA_WORKTREE_ID), and the default-cwd fallback pinned to not adopt ORCA_WORKSPACE_ROOT.
  • tests/e2e/sta4746-folder-remote-cwd.spec.ts — headless paired orca serve host.
  • tests/e2e/sta4746-paired-desktop-folder-cwd.spec.ts — headed paired desktop server + client (@headful), with git-worktree and local-only controls.
  • tests/e2e/sta4746-ssh-folder-cwd.spec.ts — real Docker SSH relay, 3 phases (clean / login-profile cd / git-worktree control), gated on ORCA_E2E_SSH_DOCKER=1 like the sibling docker specs.

Guard is non-vacuous: injecting the alleged id-derived cwd into pty-handler.ts turns 4 of the 5 relay cases red; reverting the injection returns 5/5 green.

Commands

npx vitest run --config config/vitest.config.ts src/relay/pty-handler-folder-workspace-cwd.test.ts
npx playwright test tests/e2e/sta4746-folder-remote-cwd.spec.ts --config tests/playwright.config.ts --project electron-headless
npx playwright test tests/e2e/sta4746-paired-desktop-folder-cwd.spec.ts --config tests/playwright.config.ts --project electron-headful
ORCA_E2E_SSH_DOCKER=1 npx playwright test tests/e2e/sta4746-ssh-folder-cwd.spec.ts --config tests/playwright.config.ts --project electron-headless

Gaps

CI runs all three specs on Linux runners (the changed-e2e job picked them up and all 3 passed), and the SSH phases were additionally exercised against a Linux container from a macOS host. Windows and WSL hosts were not exercised.

Closes STA-4746 as invalid.

@coderabbitai

coderabbitai Bot commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds shared Git branch-reference conflict utilities. Repository and SSH worktree flows now distinguish blocking prefixes from suffixable directory/file collisions. Worktree and runtime flows stop suffix retries for unsuffixable conflicts and use shared conflict messages. Workspace error classification recognizes existing branch-name collisions as path_collision. Tests cover ref probing, conflict classification, message formatting, repository probes, suffix selection, retry termination, and SSH worktree creation.

Merge Risk: 🔵 Low · up to 90cac

The change is mergeable with owner awareness: conflict handling may omit worktree-name guidance in some error messages, and the SSH conflict regression test may not fully validate ancestor-ref probing because of a broad mock.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is largely unrelated to the changeset and omits the required template sections, describing STA-4746 instead of the STA-4762 branch-conflict fix. Rewrite the description to explain the D/F branch-reference conflict fix, link STA-4762, document testing, and complete the required template sections.
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: detecting Git directory/file branch-reference conflicts before worktree creation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Jinwoo-H added a commit that referenced this pull request Aug 19, 2026
…se gap

Review follow-up from three independent gpt-5.6-luna reviewers on PR #15383.

Suffixes are appended to the last segment of a branch name, so every proper
prefix survives them: if `release` blocks `release/1.0`, it blocks
`release/1.0-2` through `release/1.0-100` too. The create loops treated that
conflict like any other and ground through all 100 candidates before failing.
Locally that is ~2.7s of wasted work; over SSH each attempt is several
sequential remote round trips, so the same dead end costs roughly 400 RPCs.

Split the detected collision by direction:

- `local-directory` (refs nest under the requested name) stays suffixable —
  `feature` still resolves to `feature-2` with no user-visible error.
- `local-ref-prefix` (an existing ref is a path prefix) is unsuffixable, so all
  three create loops now break instead of continuing.

The message for the unsuffixable case says so, and points at the real fix —
rename or delete the blocking branch — rather than "pick a different worktree
name", which cannot help.

Also documents a limitation the probe does not close: `for-each-ref` matches
ref names byte-for-byte, while the ref store on macOS/Windows is usually
case-insensitive and may normalize unicode, so `Feature/x` blocking `feature`
(or an NFC ref blocking an NFD name) still reaches git. `--ignore-case` is
deliberately not used — on a case-sensitive volume git genuinely allows both
`feature` and `Feature/x`, so it would block names git accepts. A false
positive is worse than the raw error it would avoid; those cases fail exactly
as they do today.

Reviewers independently confirmed the reachability verdict (the bug is
reachable on main today, without PR #15104) and verified multi-pattern
`for-each-ref` on real Git 2.25.1 in an Ubuntu 20.04 container.

Regression test asserts the SSH create path issues fewer than 5 ref probes
instead of 100; reverting the break makes it fail with "expected 100".

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a59dd03f-c15b-4ec0-b69e-67bdf7e10ef7

📥 Commits

Reviewing files that changed from the base of the PR and between 635c5c3 and 05da25a.

📒 Files selected for processing (7)
  • src/main/git/branch-ref-conflict.test.ts
  • src/main/git/branch-ref-conflict.ts
  • src/main/git/repo.test.ts
  • src/main/git/repo.ts
  • src/main/ipc/worktree-remote.ts
  • src/main/ipc/worktrees-ssh-branch-conflict-suffixing.test.ts
  • src/main/runtime/orca-runtime.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review.

Comment on lines +270 to +274
if (args[0] === 'for-each-ref') {
return args.some((arg) => arg.startsWith('refs/heads/'))
? { stdout: 'refs/heads/release\n', stderr: '' }
: { stdout: '', stderr: '' }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Match the blocking ref probe exactly.

This mock returns refs/heads/release for every local-ref query. The test can pass if the implementation probes an unrelated ref or the requested branch instead of its blocking prefix.

Return the blocking ref only when the command requests the release ancestor pattern. Assert that the expected ancestor probe occurred.

@Jinwoo-H

Copy link
Copy Markdown
Contributor Author

CI red is not from this PR — main is broken for every open PR.

The 5 failures are 4 test shards plus the verify aggregator, and all of them come from two files this PR does not touch:

  • src/main/ai-vault/session-scanner-parse-wsl-stall.test.ts
  • src/main/ai-vault/session-scanner-index-cache-wsl-stall.test.ts

Cause: #15381 (4ec6bbf588, "Kill hung WSL transcript filesystem operations via child process with route quarantine") landed on main at ~00:32 and rewrote wsl-transcript-fs-gate.ts / wsl-transcript-fs-access.ts. It updated session-scanner-core-parser-wsl-stall.test.ts but left the two files above un-updated against the new gate behavior — they still drive it with fake timers via WSL_TRANSCRIPT_FS_SCAN_TIMEOUT_MS.

Evidence:

No action needed on this branch. This needs a fix on main (update the two stale tests against the new gate, or revert #15381); every open PR is blocked until then.

Git stores refs as files, so `feature` and `feature/x` can never both exist.
`getBranchConflictKind` only probed the exact ref, so neither direction was
detected and `git worktree add -b` failed with raw text surfaced through IPC:

  fatal: cannot lock ref 'refs/heads/feature':
  'refs/heads/feature/tti_fix_1440' exists; cannot create 'refs/heads/feature'

This is reachable on main today, independent of PR #15104 (still open). The
new name never needs a slash: any repo with a nested branch (`feature/x`,
`renovate/...`, `dependabot/...`) blocks the flat name typed by the user. The
reverse direction needs a slash, which main already produces via the default
`git-username` branch prefix, a custom prefix, or a create-from-review
`branchNameOverride` such as `release/1.0` over an existing flat `release`.

One `for-each-ref` covers both directions: a pattern matches a ref completely
or up to a `/`, so the requested name's own pattern finds refs nested under it
while each proper-prefix pattern finds a shorter ref blocking it. Prefix
patterns over-match, so a prefix only counts on an exact match.

A detected conflict returns the new `local-directory` kind, which the existing
suffix loops already treat as "try the next candidate" — `feature` resolves to
`feature-2` with no user-visible error. The fallback message no longer claims
the branch "already exists", which would be false: no ref has that exact name.

Wired into the local, WSL, SSH, and runtime create paths. Probe failures are
swallowed so they can never block a create git would accept.

- `for-each-ref` multi-pattern predates the Git 2.25 baseline; no capability
  gate needed. Every argv element is `refs/heads/`-prefixed, so a branch name
  can never be read as an option.
- Provider-neutral: classification is local-ref-only, so GitHub/GitLab and the
  remote-conflict path are untouched.
- Telemetry buckets the new message as `path_collision`, not `git_failed`.
…se gap

Review follow-up from three independent gpt-5.6-luna reviewers on PR #15383.

Suffixes are appended to the last segment of a branch name, so every proper
prefix survives them: if `release` blocks `release/1.0`, it blocks
`release/1.0-2` through `release/1.0-100` too. The create loops treated that
conflict like any other and ground through all 100 candidates before failing.
Locally that is ~2.7s of wasted work; over SSH each attempt is several
sequential remote round trips, so the same dead end costs roughly 400 RPCs.

Split the detected collision by direction:

- `local-directory` (refs nest under the requested name) stays suffixable —
  `feature` still resolves to `feature-2` with no user-visible error.
- `local-ref-prefix` (an existing ref is a path prefix) is unsuffixable, so all
  three create loops now break instead of continuing.

The message for the unsuffixable case says so, and points at the real fix —
rename or delete the blocking branch — rather than "pick a different worktree
name", which cannot help.

Also documents a limitation the probe does not close: `for-each-ref` matches
ref names byte-for-byte, while the ref store on macOS/Windows is usually
case-insensitive and may normalize unicode, so `Feature/x` blocking `feature`
(or an NFC ref blocking an NFD name) still reaches git. `--ignore-case` is
deliberately not used — on a case-sensitive volume git genuinely allows both
`feature` and `Feature/x`, so it would block names git accepts. A false
positive is worse than the raw error it would avoid; those cases fail exactly
as they do today.

Reviewers independently confirmed the reachability verdict (the bug is
reachable on main today, without PR #15104) and verified multi-pattern
`for-each-ref` on real Git 2.25.1 in an Ubuntu 20.04 container.

Regression test asserts the SSH create path issues fewer than 5 ref probes
instead of 100; reverting the break makes it fail with "expected 100".
… message

The runtime path passes no subject and the IPC paths pass 'worktree name' or
'branch name'; the new message hardcoded 'branch name' for all three.
@Jinwoo-H
Jinwoo-H force-pushed the jinwoo/sta-4762-branch-df-ref-conflict branch from 05da25a to 90cac3a Compare August 19, 2026 04:56

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/runtime/orca-runtime.ts (1)

24447-24481: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Pass 'worktree name' to formatBranchConflictMessage.

Without a subject, local, remote, and local-directory conflicts omit the guidance to choose a different worktree name.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7443eb42-4e33-451e-a858-881201bd13c6

📥 Commits

Reviewing files that changed from the base of the PR and between 05da25a and 90cac3a.

📒 Files selected for processing (1)
  • src/main/runtime/orca-runtime.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.

@Jinwoo-H

Copy link
Copy Markdown
Contributor Author

Rebased onto latest main — CI is now fully green (45 pass, 0 fail).

The earlier red was the main breakage described above, not this branch. main has since landed the fix for it, and all four previously-failing shards (tests node 24/26 6/16 and 14/16) now pass on this PR. Rebase was clean, no conflicts, no changes to the diff.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant