Repository navigation
fix(worktree): update local main safely, once per branch, alongside the checkout - #23698
Conversation
Review status: ready for a maintainerProblem this PR fixes: with "refresh local main when creating a workspace" on, every create waited for the local What users get now: the local Review: three independent full passes.
Live QA (Electron, on a separate Mac): before = this PR's merge-base, after = this PR. The after arm was built at 31c427c; later commits changed only a comment and tests.
Screenshots are embedded in the PR body. The QA seat was a Claude Opus agent, because the planned Grok seat had no usage left (402). Known limits, stated in the PR body:
Not caused by this PR:
Validation:
|
|
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (7)
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe change adds shared local-base inspection and fast-forward operations, including lock-contention retries and per-key refresh coordination. Local and SSH worktree creation now start eligible refreshes alongside worktree addition and await their outcomes. The relay exposes inspection and refresh operations, and the renderer keys refresh-failure notifications by repository and local branch. Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to The change refreshes the local base branch safely during worktree creation. The supplied evidence shows no remaining merge-blocking issue. The earlier concern about test isolation from global Git config appears fixed, because the test now stubs those settings. Security Architecture ReviewSecurity architecture risk: 🟡 Moderate · up to Normal branch updates gain stronger protections against overwriting work. However, read-only checks from older clients may instead update a branch during mixed-version upgrades. Retained concerns
Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
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. Comment |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Read the full 15-file diff end to end, then validated the runtime behavior against the relay and renderer consumers and ran the touched/adjacent suites.
- Overlapped refresh —
resolveWorktreeAddBaseContextnow starts the local base refresh and returns it aspendingLocalBaseRefRefreshinstead of awaiting it, sogit worktree addruns concurrently from the remote-tracking commit.performAddWorktree,finalizePreparedWorktree, and SSHcreateRemoteWorktreeeach settle the promise on their exit paths, including failure/rollback paths. - One refresh per repo — the new
keyed-serial-runnerserializes refreshes for the same repo (and the same SSH connection+path) with a 30s max wait; the Codex home lock now uses the same runner. - Lock-contention retry —
git-lock-contention.tsretries git lock errors only (not CAS "is at X but expected Y"), re-checks owner cleanliness per attempt, and treats "local already at target" as success rather than a warning; the SSH path retries the relay call with the same already-current check. - Already-current and self-branch creates — an up-to-date local base now returns
undefined(no owner check, no reset, no toast), and a create whose-bis the base's own local branch skips the refresh entirely. - Tests — new contention, real-git lock, SSH-overlap, runner, and lock-matcher suites; existing refresh/preparation suites updated for the overlap.
Verification this run: the 12 touched/adjacent test files pass, pnpm tc:node and pnpm run check:code-quality:changed are clean, and I confirmed the SSH lock matcher is not theatre — the relay error is reconstructed client-side from msg.error.message, which carries git stderr, so isGitLockContentionFailure sees real lock text.
The two documented limits (queue-wait gating the create return; case-insensitive filesystems not recognizing Feature-X as the self-branch) are real and correctly called out in the PR description rather than hidden.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
3564650 to
e616bc1
Compare
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Re-read the full 31-file diff end to end (no incremental diff was computable because the prior head 3564650 was never fetched into this shallow checkout). The head commit replaced the previously reviewed design wholesale, so the delta is effectively that redesign:
- Unsafe move replaced with Git's own fast-forward — a checked-out branch now moves with
git merge --ff-onlyunder Git's own index lock (hooks, auto-gc, autostash off, after re-confirmingsymbolic-ref HEAD), which refuses to overwrite an edit/untracked file or drop a commit; a free branch moves with a CASupdate-refcarrying a reflog message. Failure classification is exact (would be overwritten by merge→ dirty,Not possible to fast-forward→ not-ff, else error), and a failed move where local already contains the target reportsupdated. - One policy, running where Git runs —
src/shared/worktree/local-base-branch-fast-forward.tsis injected into the main process (local/WSL) and the relay; the app-side SSH duplicate of the checks, queue and retry is deleted, along withworktree-base-refresh-analysis.ts. - One coalescing run per repo and branch —
coalescing-keyed-runner.tsgives a leading run plus one shared trailing run that uses the latest requester's work; creates wait at most 30 s and never start a competing mutation. Lock contention retries on.lock: File existsonly, deliberately not on the CASis at X but expected Ymismatch. - Overlap — the refresh is started (not awaited) in
resolveWorktreeAddBaseContextand settled inperformAddWorktree,finalizePreparedWorktree, and the SSHcreateRemoteWorktreefinallybefore any sparse rollback; the checkout always builds from the remote-tracking ref, so it never depended on local base. - False-alarm reductions — already-current bases return
nothing_to_dowith no owner check or toast, self-branch creates skip the refresh, status reads use--no-optional-locks, and the warning toast is keyed by repo+branch.
I traced the failure/retry/coalescing paths and the relay wire handling (parseLocalBaseBranchFastForwardOutcome rejects unrecognized replies; relay runGitToTermination preserves numeric code/stderr, so isShowRefNoMatchError and the lock matcher behave the same as local). The new real-Git and contention suites assert exact arguments and statuses and would fail against the old reset --hard path.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 0dfaeeb8-61cf-46d7-a158-b7121a5fd142
📥 Commits
Reviewing files that changed from the base of the PR and between 3564650 and e616bc18884eefe187c98573e4fc867af77d93c0.
📒 Files selected for processing (28)
src/main/git/exact-ref-probe.tssrc/main/git/worktree-add-local-base-refresh.test.tssrc/main/git/worktree-add-local-base-suggestion.test.tssrc/main/git/worktree-add.tssrc/main/git/worktree-base-refresh-analysis.tssrc/main/git/worktree-base-refresh-contention.test.tssrc/main/git/worktree-base-refresh-lock-real-git.test.tssrc/main/git/worktree-base-refresh.tssrc/main/git/worktree-preparation-base-oid.test.tssrc/main/ipc/worktree-remote.tssrc/main/ipc/worktrees-ssh-local-base-refresh-overlap.test.tssrc/main/ipc/worktrees-ssh-local-base-refresh.test.tssrc/main/providers/ssh-git-provider-api.test.tssrc/main/providers/ssh-git-provider-worktree.test.tssrc/main/providers/ssh-git-worktree-provider.tssrc/relay/git-handler-local-base-ref-refresh.tssrc/relay/git-handler-registration.tssrc/relay/git-handler-worktree-operations.tssrc/relay/git-handler-worktree-provisioning.test.tssrc/relay/git-handler.test.tssrc/renderer/src/store/slices/worktrees-create-base-status.test.tssrc/renderer/src/store/slices/worktrees/create/local-base-ref-refresh-toast.tssrc/shared/coalescing-keyed-runner.test.tssrc/shared/coalescing-keyed-runner.tssrc/shared/git-show-ref-no-match.tssrc/shared/worktree/local-base-branch-fast-forward-real-git.test.tssrc/shared/worktree/local-base-branch-fast-forward.test.tssrc/shared/worktree/local-base-branch-fast-forward.ts
💤 Files with no reviewable changes (1)
- src/main/git/worktree-base-refresh-analysis.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 4 remain after this review.
…nd skip false alarms
…un one refresh per repo at a time
… branch itself Creating a workspace named feature-x from origin/feature-x runs `worktree add -b feature-x`, which now overlaps the refresh. The refresh's drift probe could see refs/heads/feature-x missing and its presence probe then see it (the add just wrote it), which reported "not fast-forward" and showed a sticky "Local feature-x was not refreshed" warning. `-b` refuses an existing branch, so there is nothing to refresh in that case: skip it on the local, prepared-checkout and SSH create paths. The SSH overlap tests move to their own file so the existing suite stays under the line limit.
…queue wait expires
…-only per branch Moves the whole local base refresh into one shared routine that runs on the execution host (main process for local and WSL repos, the relay for SSH), so the app no longer keeps a second copy of the checks, queue and retry. A checked-out branch now moves with merge --ff-only (hooks, auto-gc and autostash off) instead of status-then-reset --hard, which silently overwrote an untracked file the new commit adds and could discard an edit or a commit made after the check. A free branch moves with a compare-and-swap update-ref that writes a reflog message. Status reads no longer take index.lock. Creates of one branch share one run plus at most one trailing run; a create waits at most 30 s and never starts a competing mutation. The failure toast is keyed by repo and branch because every create that joined a run reports the same fact.
…gned merges With merge.verifySignatures=true, the owner-checkout fast-forward refused an unsigned origin/main tip, so every create warned "Local main was not refreshed" where the old reset moved main. The new workspace is already created from that same unsigned commit, and a branch that is not checked out moves without a signature check, so the refusal protected nothing. Turn the setting off for this one merge, like the hooks, gc and autostash overrides.
…lands late The update of local main can finish after a create stopped waiting for it, so the shared run now invalidates git read caches itself. The index.lock real-git test also no longer reads the developer's global git config.
…ocal main A plain `git merge --ff-only` silently replaces an ignored file (for example a local `.env`) at a path the new commit starts tracking. Pass `--no-overwrite-ignore` so git refuses instead and the create reports the checkout as having local changes. Supported on the fast-forward path since well before Git 2.25. Also make the relay test for one-refresh-per-branch hold the first merge until the second request has reached the relay, so it fails without the coalescing.
…er the user's merge settings say A per-branch mergeOptions such as '-s ours' or '--squash', or pull.twohead=ours, made the update create a merge commit that dropped upstream, or stage upstream without moving main, while reporting success. The command now clears the branch's mergeOptions and passes the strategy and signature choice on the command line, which beats any config. After the move Orca confirms local main is exactly the target before reporting it updated. The exact command also runs in the Git 2.25 compatibility suite.
… rerun on every change to it The new real-Git contract for the local main fast-forward wrote a post-merge hook into .git/hooks, which does not exist when the repo is created by the uninstalled Git 2.25.5 build CI uses (no templates), so the Git compatibility check failed. Create the directory first. The Git compatibility check also did not run when only the fast-forward module changed, so a later edit to its merge arguments (for example a flag Git 2.25 lacks) would skip the one check that tests them. Add the module to the check's paths.
71af170 to
3613960
Compare
Review status: ready (head
|
There was a problem hiding this comment.
ℹ️ Minor suggestions only.
Reviewed changes
Read the full 34-file diff at 3613960 end to end. The prior pullfrog review was at e616bc1; the branch was rebased after it, so this covers the whole head range ed4da90…3613960, with the newest commits being the merge-settings/ignored-file/signed-merge hardening and the Git 2.25 CI contract.
- One shared refresh policy on the execution host —
src/shared/worktree/local-base-branch-fast-forward.tsmoves localmainwithgit merge --ff-onlywhen it is checked out, or a compare-and-swapupdate-refwhen it is free; the app-side SSH copy of the checks, queue and retry is deleted. - One run per repo+branch — new
src/shared/coalescing-keyed-runner.tscollapses a burst into a leading run plus one shared trailing run; the main process and the relay each own one. - Overlapped with the checkout — the refresh is started, not awaited, and settled on every exit path (
worktree-add.ts,worktree-create-preparation.ts,worktree-remote.ts); the relay now performs the whole refresh host-side and returns the outcome. - Safety hardening —
--no-overwrite-ignore, clearedbranch.<name>.mergeOptions,--no-verify-signatures, hook/auto-gc/autostash suppression, a post-move exact-OID check,--no-optional-locks status, lock-contention retry, and a repo+branch-keyed warning toast.
I checked the move command against Git's manual and source (--no-overwrite-ignore, --no-verify-signatures, the -c branch.<name>.mergeOptions= parse for dotted and slashed branch names, and the refusal messages the classifier matches), and the real-git tests carry live controls, so they can fail against the old command. One residual race and one leftover are below.
ℹ️ Nitpicks
probeWorktreeBaseRefPresence(src/main/git/worktree-base-ref-probe.ts:101) lost its last production caller in this PR —worktree-remote.tsnow routes through the shared routine — and survives only via its unit test. Consider deleting it (and itsisShowRefNoMatchErrorimport) or leaving a note on why it is retained.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
…ocal-base-refresh # Conflicts: # src/main/providers/ssh-git-provider-api.test.ts
…s own base Creates from different remotes' main (origin/main and upstream/main) shared one queued update per repo and branch, which ran only the latest caller's target: a create could get no result for its own base, or a false "not refreshed" warning computed for another remote's main. The per-branch runner now queues one run per distinct target, still one at a time per branch, and only callers toward the same target share a queued run. Applied in the app and the relay.
| const joinable = queue?.find((run) => run.shareKey === shareKey) | ||
| if (joinable) { | ||
| joinable.work = work | ||
| return joinable.result | ||
| } | ||
| const run = createQueuedRun(shareKey, work) | ||
| if (queue) { | ||
| queue.push(run) |
There was a problem hiding this comment.
Already-contained target triggers warning
When overlapping creates request origin/main, then a newer upstream/main, then origin/main again, this queue runs all three targets in order. If upstream contains origin, the upstream run moves local main past origin/main. The final origin run then reports skipped_not_fast_forward, showing a “Local main was not refreshed” warning even though local main already contains that create’s requested base.
…burst tests and update the toast id rationale
There was a problem hiding this comment.
✅ No new issues found. The new commits keep the fast-forward correct and make every create answer toward its own base.
Reviewed changes
Reviewed the delta since the prior pullfrog review at 3613960 (head is now 52e48a8): one substantive commit plus the earlier merge-only sync. The substantive commit reworks the coalescing runner so a create is never answered with a different remote's refresh outcome.
- Coalescing runner now shares per
shareKey—createCoalescingKeyedRunnergained a thirdshareKeyargument: a caller joins a queued (not-yet-started) run with the sameshareKeyand the run uses the latest joiner'swork; otherwise it queues its own run. Runs drain in arrival order, so a burst costs at most one run per distinctshareKeybeyond the one in flight, and distinct remote targets no longer overwrite each other's work. - Refresh callers pass the remote-tracking ref as the share key —
worktree-base-refresh.tsand the relay'sgit-handler-local-base-ref-refresh.tspassremoteTrackingRef, leaving the per-repo+branchkeyunchanged so moves of one local branch still never overlap. - Tests — real-git fork-repo cases for two remotes (each asserting
maxConcurrent === 1and exact outcomes, and each failing against the old shared-trailing-run behavior), relay provisioning cases holding the first merge open while later clients queue, and runner unit cases for cross-shareKeyordering/bounding. - Comment honesty — the owner fast-forward comment now states the
symbolic-refre-check narrows but cannot close the branch-switch window, retiring the prior review's residual-race thread.
I traced the runner's queue/drain interaction (a run is only joinable while queued, queues.delete runs synchronously after the loop drains, settled swallows rejections) and found no interleaving that drops a queued run or runs two moves of one branch concurrently. The new tests discriminate against the previous behavior rather than passing under it.
ℹ️ The PR description's cross-remote "Known limit" is now stale
The "Known limits" bullet still says two creates from different remotes "share one update, which uses whichever request came last." This commit removes exactly that behavior, so the description now understates the design and contradicts the code.
Technical details
# Stale PR description for the cross-remote refresh behavior
## Affected sites
- PR body, section `## Known limits`, final bullet — states creates toward different remotes "share one update, which uses whichever request came last".
- `src/shared/coalescing-keyed-runner.ts` — now keys sharing by `shareKey` (the remote-tracking ref), so creates toward different remotes queue independent sequential runs.
## Required outcome
- The description should describe the current behavior: one sequential run per distinct remote target, with local ending at the last-arriving target; a create toward a remote that local has already moved past then reports `skipped_not_fast_forward` rather than being answered with another remote's result.
## Open questions for the human
- Is the third-create warning in `worktree-base-refresh-lock-real-git.test.ts:176` (a create toward `origin/main` after local moved to `upstream/main`) the intended signal, or should an already-contains-target branch read as `nothing_to_do` instead of `skipped_not_fast_forward`?DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
…ocal-base-refresh # Conflicts: # src/main/git/worktree-add-local-base-refresh.test.ts # src/main/git/worktree-add.ts # src/main/ipc/worktree-remote.ts



ELI5
With "keep local main up to date" on, Orca fast-forwards your local
mainto matchorigin/mainevery time it creates a workspace, so agents that compare againstmainsee only their own changes. Two things went wrong. When two workspaces were created at once, both tried to update the same checkout, one lost the race for git's lock file, and you got a "Local main was not refreshed" warning even thoughmainended up correct. And the way Orca movedmain(check that the checkout is clean, then force it withgit reset --hard) could silently destroy your work in the gap between the check and the reset. Now one update ofmainruns per repo at a time, creates from the same base that arrive during it share one follow-up update, and every create reports the result for the base it was created from; the update uses git's own safe fast-forward, which refuses instead of overwriting anything; and it runs alongside the checkout instead of before it.What Changed
What you saw before:
mainwas updated (2026-09-28 incident: the other create'sreset --hardheld.git/index.lockfor 8.3 s on a loaded machine).git worktree addstarted.mainwas already current, and warned about "uncommitted changes" if that checkout had edits, even though there was nothing to update.git status(tracked files only) and then rangit reset --hard. An untracked file at a path the newmainadds was overwritten; an edit saved, or a commit made, between the check and the reset was discarded. I reproduced the untracked-file case with real git; the new tests reproduce all three against the old command.What you see now:
mainalready current and does nothing.mainat the same time (sayorigin/mainandupstream/mainin a fork): each gets the result for the base it was created from.git worktree add(the new workspace is created fromorigin/main, so its contents never depended on localmain).mainalready current: nothing runs, nothing warns.mainis checked out, local-only commits onmain) still warns, once per repo and branch, not once per create..env), or a new commit would be lost, git refuses and you get the "uncommitted changes" or "cannot be fast-forwarded" warning instead.Mechanism:
src/shared/worktree/local-base-branch-fast-forward.tsholds the whole policy: read local and remote commits, count how far behind, find the checkout that has the branch, check it is clean, move it. The main process runs it for local and WSL repos; the SSH relay runs it on the remote host. The app-side SSH copy of the checks, the queue and the lock retry are deleted, so local and SSH can no longer disagree.git merge --ff-only <commit>instead ofgit reset --hard. Git applies the move under its own index lock and refuses to overwrite an edit or an untracked file, or to drop a commit that is not in the target;--no-overwrite-ignoremakes it refuse to replace an ignored file too (a plain fast-forward, like the old reset, silently replaces an ignored.envat a path the newmainstarts tracking). Orca first confirms the checkout still has that branch (git symbolic-ref HEAD).git mergereads settings thatresetnever did, and some would turn it into something else: a branch'smergeOptionsof-s ours(orpull.twohead=ours) made a merge commit that dropped upstream's changes,--squashstaged upstream without movingmain, and a signed-merges requirement refused the unsignedorigin/maintip the new workspace was already created from. Orca clears the branch'smergeOptionsfor this one command and passes the strategy (-s recursive) and--no-verify-signatureson the command line, which beats any config. The user's hooks are off (core.hooksPath=/dev/null; the old reset ran no hooks either, and the create waits on this), and so are auto-gc and autostash. After the move Orca confirmsmainis exactly the target before reporting it updated; otherwise it warns. A branch whose name contains=cannot be passed this way and is left alone with a warning.git update-ref, as before: it only moves if the branch is still where Orca read it. It now writes a reflog message ("orca: fast-forward to refs/remotes/origin/main") so you can see what moved your branch.src/shared/coalescing-keyed-runner.ts). Creates toward the same remote's branch that arrive while an update runs share a single queued update, which uses the newest request. A create toward a different remote's branch (origin/mainvsupstream/main) gets its own queued update, and queued updates run one after another, so every create reports the result for the base it was created from. A burst of creates costs one update per distinct base on top of the one already running. Nothing ever starts a second update of the same branch in parallel.git, an editor) is retried after 50, 150 and 400 ms. If it still fails, Orca checks whethermainalready contains the target (someone else updated it) and reports success in that case..git/index.lock(git --no-optional-locks status), so it cannot make your owngit commitin that checkout fail.feature-xfromorigin/feature-x): there is no local branch to update, and probing it while the checkout creates it caused a false "not fast-forward" warning.Why
Root cause: each create ran its own update of the same branch, and the update itself was a check followed by a forced overwrite. Two creates meant two overwrites racing for one lock, and any change between the check and the overwrite was lost. An earlier version of this PR serialized the updates inside the app with a 30 s cap and a lock retry; review showed that past the cap it still started a competing update (the same false warning), that the SSH path kept a second copy of the whole policy in the app, and that the unsafe overwrite remained.
Why this design:
merge --ff-onlymakes a race lose nothing. This is the standard way tools that move a checked-out branch for the user do it.maincurrent", once. It removes the race instead of retrying around it, and bounds the work under a burst.Alternatives considered:
git reset --keep: refuses to overwrite edits and untracked files and runs no hooks, but still moves the branch if you committed in the meantime, dropping that commit frommain. Rejected.git merge --ff-onlywith hooks on: would run a repo's post-merge hook (oftennpm install) in your checkout on every create, and the create would wait for it. Rejected; the old reset never ran hooks.mainwhenever Orca fetchesorigin/main: would move your checkout at arbitrary times. Out of scope; the setting is "on workspace create".Known limits:
mainis being rebased or bisected in some checkout, git lists that checkout as detached, so Orca treatsmainas not checked out and can move it withupdate-ref. Git's own check at the end of the rebase then refuses, so nothing is lost, but that rebase has to be redone. This existed before this PR.reset --hardran before the checkout and held every create forever.mainhas moved toupstream/main, a create fromorigin/mainwarns thatmain"cannot be fast-forwarded", becausemainis now ahead oforigin/main. Nothing is wrong and nothing moves. This existed before this PR; a burst of creates from both remotes now shows it the same way creates made one after another do. This is left for a follow-up: when localmainalready contains the target, Orca will report nothing to do instead of a warning, and warn only when local and the target have truly diverged.Differences from the common pattern
Matches the common pattern: a checked-out branch is moved only by git's own
merge --ff-onlyafter a cleanliness and ancestry check, so a race loses nothing and git's refusal becomes the warning; an already-current branch does nothing even if its checkout is busy or dirty; transient lock failures are retried after 50, 150 and 400 ms; one update of a branch runs at a time; and the work runs on the host that runs git.mainwhen a workspace is created; the common pattern only moves checkouts the app created itself. Kept because agents compare their work against localmain, and a stale one makes their diffs wrong; the setting is opt-in and every move is a fast-forward git itself checks.merge --ff-only. Safe and necessary because this is the user's checkout: their merge settings turned the fast-forward into a merge commit that dropped upstream, a squash, or a refusal, and a plain fast-forward silently replaced an ignored.env.mainadds (shown in the screenshots), and it matches the setting's text, so an unrelated untracked file in that checkout doesn't block every update.update-ref; the common pattern never moves a branch with no checkout. With no working tree there is nothing to overwrite, and the swap only applies if the branch is still where Orca read it.mainalready contains the remote's commits (for example after a create from another remote moved it ahead), Orca warns "cannot be fast-forwarded", where the common pattern treats it as already up to date. This predates this PR; the follow-up reports nothing to do in that case and warns only when the two have truly diverged.Linked Issue
None. This comes from an internal incident report (slow creates and a false warning on 2026-09-28); there is no public issue.
Visual Proof
Live Electron QA on a separate Mac (M4Air, heavily loaded: load average 19-29) against a local fixture repo
appcloned from a local bareorigin, localmain2 commits behindorigin/main, setting on. Before = the refresh code as onorigin/main(no refresh-related file changed between the build's base and currentorigin/main); after = this PR at3613960, proven by strings only it contains in the built main bundle. Creates were started through the same store action the create dialog calls; every toast ever shown was recorded.mainended up correctmaincorrectnotes.txtin the primary checkout, and the newmainaddsnotes.txtmainleft alonemainbehind, primary checkout has edits, two createsmainalready current, primary checkout has editsmainupdatedTwo creates at once. Before: a false warning, even though local
mainwas updated.After: both workspaces are created and no warning appears.
An untracked file where the new
mainadds that path. Before: no warning, and the user's file content was replaced by upstream's.After: the file is kept and the user is told why
mainwas not moved.Local
mainbehind with edits in its checkout, two creates. Before: two warnings for one fact.After: one warning per repo and branch.
Local
mainalready current, checkout has edits. Before: a false warning.After: nothing to do, no warning.
Re-run on the final code (M4Air, isolated dev app built from
822bad1, same fixture approach). Two creates at once fromorigin/main: no warning 3/3, localmain==origin/main. Untrackednotes.txtin the way: file kept, "uncommitted changes" warning,mainnot moved. New: a repo with anupstreamremote whosemainis ahead oforigin/main; creates fromupstream/main,upstream/mainandorigin/mainat once (3 runs), and fromorigin/mainandupstream/mainat once (3 runs): no create fromupstream/mainever warned, and localmainended atupstream/mainin all 6 runs. On the code before this fix, the same three-create burst gave one of theupstream/maincreates a false "cannot be fast-forwarded" result in 1 of 3 runs.Two creates at once from
origin/main: both created, no warning.Untracked file where the new
mainadds that path: file kept, and the user is told whymainwas not moved.Creates from
origin/mainandupstream/mainat once: both created, no warning, localmainatupstream/main.Creates from
upstream/main,upstream/main,origin/mainat once: no warning for theupstream/maincreates. Theorigin/maincreate warns because localmainis now ahead oforigin/main; that rule predates this PR and is listed under Known limits.Testing
src/shared/worktree/local-base-branch-fast-forward-real-git.test.ts(real git): an untracked file at a path the newmainadds is kept andmaindoes not move; a post-merge hook does not run (and does run for a plain merge, as a control); an edit made just before the move is kept; a commit made just before the move stays onmain; a free branch moves with the reflog message; an ignored file at a path the newmainadds is kept andmaindoes not move (fails without--no-overwrite-ignore; a plain fast-forward in the same test replaces it). With the oldreset --hardput back, the untracked-file, edit and commit tests fail; withcore.hooksPathremoved, the hook test fails; withmerge.verifySignatures=true,branch.main.mergeOptionsof--verify-signatures,-s oursor--squash, orpull.twohead=ours,mainstill lands exactly on the target with one parent and a clean index, and a plaingit merge --ff-onlyin the same test shows each setting is live (the-s ours,--squashandpull.twoheadcases fail without the pins).src/main/git/worktree-base-refresh-lock-real-git.test.ts(real git, heldindex.lock): retry after release; another process already movedmain→ updated; lock outlives the retries → warning; three concurrent creates → exactly one merge and no warning (fails without the one-update-per-branch rule); creates fromorigin/mainandupstream/mainin one burst each get the result for their own base, with never two merges at once (fails when the runner ignores the base, and when it runs different bases in parallel).src/main/git/worktree-base-refresh-contention.test.ts: late creates fold into one follow-up; different repos do not wait on each other; a stuck update makes a later create stop waiting at 30 s with no second update started (fails without the rule); the shared update ignores one create's cancel signal.src/shared/worktree/local-base-branch-fast-forward.test.ts: every status the routine can return, the exact git arguments, retry and failure classification, and validation of the relay's reply.src/shared/coalescing-keyed-runner.test.ts: one run; a burst toward one base shares one follow-up that uses the newest request; a different base queues its own run behind the one running, never beside it; a mixed burst costs one run per distinct base; failures do not block; keys are independent.SSH:
src/relay/git-handler-worktree-provisioning.test.tsruns the relay handlers against real git (owner fast-forward, free branch, missing branch, dirty checkout kept, diverged branch not moved, invalid requests rejected, concurrent requests give one merge, concurrent requests fromorigin/mainandupstream/maineach get their own base's result).src/main/ipc/worktrees-ssh-local-base-refresh*.test.ts: the app maps the relay's result, runs no git of its own for this, treats a timed-out or lost request as "unknown" (no warning) and other failures as a warning, and starts the checkout while the update is in flight.src/renderer/src/store/slices/worktrees-create-base-status.test.ts: one warning id per repo and branch.Wider run: all
worktree*git tests, relay git handler tests, local create flow, SSH setup, Windows worktree and runtime create suites pass (595 tests).Typecheck: no errors in changed files (node and web projects). Changed-lines quality gate: 0 new findings.
Platforms: unit and real-git tests on macOS. Git arguments are the same on every platform;
core.hooksPath=/dev/nullworks on Windows because no hook can exist under that path.I manually tested these changes locally
Automated tests added/updated, or explained why not below
AI Disclosure
Written with Claude.
Review
An independent architecture review challenged the earlier version of this PR; this redesign follows from it. Three review rounds plus a second architecture challenge of the move command (which found the merge-settings cases); results are in the status comment.
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
Ensure no issues in: Security, Cross-platoform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance
src/shared/git-binary-compatibility.test.ts, which CI runs against Git 2.25.5 (also run locally against 2.25.5: passes).merge --ff-only -s recursive --no-verify-signatures --no-overwrite-ignore,symbolic-ref -q,update-ref -m,--no-optional-locks(2.15) andcore.hooksPath(2.9) all predate it.maintenance.autoandmerge.autoStashare config keys older git ignores.localBaseRefRefreshon the create result keeps its shape and its four statuses.mainnow costs tworev-parsecalls and nothing else (the SSH relay also validates the two ref names first, as before). A behindmainadds onerev-list, oneworktree list, one status and the move, all overlapped with the checkout.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)