Repository navigation
fix(worktree): serialize worktree admin writes per repo and attribute git to its create - #22342
brennanb2025 wants to merge 3 commits into
Conversation
Every git command that rewrites a repository's worktree admin data (worktree add/remove/move/lock/unlock/repair/prune) now runs under a process-local queued lock keyed by the repo's git common dir, including the prepared-checkout pool's arm, re-arm and finalize. A user create outranks a pool re-arm; if the lock key cannot be derived the command runs unlocked. git.exec spans now carry the create or pool preparation span that issued them.
There was a problem hiding this comment.
ℹ️ Minor suggestion only — otherwise a clean, well-scoped change.
Reviewed changes
Read the complete 19-file diff at 5ca80b9 and traced both mechanisms through their callers; ran the new/affected lock suites plus the real-git, runtime-create, and observability tests (77 passing).
- Per-repo admin lane at the exec layer —
runWithGitExecLocksclassifies worktree admin argv and queues it on<common-dir>/worktrees;worktree liststays unlocked, and the relay'sgit()gets the same treatment. - Priority waiter queue —
runWithGitOperationLockmoves from a promise chain to an explicit queue (higher priority first, arrival order within a priority). Movingacquireoutside thetryalso fixes the old behavior where an aborted waiter released a lock it never held. - Shared common-dir walk — the
.git-marker/commondirwalk (norev-parse) is extracted togit-common-dir-location.tsand consumed by both locks, with a bounded cache only when a real.gitwas found. - Operation-scoped trace attribution —
withWorktreeSpan/withWorktreePrepareSpandeclare an operation,git.execgainsgit.operation/git.operation_span_id, and admin commands recordgit.worktree_admin_lock_wait_ms.
I confirmed the concurrency invariants of the new queue (no concurrent holders on a key, no stranded lane, no lost wakeup, ordering preserved under abort/reject) and the operation-attribution chain (no stale or cross-operation leakage, including the root: true prepare span). No critical issues.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
…its wait Cold creates run `worktree add --no-checkout` under the per-repo admin lane, then check out and run post-checkout (via `git hook run`) after release; Git without `git hook run` keeps the plain add. Inline tree-deleting removes skip the lane. Waits are bounded at 15 s, after which the command runs unlocked, and an aborted admin command holds the lane until its child exits in both main and the relay.
5ca80b9 to
5fe9128
Compare
There was a problem hiding this comment.
Important
The new hook-run probe does not work on Git 2.55+, so every cold create falls back to the plain worktree add under the admin lane — the long lock hold this PR is meant to remove — and the failed probe is never cached. This also leaves the new real-git test red on Git 2.55.0.
Reviewed changes
Reviewed the second commit (5fe9128, 30 files, +679/−85) against the prior review at 5ca80b9; read the full commit diff and ran the lock, split, and real-git suites (44 pass, 1 fail — detailed inline).
- Split the cold add around the admin lane —
addWorktreeWithCheckoutOutsideAdminLockrunsworktree add --no-checkoutinside the lane, thenreset --hardand thepost-checkouthook after release, with a plain-add fallback for hosts withoutgit hook run. - Bounded the lane wait and tied release to child exit —
maxWaitMs(15 s) makes a command log and run unlocked, and aholdUntillease keeps the lane until an aborted or timed-out child exits, in both main and the relay. - Opted inline tree deletes out of the lane —
worktreeAdminLock: falseon the main removal fallback/force retry,discardPreparedWorktree, and the relay equivalents. - Retired the prior review nit —
GitWorktreeAdminLockOptions.gitDiris removed. - Extended trace attribution —
worktree.prepareroot spans,worktree.create.await_preparation, andgit.operation/git.operation_span_id; runtime/CLI create now opens aworktree.createspan.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
| return /is not a git command|unknown (sub)?command|unknown option|usage: git hook/i.test(text) | ||
| } | ||
|
|
||
| async function supportsHookRun(request: SplitWorktreeAddRequest): Promise<boolean> { | ||
| const { capabilities, git, globalArgs, repoPath } = request | ||
| if (capabilities.isKnownSupported('hook-run')) { | ||
| return true | ||
| } | ||
| if (!capabilities.shouldTry('hook-run')) { | ||
| return false | ||
| } | ||
| try { | ||
| await git([...globalArgs, 'hook', 'run', '--ignore-missing', HOOK_RUN_PROBE_NAME], repoPath) | ||
| capabilities.rememberSupported('hook-run') | ||
| return true | ||
| } catch (error) { | ||
| if (isHookRunUnsupportedError(error)) { | ||
| capabilities.rememberUnsupported('hook-run') | ||
| } |
There was a problem hiding this comment.
git hook run --ignore-missing orca-capability-probe fails on Git 2.55+ with error: unknown hook event 'orca-capability-probe'; use --allow-unknown-hook-name to allow non-native hook names. So supportsHookRun returns false, every cold create falls back to the plain worktree add under the admin lane (the long hold this PR removes), and because that message matches none of the isHookRunUnsupportedError patterns the failure is not cached, so the probe is re-spawned on every create. The new real-git test checks out and runs post-checkout after the admin lane is released fails on Git 2.55.0 for exactly this reason.
Technical details
# `hook-run` probe fails on Git 2.55+
## Affected sites
- `src/shared/worktree/split-worktree-add.ts:41` — probe runs `git hook run --ignore-missing orca-capability-probe`; Git 2.55.0 exits non-zero with `unknown hook event`. (Git 2.55 added `--allow-unknown-hook-name` and now bails on non-native hook names.)
- `src/shared/worktree/split-worktree-add.ts:24-30` — `isHookRunUnsupportedError` tests for `is not a git command|unknown (sub)?command|unknown option|usage: git hook`; none match `unknown hook event`, so `rememberUnsupported('hook-run')` never runs and `shouldTry` stays true.
- `src/main/git/worktree-admin-lock-real-git.test.ts:181-183` — fails at `expect(_gitOperationLockHeldForTests(key)).toBe(false)` because the fallback plain add is blocked in the hook while holding the lane.
## Required outcome
- On Git 2.55+ the probe must classify `git hook run` as supported so the split path engages.
- Only a genuinely absent `git hook` subcommand (Git < 2.36) should mark the capability unsupported; a 2.55 probe failure must not be re-spawned per create.
## Suggested approach (optional)
- Probe with `--allow-unknown-hook-name`; if that flag is rejected as an unknown option (Git 2.36–2.54), treat hook-run as supported and use the plain form. Treat `unknown hook event` without the flag as supported too. Reserve `is not a git command`/`unknown command` for unsupported.
- Update the `hook-run` row in `docs/reference/git-compatibility.md`: the fallback applies before 2.36 and, until fixed, again on 2.55+.
## Open questions for the human (optional)
- Should the probe avoid a synthetic name entirely (e.g. `--ignore-missing post-checkout`), accepting that it would run a user's hook when one exists?…dline The cleanup `worktree remove --force` after a failed or cancelled checkout now runs without the create's AbortSignal and with its own timeout, so a cancel mid-checkout no longer leaves a registered, half-populated worktree. The split add's steps share one deadline instead of each getting the full add timeout.
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. 📝 WalkthroughWalkthroughThe change adds canonical Git metadata resolution and process-wide worktree-admin locking. Git command execution now applies lock policies, priorities, bounded waits, and termination-aware leases. Worktree creation can split admin-locked registration from checkout and hook execution when Priority: ⬇️ Low Merge Risk: 🟡 Moderate · up to On current Git releases (2.55 and later), the new split worktree creation never activates. Each create re-runs a failing capability probe and then holds the repository's worktree lock through the full checkout and hook, so concurrent creates and pool re-arms still serialize behind long checkouts. A new real-Git test fails for this reason. The PR's main goal is not delivered on up-to-date Git until the probe classification is fixed. 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 34.92% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 63 functions across 40 files. (1 skipped: 1 unsupported.) Full details: Description checkExplanation The description explains the changes, rationale, testing, limitations, platform impact, and AI usage in detail. However, the required Linked Issue section states "No linked issue" even though the template requires an issue link.
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
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.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: b0b197c4-592d-4422-9fd6-2764038cbb3b
📒 Files selected for processing (41)
docs/reference/git-compatibility.mdsrc/main/git/command-runner/git-exec-admission-lifetime.test.tssrc/main/git/command-runner/git-exec-file.tssrc/main/git/command-runner/git-exec-lock-policy.tssrc/main/git/command-runner/git-exec-options.tssrc/main/git/remove-worktree.test.tssrc/main/git/worktree-add-creation-config.test.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-admin-contention.bench.test.tssrc/main/git/worktree-admin-lock-real-git.test.tssrc/main/git/worktree-create-git-executor-real-git.test.tssrc/main/git/worktree-create-preparation-real-git.test.tssrc/main/git/worktree-create-preparation.tssrc/main/git/worktree-removal.tssrc/main/git/worktree-test-harness.tssrc/main/observability/instrumentation.tssrc/main/observability/tracer.tssrc/main/runtime/runtime-local-worktree-create.test.tssrc/main/runtime/runtime-local-worktree-create.tssrc/main/worktree-create-preparation-pool.tssrc/main/worktree-create-preparation.tssrc/relay/git-handler-operation-context.tssrc/relay/git-handler-ops.tssrc/relay/git-handler-termination.test.tssrc/relay/git-handler-worktree-operations.tssrc/relay/git-handler-worktree-ops.test.tssrc/relay/git-handler-worktree-ops.tssrc/relay/git-handler-worktree-provisioning.test.tssrc/relay/git-handler-worktree-remove.tssrc/relay/git-handler.tssrc/shared/git-capability-cache.tssrc/shared/git-common-dir-location.tssrc/shared/git-fetch-head-lock.tssrc/shared/git-operation-lock.test.tssrc/shared/git-operation-lock.tssrc/shared/git-worktree-admin-lock.test.tssrc/shared/git-worktree-admin-lock.tssrc/shared/worktree/split-worktree-add.test.tssrc/shared/worktree/split-worktree-add.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 4 remain after this review.
| function isHookRunUnsupportedError(error: unknown): boolean { | ||
| const text = [ | ||
| error instanceof Error ? error.message : String(error), | ||
| typeof error === 'object' && error !== null && 'stderr' in error ? String(error.stderr) : '' | ||
| ].join('\n') | ||
| return /is not a git command|unknown (sub)?command|unknown option|usage: git hook/i.test(text) | ||
| } | ||
|
|
||
| async function supportsHookRun(request: SplitWorktreeAddRequest, git: StepGit): Promise<boolean> { | ||
| const { capabilities, globalArgs, repoPath } = request | ||
| if (capabilities.isKnownSupported('hook-run')) { | ||
| return true | ||
| } | ||
| if (!capabilities.shouldTry('hook-run')) { | ||
| return false | ||
| } | ||
| try { | ||
| await git([...globalArgs, 'hook', 'run', '--ignore-missing', HOOK_RUN_PROBE_NAME], repoPath) | ||
| capabilities.rememberSupported('hook-run') | ||
| return true | ||
| } catch (error) { | ||
| if (isHookRunUnsupportedError(error)) { | ||
| capabilities.rememberUnsupported('hook-run') | ||
| } | ||
| return false | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
The hook-run probe still fails on Git 2.55+. The CI failure confirms it.
The probe still runs git hook run --ignore-missing orca-capability-probe. Git 2.55 rejects hook names it does not know unless the command passes --allow-unknown-hook-name. That error text matches none of the patterns in isHookRunUnsupportedError. As a result, supportsHookRun returns false without caching the result:
- Every cold create re-runs the probe.
- Every cold create falls back to the plain
worktree add, which holds the admin lane through checkout andpost-checkout.
The failing check src/main/git/worktree-admin-lock-real-git.test.ts:183 (expected true to be false) shows this outcome.
Treat unknown hook event as a supported result, or probe with --allow-unknown-hook-name first. Keep the unsupported classification for a missing git hook subcommand only. After the fix, update the hook-run row in docs/reference/git-compatibility.md.
Possible fix
function isHookRunUnsupportedError(error: unknown): boolean {
const text = [
error instanceof Error ? error.message : String(error),
typeof error === 'object' && error !== null && 'stderr' in error ? String(error.stderr) : ''
].join('\n')
- return /is not a git command|unknown (sub)?command|unknown option|usage: git hook/i.test(text)
+ return /is not a git command|unknown (sub)?command/i.test(text)
+}
+
+// Why: Git 2.55+ rejects non-native hook names, which proves `git hook run` exists.
+function isHookRunUnknownNameError(error: unknown): boolean {
+ const stderr =
+ typeof error === 'object' && error !== null && 'stderr' in error ? String(error.stderr) : ''
+ const message = error instanceof Error ? error.message : String(error)
+ return /unknown hook event/i.test(`${message}\n${stderr}`)
}
@@
} catch (error) {
+ if (isHookRunUnknownNameError(error)) {
+ capabilities.rememberSupported('hook-run')
+ return true
+ }
if (isHookRunUnsupportedError(error)) {
capabilities.rememberUnsupported('hook-run')
}
return false
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function isHookRunUnsupportedError(error: unknown): boolean { | |
| const text = [ | |
| error instanceof Error ? error.message : String(error), | |
| typeof error === 'object' && error !== null && 'stderr' in error ? String(error.stderr) : '' | |
| ].join('\n') | |
| return /is not a git command|unknown (sub)?command|unknown option|usage: git hook/i.test(text) | |
| } | |
| async function supportsHookRun(request: SplitWorktreeAddRequest, git: StepGit): Promise<boolean> { | |
| const { capabilities, globalArgs, repoPath } = request | |
| if (capabilities.isKnownSupported('hook-run')) { | |
| return true | |
| } | |
| if (!capabilities.shouldTry('hook-run')) { | |
| return false | |
| } | |
| try { | |
| await git([...globalArgs, 'hook', 'run', '--ignore-missing', HOOK_RUN_PROBE_NAME], repoPath) | |
| capabilities.rememberSupported('hook-run') | |
| return true | |
| } catch (error) { | |
| if (isHookRunUnsupportedError(error)) { | |
| capabilities.rememberUnsupported('hook-run') | |
| } | |
| return false | |
| } | |
| } | |
| function isHookRunUnsupportedError(error: unknown): boolean { | |
| const text = [ | |
| error instanceof Error ? error.message : String(error), | |
| typeof error === 'object' && error !== null && 'stderr' in error ? String(error.stderr) : '' | |
| ].join('\n') | |
| return /is not a git command|unknown (sub)?command/i.test(text) | |
| } | |
| // Why: Git 2.55+ rejects non-native hook names, which proves `git hook run` exists. | |
| function isHookRunUnknownNameError(error: unknown): boolean { | |
| const stderr = | |
| typeof error === 'object' && error !== null && 'stderr' in error ? String(error.stderr) : '' | |
| const message = error instanceof Error ? error.message : String(error) | |
| return /unknown hook event/i.test(`${message}\n${stderr}`) | |
| } | |
| async function supportsHookRun(request: SplitWorktreeAddRequest, git: StepGit): Promise<boolean> { | |
| const { capabilities, globalArgs, repoPath } = request | |
| if (capabilities.isKnownSupported('hook-run')) { | |
| return true | |
| } | |
| if (!capabilities.shouldTry('hook-run')) { | |
| return false | |
| } | |
| try { | |
| await git([...globalArgs, 'hook', 'run', '--ignore-missing', HOOK_RUN_PROBE_NAME], repoPath) | |
| capabilities.rememberSupported('hook-run') | |
| return true | |
| } catch (error) { | |
| if (isHookRunUnknownNameError(error)) { | |
| capabilities.rememberSupported('hook-run') | |
| return true | |
| } | |
| if (isHookRunUnsupportedError(error)) { | |
| capabilities.rememberUnsupported('hook-run') | |
| } | |
| return false | |
| } | |
| } |
Source: Linters/SAST tools
There was a problem hiding this comment.
ℹ️ No new issues in the new commits — the delta is a clean follow-up. The previously flagged
hook-runprobe failure is still open (existing inline thread), so this is not an approval.
Reviewed changes
Re-reviewed the third commit (29d238d, 4 files, +137/−12) against the prior review at 5fe9128; read the incremental range-diff in full and traced the new code through its main-process and relay callers. Ran src/shared/worktree/split-worktree-add.test.ts (8 pass) and src/main/git/worktree-admin-lock-real-git.test.ts (3 pass, 1 fail — the known hook-run probe on Git 2.55).
- One deadline for the whole split add —
withSharedDeadlinehands each step only the remaining budget and rejects withETIMEDOUTonce it is spent, so the split add is bounded like the plain command instead of three full timeouts. - Detached, self-budgeted cleanup — a failed checkout now runs
worktree remove --forcethrough the unwrapped adapter withdetached: true(create signal stripped),worktreeAdminLock: false, and its own 60 s timeout, so a cancelled or timed-out create is unregistered. - Cancellation coverage — a new real-git test cancels a create mid-smudge-filter and asserts the worktree is not left registered.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
| /** Replaces the caller's own per-command timeout. */ | ||
| timeout?: number | ||
| /** Runs without the create's AbortSignal, so cleanup still happens after a cancel. */ | ||
| detached?: true |
There was a problem hiding this comment.
detached is honored only by the main-process adapter (src/main/git/worktree-add.ts); the relay's GitExec type omits it and the relay create path never threads an AbortSignal, so this cleanup-after-cancel guarantee is main-only today. Inert rather than broken right now, but worth a comment here if a request signal is ever threaded into relay creates.
|
Closing. It is ~2,400 commits behind and conflicts in 19 files, and its repo-wide lock does not fix the main slow-create cause (a create waiting on an unfinished spare checkout). That cause is addressed in #29115; main has since added narrower concurrency controls and create timing data. |

ELI5
Git keeps a small bookkeeping folder per repository listing every worktree. Every
git worktree add / remove / move / lock / unlock / repair / prunerewrites that folder and walks every entry in it. In a repo with ~800 worktrees, two of those commands running at once on the same repo slow each other down. Orca now lets only one of them run per repository at a time, lets a user's create go before background warm-up, and tags every git command in the trace with the create (or warm-up) that issued it, so a slow create can be explained from the logs.What Changed
Before. Nothing stopped two admin commands from running at once on one repo. So a user's create could overlap the prepared-checkout pool warming up, a removal, or a prune. In the trace, a pool warm-up's git showed up as parentless root spans, or parented to whichever create happened to fire the re-arm. Runtime/CLI creates opened no
worktree.createspan at all. So the 32 sgit_worktree_addphase could not be tied to specific commands.After.
gitExecFileAsync(main) and the relay'sgit()already recognize argv that needs a per-repo lock (fetch/pull→ the FETCH_HEAD lane). That recognition now lives inrunWithGitExecLocks(src/main/git/command-runner/git-exec-lock-policy.ts) and also recognizes worktree admin mutations. Those queue on a lane keyed by<common-dir>/worktrees.worktree listis a read and stays unlocked. Every existing and future caller is covered without touching call sites: create, cold add, sparse add, pool arm/finalize/discard, stale-preparation cleanup, removal, prune, move, and the relay..git-marker/commondirwalk the FETCH_HEAD lock already used moved tosrc/shared/git-common-dir-location.ts, so both locks share it. The admin lock caches the result per(cwd, --git-dir), holding at most 256 entries, and only when a real.gitwas found. It never runsrev-parse.runWithGitOperationLockwent from a promise chain to an explicit waiter queue. Higher priority is granted first, arrival order holds within a priority, and an aborted waiter leaves the queue immediately. The admin lane takes its priority from the existing git admission tier: creates runinteractive(2), pool work runsstatus(1), and stale-preparation reclaim runsbackground(0). A queued create therefore goes before a queued re-arm. The existing FETCH_HEAD and per-checkout locks keep plain FIFO.git worktree addwrites the admin entry and checks out every file and runs thepost-checkouthook, all in one process. Holding the lane for that whole process would serialize every checkout in the repo (the reported trace has a 134.9 s cold add). Cold creates on the runtime/CLI path, the IPC path, and the relay now runworktree add --no-checkoutunder the lock, then runreset --hard --no-recurse-submodulesin the new worktree and thepost-checkouthook after the lane is released. The shared helper issrc/shared/worktree/split-worktree-add.ts. The prepared pool already arms this way.reset --harddoes not run hooks. So after it, Orca runsgit hook run --ignore-missing post-checkout -- <null oid> <new HEAD> 1. Those are the same three arguments git passes, and the hook runs from the new worktree as before. The one difference is thatGIT_DIRis set in the hook's environment, which is also true when a user runs a normalgit checkout. A failing hook still fails the create and keeps the worktree, like the plain command. A failed or cancelled checkout removes the worktree and keeps the branch, also like the plain command. That cleanupworktree remove --forceruns without the create's cancel signal, with its own 60 s timeout, so cancelling a create mid-checkout does not leave a registered, half-filled worktree behind.git hook runneeds Git 2.36+. It is probed once per execution host throughGitCapabilityCache(new capabilityhook-run). On older Git the plain fullworktree addruns under the lock instead of skipping the user's hook.rev-parseand hook share the singleworktree addtimeout (default 180 s, orORCA_WORKTREE_ADD_TIMEOUT_MS). Each step gets only what is left, so the split add is bounded the same as the plain command, not three times over.worktree remove(removal fallback, submodule--forceretry, discard of a prepared checkout, and the relay's equivalents) deletes the whole working tree. Those calls passworktreeAdminLock: falseand skip the lane. Git's own lock files still guard the admin entry.worktree admin lock still busy … running unlockedand runs without the lane, the same as before this PR. It does not release the current holder. The span attributegit.worktree_admin_lock_heldrecords which case applied. A create can no longer wait behind N queued commands times each one's timeout.git.execspan getsgit.operationandgit.operation_span_id, plusgit.worktree_admin_lock_wait_mson admin mutations.worktree.createis an operation, for both the IPC create and now the runtime/CLI create. Pool work runs in its own rootworktree.preparespan withreason=prefetch | rearm | discardandtriggered_by_span_id, so it can no longer inherit the create that fired it. When a create claims a checkout that is still being built, the wait gets its ownworktree.create.await_preparationspan naming the preparation's span id.Why
What actually happened in the reported 37 s create. I read the create path on a prepared-checkout hit end to end and checked it against the raw trace (
main.trace.ndjson, trace4a972071…: 37 365 ms total,git_worktree_add32 181 ms, hit, not retargeted).Git commands that run inside
git_worktree_addon a hit, in order:rev-parseof the base (when selection needs the canonical base); on a retarget, the divergence probes (rev-parse,rev-list,merge-base).ready. If the entry is still arming, this covers that preparation'sworktree list --porcelain(stale scan),worktree add --detach --no-checkout,reset --hard, andworktree lock. All of it runs in the preparation's context, not the create's.rev-parses, and when base refresh/suggestion is on:worktree list --porcelain+statusper sibling checkout)rev-parse --verify HEADin the prepared checkoutreset --hard <target>if HEAD driftedworktree move -f -f,checkout --no-track -bconfig(creation base,push.autoSetupRemote)worktree unlockIn that trace:
worktree add --detach --no-checkoutstarted at +1.2 s (1.3 s). Itsreset --hardran 21 805 ms in a separate root trace. Itsworktree lockran at +24.3 s. The create resumed at +25.3 s.worktree list906 ms and 1289 ms, each followed bystatus; a driftreset --hard458 ms;worktree move2926 ms;checkout -b2073 ms;worktree unlock1312 ms.worktree addabout 169 s earlier has 7 arguments. That is the cold--no-track -b <branch> <path> <base>shape; a pool arm has 6. So it was most likely a runtime/CLI cold create, which had no create span, rather than a pool re-arm.So this PR does not remove the dominant 23.6 s, and does not claim to make creates faster. That time is a create claiming a "hit" whose checkout is still materializing. This PR keeps admin writes on one repo from overlapping each other, keeps checkouts and hooks out of that critical section, and makes the 23.6 s visible and named in the trace. A still-arming preparation will still hold up the create that claims it. Fixing that changes create behavior and belongs in its own PR; see Remaining gaps.
Alternatives considered.
git rev-parse --git-common-dirper command. Rejected: that adds a subprocess per admin command, and the file walk already exists and handles WSL/UNC spellings.worktree add. Rejected: it serializes every checkout and hook in the repo behind one lane.post-checkout. Rejected: it would silently drop users' hooks (for example dependency installs). On Git withoutgit hook run, Orca keeps the plain add instead.Linked Issue
No linked issue.
Visual Proof
N/A: no UI change. Evidence is the trace numbers above and the benchmark below.
Testing
New tests. Each one was confirmed red with its mechanism removed.
src/shared/git-operation-lock.test.tssrc/shared/git-worktree-admin-lock.test.tssrc/main/git/worktree-admin-lock-real-git.test.tsworktree moveran first, both succeed, git spans carry the right operation idsworktree.prepareran first); prepare span not a root operation → fails (re-arm git not attributed)src/main/runtime/runtime-local-worktree-create.test.ts(new case)worktree.createoperationsrc/main/git/worktree-admin-lock-real-git.test.ts(new cases)addWorktreewhosepost-checkouthook blocks. While the hook runs the admin lane is free and aworktree prunecompletes; the hook got null oid, new HEAD,1, and the files are checked out. Discarding a prepared checkout completes while the lane is held.src/main/git/worktree-admin-lock-real-git.test.ts(cancel case)src/shared/worktree/split-worktree-add.test.tsgit hook runkeeps the plain add and is remembered; failed checkout removes the worktree detached and without the lane; failed hook keeps it; WSL routing invalidation runs even when the add fails; steps share one deadline; a spent deadline fails withETIMEDOUTand cleans upsrc/shared/git-operation-lock.test.ts,src/shared/git-worktree-admin-lock.test.ts(new cases)holdUntilkeeps the lane until settled; admin lock warns and runs unlocked past its boundsrc/main/git/command-runner/git-exec-admission-lifetime.test.ts,src/relay/git-handler-termination.test.ts(new cases)holdUntilline removed → fails; relay not routed to the termination runner → failssrc/main/git/remove-worktree.test.ts,src/relay/git-handler-worktree-ops.test.tsCommands run.
vitest run src/main/git src/relay src/shared/worktree src/shared/git- src/main/runtime/runtime-local-git-worktree: 494 files, 4974 passed, 33 skipped (the skips include the opt-in benchmark and Windows-only cases). Suites that queue git replies in order pin the plain-add path; the split add has its own tests.pnpm tc: passes (all projects), after rebasing onto current mainpnpm run check:code-quality:changed: passes, 0 findings across 40 changed filesoxlinton the touched files: cleanBenchmark (opt-in
src/main/git/worktree-admin-contention.bench.test.ts). Synthetic repo in/tmpwith 600 linked worktrees and 4000 files. Each trial arms a prepared checkout, then runs a pool re-arm and a create that consumes it at the same moment. Per-command timings are from the create's own trace, in ms; "wait" is the admin-lock wait. Before =origin/main, after = this branch, same machine.worktree moveworktree unlockWhat this shows, plainly:
worktree movenow gets the lane first, with zero wait, and runs a little faster.worktree unlockwaits 250–420 ms behind the re-arm'sworktree add. That add was granted while the create was runningcheckout/config, and a running command is never preempted.The benchmark predates the split add and the wait bound; it measures prepared-hit creates, which do not use the cold add path. Platforms: macOS only. Windows, Linux, WSL, and SSH are covered by unit and relay-path tests only.
AI Disclosure
Implemented with Claude (Claude Code).
Review
Remaining gaps / adjacent findings (not fixed here).
claimPreparedWorktreetakes an entry and awaits itsreadyeven when the checkout has just started. It could treat an unfinished entry as a miss and do a cold add, or race the two. That changes create behavior, so it belongs in its own PR. The newworktree.create.await_preparationspan measures it.readystill has that preparation's admin commands queued atstatuspriority. They are not promoted.worktree removedoes not also take the per-checkout lock that pull/rebase use.Agent skill upstream boundary
Notes
git()applies the same lock on the execution host, arrival order only because the relay has no admission tier.CreateWorktreeResult.timingis unchanged. New span attributes stay in local traces only.performance.now()per admin command. The split cold add runs three git commands after the admin write instead of none:reset --hard(the checkout the plain command did internally),rev-parse HEAD, andhook run post-checkout. It also runs a one-timegit hook runprobe per execution host.docs/reference/git-compatibility.mdcapability table gains thehook-runrow.Checklist
N/Afor visuals, with reasonpnpm lint,pnpm typecheck,pnpm test,pnpm build: typecheck and targeted tests and lint run locally; full suite and build left to CI