Skip to content

perf(worktree): keep one worktree list per repo instead of re-running git worktree list - #24085

Closed
brennanb2025 wants to merge 23 commits into
brennanb2025/create-scan-idle-gatefrom
brennanb2025/worktree-membership-events
Closed

brennanb2025 wants to merge 23 commits into
brennanb2025/create-scan-idle-gatefrom
brennanb2025/worktree-membership-events

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 67 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1947 $\color{#cf222e}{\Huge{\mathbf{−}}}$​885 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1062
Prod 47 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2142 $\color{#cf222e}{\Huge{\mathbf{−}}}$​852 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1290

ELI5

To know which worktrees a repo has, Orca kept asking Git to list every one of them, thousands of times a day, even when nothing had changed. On a repo with ~800 worktrees each answer took 0.2–0.5 s of disk work, and it fought worktree creates for the disk. Now Orca keeps one list per repo. It asks Git once to build it, then reads the same small files Git reads. When something asks for the list, Orca checks whether those files changed and re-reads only the ones that did. A repo whose folder was deleted costs one file check and never runs Git.

What Changed

The problem the user saw. On 2026-09-28 creating a worktree took 80–268 s instead of 5–15 s. Part of the cause was Orca competing with itself for the disk: git worktree list ran over and over, and each run walked the bookkeeping files of every worktree. On the main repo (792 worktrees) the traces show 17,391 listings in 6 days, 3,730 of them on 09-28 costing 2,604 seconds of Git time, in bursts about every 24 s. Three repos whose folders had been deleted were listed, and failed, about every 37 s, with no end. Listings also crowded Orca's background Git queue: on 09-25, 107 of 173 queue waits longer than 5 s were listings.

Why it happened. Every worktree change event (a commit, checkout or branch delete in any worktree) and every cache expiry made several independent readers each re-run the full git worktree list: the sidebar's worktree scan, the scan behind orca worktree list / ps and mobile, the rebuild of the folders Orca may access, terminal restore, and a handful of per-action checks. Each reader kept its own short cache, so one change fanned out into several full listings.

What is different for the user.

  • Creating a worktree no longer competes with repeated 800-worktree listings. Once a repo's list is built, these readers run no git worktree list for it, apart from the repo layouts listed below that stay on Git.
  • A repo whose folder was deleted costs one file check per read and never runs Git. A registered folder that still exists but is no longer a Git repo costs two file checks per read after Git first says so, and Git runs again as soon as its .git reappears (or after five minutes).
  • The sidebar, orca worktree list / ps, mobile and the PR panels show the same rows as before, and they are at least as fresh. A worktree added, removed, locked or committed to from a terminal shows on the next refresh. That holds when the window is closed or hidden and in headless mode too, where no file watcher runs.
  • On Git older than 2.31, locked and prunable now follow Git's own rule (see "Deliberate choices and known limits").

How it works now.

  • One model per local repo. The new src/main/git/worktree-membership/ keeps one worktree model per registered local repo, owned by the main process. Its rows use the existing GitWorktreeInfo shape: path, head, branch, bare, main, locked and its reason, prunable, sparse.
  • Built with one Git listing. Building a model runs git worktree list once. Git supplies the main worktree's exact path spelling and whether the repo is bare; worktree ids are repoId::path, so the spelling must match Git's byte for byte. Every linked worktree's row is then read from Git's own bookkeeping files under .git/worktrees/<name>/: gitdir, HEAD, the branch file HEAD points through, packed-refs, locked, the checkout's .git, and the sparse-checkout config. The rows read from files must equal Git's rows exactly (the parity check). If they don't, that repo stays on Git until the next launch and a warning is logged.
  • A stat check on every read. For each row, the model records the files it was read from, with each file's size, modification time, change time and inode (a "stat"). A later read stats those files and re-reads only the entries whose stats moved. Git replaces HEAD, refs and gitdir by writing a new file and renaming it over the old one, so the inode changes even on disks with coarse timestamps. Every five minutes a read re-reads every entry anyway, in case a coarse clock hid a rewrite. An idle repo costs nothing: there are no timers.
  • When a result is reused. Each model has a change counter. A read reuses an earlier result, finished or still running, only if that work started at the current counter value and less than 200 ms ago. The file watcher waits 250 ms after the last change before it tells readers to refresh. So a refresh triggered by a change always starts too late to reuse work that began before the change, and it re-checks the disk.
  • Orca's own changes bump the counter. Worktree creates, removes and moves, the git worktree prune runs Orca makes, and Orca's existing worktree-change invalidations all bump the counter of the repo's model. Other registered repos that share the same Git directory are bumped too. The next read reuses nothing from before the change and re-lists the .git/worktrees/ folder.
  • One re-read at a time per repo. Each model runs at most one re-read at a time. A reader that cannot reuse the running one waits for a single shared follow-up, which starts when the running one finishes. So a slow or hung disk never stacks up file work. Each reader waits under its own deadline and cancel signal and detaches when it gives up. The shared work keeps Git's own timeout, and one reader's cancel never cancels it. The runtime scan still waits at most 5 s per repo.
  • Layouts that stay on Git. These repos keep a Git listing, re-run only when one of the files it depends on changes: reftable ref storage, gitdir records written as relative paths (Git 2.48+), core.worktree, a repo config that uses include or includeIf, a symlinked HEAD, and any repo that failed the parity check. When Orca cannot find the repo's Git directory from files at all, Git answers every read, as before.
  • What behaves as before. WSL repos never get a model: a stat over the WSL file share can report a folder that exists as missing. They keep today's Git listing and caches. SSH repos keep the remote host's listing. Folder workspaces never reach the model. Removing a worktree and deleting a branch still ask Git directly, once per action.
  • Missing repos. When a repo's folder is gone, a read costs one stat and no Git, and it counts as a real empty answer. Lenient readers get [], strict readers reject, and the sidebar's scan accepts the repo as empty. When only the repo's Git directory is gone, the model is dropped and Git answers that read.
  • Upper caches stop storing rows for repos the model reads from files: the sidebar scan's 5 s rows and the runtime's 30 s per-repo rows. They keep their change-counter checks. Repos that stay on Git keep those row caches. The runtime's old Git-folder fingerprint (repo-worktree-admin-fingerprint.ts) and its reference doc are deleted. The stat check replaces the fingerprint for every layout.
  • No new cap on the fleet scan. The runtime scan behind the CLI and mobile still reads every repo at once, each with its own 5 s budget, as on the base branch.
  • Sparse flag from the model. Model rows carry isSparse, so listings skip the per-row sparse probe. That probe's 512-entry cache evicted on every pass at ~800 worktrees.
  • Lifetime. A model is dropped when its repo is removed from Orca, when nobody has read it for 30 minutes, or when its Git directory disappears. The next read rebuilds it with one Git listing.
  • One reader of Git's files. worktree-admin-file-reads.ts holds the rules for reading these files: absent versus unreadable, safe ref names, packed-refs, and following symbolic refs. The model and the existing head-identity reader both use it, so they cannot disagree about a HEAD. Moving the code here also fixed the head-identity reader for a chain of symbolic refs (HEAD → refs/heads/alias → refs/heads/main). It now reports refs/heads/main, as Git does, instead of alias.

Why

  • Why read Git's files instead of caching Git's answer behind a stat check. A shared cache of full git worktree list output would be cleared whenever any file it depends on changes. Branch heads change on every commit, checkout and fetch in any worktree. So on a busy repo it would still run hundreds of full 800-worktree listings a day. Reading the files lets a commit in one worktree cost a re-read of that one entry.
  • Why not run Git only when a worktree is added or removed. Every create adds an entry, so that design runs an 800-worktree listing in the middle of each create. That is when the disk is busiest and the user is waiting.
  • Why check stats on read instead of trusting the file watcher. In several states the watcher silently delivers nothing: the window closed on macOS, no worktrees/ folder yet while hidden, the polling fallback while hidden, the worktrees/ folder deleted, and the gap while a watcher restarts. Headless mode has no watcher at all. A stat check on read needs no watcher to be alive. The watcher still triggers refreshes, and the 200 ms reuse window makes sure each of those refreshes re-checks the disk.
  • Why keep one Git listing per model. Only Git gives the main row's exact spelling for separate Git directories, submodules and bare repos. The parity check turns any bug in the file rules into "this repo uses Git" instead of a wrong row.
  • Why one re-read at a time instead of a cap on the fleet scan. A cap across repos lets a few stalled repos (a half-open SSH host, a hung mount) hold every slot and starve healthy ones. One re-read per repo bounds the disk work per repo without letting one repo's stall reach another.
  • Measured on the main repo (812 worktrees, macOS, Git 2.50.1, idle machine): the model's rows matched Git's byte for byte. Building the model took 219 ms, including its one Git listing. Later re-checks took 16–20 ms with no Git. git worktree list plus the sparse probe took 133 ms per call.

Related open PRs. #16352 ("Make local Git metadata observation event-driven") landed as #16404 (merged 2026-08-25). This PR builds on #16404's watcher and does not change it: the watcher still triggers refreshes. #16175 ("stagger and bound metadata polling") is unrelated: this PR adds no polling. This PR neither depends on nor conflicts with either.

Differences from the common pattern

Matches the common pattern in these ways. Each repo's worktree list is held in memory by one long-lived process, one per registered repo. Orca's own creates, removes, moves and prunes bump a per-repo change counter, so no later read reuses older work. Each repo runs one re-read at a time, plus one shared re-read queued behind it. A repo whose folder is gone costs one file check and no Git, and so does a registered folder Git says is no longer a repository, until its .git reappears. Linked worktrees are read from the same two Git bookkeeping files (.git/worktrees/<name>/gitdir and HEAD) that the common pattern reads instead of running Git. SSH repos are still listed by Git on the remote host, with nothing cached.

  • Intended: freshness comes from checking file stats on each read (size, times, inode), not from trusting a file watcher, a timed cache, or re-reading everything on every refresh. Safe because the watcher silently delivers nothing in several states (window closed on macOS, polling while hidden, headless mode), and the stat check needs nothing to be running; it costs 16–20 ms at ~800 worktrees, and an idle repo costs nothing.
  • Intended: the five-minute full re-read happens on the next read instead of on a timer. Safe because every read already re-checks stats; the floor only covers disks whose timestamps are too coarse to show a rewrite.
  • Intended: building a repo's list runs git worktree list once and requires the rows read from files to match it exactly, or that repo stays on Git until the list is rebuilt. The common pattern does not check rows read from files against Git. Safe because any gap in the file rules becomes "this repo uses Git", never a wrong row.
  • Intended: the main worktree's path and bareness come from that one Git listing, not from the parent of the Git folder. Safe because only Git spells the main path correctly for separate Git folders, submodules and bare repos, and worktree ids depend on that exact spelling.
  • Intended: repos whose layout the file rules can't read (reftable, relative gitdir records, core.worktree, config includes, a symlinked HEAD) stay on Git, which re-runs only when a file the listing depends on changes by stat, not on every watcher event or every request. Safe because those stats cover the same files Git reads for the listing, and the five-minute floor still re-runs Git.
  • Intended: there is one list per registered repo path rather than one per Git folder, so two registered repos that share a Git folder each keep a list, and a change in one marks both. Safe because how Orca spells the main path depends on the path the repo was registered with; the cost is duplicate stats in that rare setup.
  • Intended: a finished or running re-read is reused for only 200 ms, not seconds or minutes. Safe because the watcher waits 250 ms after a change before it tells readers to refresh, so a refresh caused by a change always re-checks the disk.
  • Intended: each reader waits on the shared re-read under its own deadline and cancel signal, and stops waiting when either runs out. The common pattern only times out the shared Git run. Safe because one reader's cancel never cancels the others, and a hung disk read blocks only that repo's re-read, not other repos.
  • Intended: the scan behind orca worktree list / ps and mobile reads every repo at once, each with its own 5 s budget, as before this PR. Safe because each repo already runs at most one re-read at a time, and running them one after another would let one stalled repo (a half-open SSH host, a hung mount) delay every healthy one.
  • Intended: a repo's list is dropped after 30 minutes with no reads, or when the repo is removed from Orca, with no cap on how many are kept. Safe because their number is bounded by the registered repos, and a dropped list is rebuilt with one Git listing.
  • Intended: WSL repos keep today's Git listing and its 5 s / 30 s caches, with no in-memory list. Safe because a stat over the Windows-to-WSL file share can report an existing folder as missing, which would drop and rebuild the list over and over; WSL behaves exactly as before.
  • Temporary: the head-identity refresh (which updates each worktree's branch and commit) still keeps its own per-worktree cache and re-reads everything every 60 s, beside the new list. The common pattern updates the worktree list and heads together in one pass. Both readers use one shared set of file rules, and a test asserts they agree. A follow-up PR (not yet opened) moves that refresh onto the list.
  • Temporary: SSH repos keep the remote host's listing with nothing cached, so local repos get the new list and remote repos don't. A follow-up PR (not yet opened) runs the same stat-checked list on the remote host, behind the existing git.listWorktrees call, with no change to what client and host send each other.

Where this fits

On 2026-09-28 worktree creates took 80–268 s because Orca competed with itself for the disk. Three things caused it. A spare checkout was built and waited on with no time limit. Background Git (fetches, status sweeps, PR conflict checks) ran during the create. And git worktree list was spawned over and over, while three deleted repos were polled forever. This plan fixes those in separate PRs:

Step PR Base What it does
1 #23699 main A create does one fetch and one checkout (the spare checkout is deleted); background disk work waits for a local create, 2 min cap
2 #24085 (this PR) #23699 One worktree model per local repo, read from Git's own files and checked by stat; no repeated git worktree list; deleted repos stop being polled
2b later PR #23699 The create card shows Git's checkout progress
3 #22356 main PR conflict checks stop writing throwaway objects into the user's repo
separate #23698 main Local main is refreshed safely, once per repo and branch, alongside the checkout

This PR is step 2.

Not in this PR

  • Follow-up: publish only real row changes. worktrees:changed would fire only when a row's membership, path, branch, lock, prunable or sparse state changed, and head-only moves would go out as worktrees:headIdentitiesChanged. Any change event still re-publishes the whole worktree list to the renderer, mobile and the metadata cleanup pass. After this PR that is an in-memory read with no Git and no disk work, but it still costs a message and a store comparison sized to the whole list, per event. It gets its own PR because it changes the notification contract that fix(worktree): a create does one fetch and one checkout, and background work waits for it #23699's create gate wraps and that the renderer and mobile consume.
  • Follow-up: the head-identity refresh reads the model (TEMPORARY difference). The head-identity refresh still keeps its own per-entry memo and a 60 s full re-read. Until that follow-up lands, both readers share one set of file rules. A test feeds both the same fixtures, including a symbolic-ref chain and a branch that exists only in packed-refs, and asserts the same heads.
  • The same model for SSH repos, on the remote host. SSH repos keep today's uncached listing. The remote host's worktree watch can't be trusted yet: provider.watch has no handler for a fatal error, and nothing re-subscribes after a reconnect (an existing bug). The follow-up runs the same stat-checked model on the remote host, behind git.listWorktrees. It needs no wire change, and older remote hosts keep running Git.
  • A cheaper two-level stat check. The idea: stat the worktrees/ folder and each entry folder first, and the deeper files only when those moved. At 811 entries that measured 877 stats in 3.3 ms, against 3,949 stats in 17 ms today. It is deferred until it is checked on a loaded disk and on filesystems other than APFS, because it relies on folder modification times.
  • The listing fan-out when an SSH connection opens (ssh-relay-session.ts), and the missing watch for WSL repos opened through a \\wsl$ path.

Deliberate choices and known limits

  • INTENDED: locked and prunable follow Git's own rule on Git older than 2.31. Those versions print neither flag, so Orca used to guess prunable by checking whether the worktree folder exists. The model reads the flags the way newer Git does. A locked worktree shows as locked. A locked worktree whose folder is gone is not prunable, because Git never prunes a locked worktree. A worktree whose checkout .git is gone is prunable. Orca's refusal to remove a locked worktree and the disk-usage scan's filter now see these rows as Git itself would.
  • INTENDED: one model per registered repo path, not per Git directory. How Orca spells the main path (separate Git directories, submodules) depends on the path the repo was registered with. Two registered repos that share one Git directory each keep a model, and a change to one bumps both. The cost is duplicate stats in that rare setup.
  • INTENDED: a model's first re-check re-reads every entry once more. A file a row turns out to depend on (its branch file, its checkout's .git) has no stat from before it was read. So it counts as changed once, rather than risking a missed write. At 812 worktrees that took 70 ms, once per model, with no Git.
  • Known limit: core.bare changed while Orca runs. The main row's bareness comes from the model's one Git listing, so the change shows at the next launch or model rebuild.
  • Known limit: core.ignorecase set only in the global Git config. Linked rows can come out in a different order from Git's. The rows themselves are identical, and the parity check ignores order.
  • Known limit: a file read that never returns, as on a dead hard-mounted network drive. It holds that repo's one re-read, and one thread of Node's file thread pool, until it returns. Readers still fail at their own deadline, and no more file work starts for that repo. On the base branch the same hang sat in a git child process instead.

Linked Issue

N/A (internal). Root cause from the 2026-09-28 create-latency traces.

Visual Proof

N/A: no UI change. The sidebar, CLI and mobile show the same rows; what changes is how often Orca runs Git to get them. Screenshots from the live QA run are under Testing.

Testing

Automated tests (vitest, explicit files):

  • Parity with real Git (worktree-membership-real-git.test.ts) builds repos with the real Git binary and asserts the model's rows equal git worktree list row for row. It also asserts that the model stayed on file rows, so a silent fall back to Git cannot pass. States covered: main, branch, detached, unborn, locked with a reason, prunable (folder deleted), missing linked HEAD, a branch only in packed-refs, a branch shared through --force, a path with a space, sparse, and a bare repo with worktrees. A commit, a sibling's update-ref, an outside add, a lock/unlock and an outside remove are each seen by stat alone, with no Git listing. Relative gitdir (Git 2.48+) and reftable (Git 2.45+) repos go to Git and still match it exactly. The test also runs in CI's Windows packaging job, where Git writes C:/… paths.
  • Model store (worktree-membership-store.test.ts):
    • Five rounds of a commit plus Orca's change marks, each read by three readers at once, run no git worktree list after the first build, and every reader sees the new head.
    • A missing repo costs one stat and no Git. A Git failure keeps the lenient, strict and real-empty answers apart. A folder that is not a Git repo is a real empty. A WSL path builds no model.
    • A result is reused inside the 200 ms window, and an unreported change is then seen by stat. A reader that arrives after an Orca change never reuses older work, including work still running or a first build.
    • A change in a repo registered through a symlinked path is seen one watcher debounce later.
    • Registered repos that share a Git directory are marked together.
    • Files that keep failing to read run Git only when their stats move. File rows are adopted after a failed first read only through the parity check.
    • The five-minute full re-read. Only the entry whose files moved is re-read. Unregistered and idle models are dropped.
  • Deadlines on a hung disk (worktree-membership-store-deadline.test.ts): a read fails at its own deadline, and nothing new starts behind the stalled work. One re-read runs at a time however many readers arrive. Readers that gave up leave nothing attached to a hung re-read or a hung first build. A read inside the reuse window touches no disk.
  • Shared file rules (worktree-admin-file-reads.test.ts, worktree-membership-git-rows.test.ts): the head-identity reader and the model agree on the same fixtures. A symbolic-ref chain ends where Git's does. A symlinked HEAD goes to Git. Only the packed-refs entries rows use are kept. Parity ignores linked-row order, which depends on the Git build and locale.
  • Runtime scan (worktree-scan-runtime-cache.test.ts, which also runs in the Windows packaging job; worktree-scan-runtime-membership.test.ts):
    • An invalidation rescans, and a just-created id resolves.
    • Concurrent callers share one scan. Scoped id resolution works.
    • A fleet snapshot settles within one per-repo budget however many repos stall. A healthy local repo answers fresh on every snapshot while many SSH repos stall.
    • A 1 Hz poll is answered with no re-listing, and row caches are kept only where Git answers. This replaces the deleted 1 Hz polling test.
  • Sidebar scan (detected-worktree-scan-membership.test.ts): no stored rows for a repo the model reads from files, and an unreported change still shows. The 5 s row cache is kept for a repo that stays on Git.
  • Changed existing tests:
    • Runtime tests that mocked listWorktreesStrict for the runtime scan also mock listWorktreesFromMembershipStrict.
    • Git-level suites that drive listings through a mocked Git with made-up paths now route reads through worktree-membership-store-git-answered-mock.ts, the Git-answered path the model keeps for layouts it cannot read.
  • The union of test files around the changed modules (git listing, sidebar scan, watcher, head identity, removal, runtime, and the orca-runtime.test.ts spec harness) passes locally. repos-remote-client-events.test.ts can time out under that full load. It passes alone, and it does the same on the base branch.
  • Each concurrency and freshness fix was also checked by reverting it alone and confirming its test fails.

Live QA in a built app on a second Mac, with an isolated app profile. It compared the base branch's build (A) with this PR's build (B, commit ed080aa5908) on a repo with 300 worktrees plus a small one. Git was wrapped to count every git worktree list, and the counts were cross-checked against trace spans.

  • Same rows. The sidebar and orca worktree list showed 302/302 rows for the 300-worktree repo and 4/4 for the small one, on both builds. Paths, branches and heads matched git worktree list. After creating, renaming and removing a worktree from the UI, B showed 304/304.
  • Outside changes appear fast. Every change made from a terminal appeared on B within 0.9–3.0 s. A commit on a branch that exists only in packed-refs took 29.1 s to reach the CLI on A and 1.2 s on B.
  • Regression smoke. On B, terminals opened and worktrees switched with 0 renderer console errors and 0 main-process errors. No repo fell back to Git. A plain folder workspace opened with a terminal, and Orca ran 0 Git commands against it.
git worktree list runs A (base) B (this PR)
Startup and registration 13 3 (one per repo)
5 min idle 0 0
20 commits over 10 worktrees, plus 2 outside adds 8 0
9 outside changes 15 0
Deleted-folder repo, 150 s 2, plus 2 failed runs on the deleted repo 0

On that idle machine, A made one failed pair of runs 17 s after the repo was deleted and then stopped; it did not show the every-37 s loop from the traces.

Seen on both builds, so not caused by this PR: after adding a repo and turning on "show external worktrees" right away, the sidebar shows only the main worktree until a second refresh. A worktree folder deleted from outside Orca drops out of the sidebar while Git still reports it as prunable.

Screenshots (build B), also in this comment:

Sidebar after startup with the 300-worktree repo listed

Sidebar search after changes made from a terminal

A worktree created from the UI

wt-10 after the rename and switch

A plain folder project, with no Git runs against it

Re-run on d8d09fe1048 (the head after the last concurrency fix): startup parity 302/302, 9 outside changes all within 3 s (CLI 304/304 after), git worktree list 3 at startup then 0 during the burst, the outside changes and the deleted-repo window, and 0 renderer console errors. The final head 297fe960dbb only swaps in the existing promise-settlement-waiters helper for the waiter list; CI is green on it.

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Review

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Security: ref names read from Git's files are checked before they are joined into a path (no .., \ or :). Object ids must be hex of the repo's object-format length. Paths read from gitdir are only stat'ed, never read.
  • Cross-platform: Windows C:/ records are used byte for byte, and core.ignorecase changes only the sort order. WSL and SSH are routed as described above. Folder workspaces never reach the model.
  • Remote/SSH and wire compatibility: SSH is unchanged, and no RPC or stream changed. Published rows differ only in locked/prunable on Git older than 2.31, as described above.
  • Mobile: the row shape is unchanged; only prunable/locked on Git older than 2.31 differ, as described above.
  • Git 2.25: the file rules never run Git. On old Git the model reports locked/prunable more accurately than old Git's own git worktree list output does.
  • Performance: no new Git on hot paths. A read of a model backed by files costs a stat pass (16–20 ms at ~800 worktrees), and nothing while the 200 ms reuse window holds. An idle repo costs nothing.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

…f re-running git worktree list

Every local listing (sidebar, runtime fleet, file-access roots, PTY hydration,
push-target and ownership checks) now reads a per-repo membership model that
derives rows from Git's admin files and re-validates them by stat on reads
older than 1 s. One `git worktree list` per model supplies the main row and a
parity check; repos whose layout the file rules do not cover (reftable,
relative gitdir records, core.worktree, config includes, WSL) or that fail
parity stay on Git, gated by the same stat signature. A missing repo costs one
stat and no Git. The runtime's admin fingerprint is deleted; the detected and
runtime row caches keep no rows for model-backed repos; the fleet fan-out is
capped at 8.

The head-identity reader now shares the model's admin-file primitives, which
also makes it report the branch a symref chain ends at, as Git does.

Runtime tests that fed the runtime scan through a mocked `listWorktreesStrict`
now mock `listWorktreesFromMembershipStrict` too (same mock object where the
test reuses it), because the scan reads the model instead of Git. The admin
fingerprint gate test keeps its non-fingerprint cases as
worktree-scan-runtime-cache.test.ts.
…e waiting for

A reader arriving while another reader's derivation held the just-consumed
dirty scope saw a clean scope plus a fresh memo and got the old rows; consuming
a change now retires the memo so concurrent readers join the derivation.

Also: the annotated listing is layered on the shared graph read again, so the
Git-answered fallback keeps its one-listing sharing, and model rows (which
already carry sparse state) skip the probe. Tests: both readers agree on shared
admin-file fixtures, the symref-chain branch fix, the fleet cap of 8, and the
file layers split under the line limit.

Git-layer unit tests that drive the listing through a mocked Git with made-up
paths now route reads through the Git-answered path, since the model would
otherwise answer those paths as missing repos with one stat.
… is passed

Orca's Git runner treats a \\wsl$ UNC repo path as WSL whether or not the caller
names a distro, and translates its rows; the membership model now routes those
repos to Git by the same rule and skips its one-stat missing-repo shortcut
there, since a stopped distro can make the path read as absent.
… the membership model changed

Nine Git-level suites (porcelain forms, capability probes, removal lookups) mock Git with made-up
paths, so their listings now stopped at the model's missing-repo stat and never reached the mock.
They share one seam that lets Git answer, as the model does for layouts it cannot read from files.

Runtime factory mocks of git/worktree lacked listWorktreesFromMembershipStrict, so their fleet
scans silently took the degraded path (and three integration suites failed). They now map it to
the same mock as listWorktreesStrict, which also makes the SSH routing guard able to fail again.

The real-Git parity test compares row paths separator-insensitively (Git for Windows prints `/`)
and removes trees through the retrying helper the Windows lane requires.
…ions and bounded on a hung mount

- The 1 s memo is now keyed to the generation its derivation started at. An older derivation that
  finishes after a newer one started can still commit, but can no longer re-open the memo with rows
  from before a create or remove Orca had already marked.
- A mark that lands while a model is first built is replayed onto it when the build finishes.
- A cold build that fell back on a transient read failure runs the Git parity check on its next
  derivation instead of adopting file rows unchecked.
- Model reads are bounded by the caller's timeout (default: the listing timeout), and fail the way a
  Git timeout does. A build or derivation past its deadline stays registered until it settles, so
  later reads fail fast instead of queueing more fs work on the hung mount. The missing-repo stat
  moved into the build and derivations, off the memo-hit path.
- Models remember their common dir's realpath: a watcher mark (which names the realpath) reaches a
  repo registered through a symlink, and Orca's own mark reaches every registered repo sharing the
  changed repo's common dir.
…leet budget per snapshot

- Only a model that derives rows from Git's files counts as model-backed. A repo the model leaves
  to Git (WSL, reftable, config include, failed parity) keeps the detected 5 s and runtime 30 s row
  caches, so a WSL repo is no longer stat-validated over UNC on every read past the 1 s memo.
- The fleet snapshot's per-repo 5 s budget now runs from the snapshot's start. The cap of eight
  concurrent repo reads no longer turns stalled repos (a half-open SSH connection) into waves of
  5 s each; repos still waiting for a slot past the budget answer from stored rows.
- Tests: a runtime 1 Hz poll over real repos (one listing, no cached rows for a file-derived repo,
  cached rows for a Git-answered and an SSH repo, and still fresh), the detected cache's two
  branches, and the fleet budget under 16 stalled repos.
…er; keep packed refs small

- A symlinked HEAD (`core.preferSymlinkRefs`) is read by Git as a symref to the link's name; the
  file rules followed the link and reported a detached or missing HEAD. It now routes the repo to
  Git, which also covers the setting coming from global config.
- The symref chain budget counts the HEAD read, as Git's does. A four-hop chain now pins to Git
  instead of reporting a branch Git does not print. The head-identity reader shares the rule.
- The parity check compares linked rows regardless of order. Git's order depends on its build and
  locale (Apple Git folds Latin-1 case under a UTF-8 locale), so non-ASCII names no longer pin a
  healthy repo to Git; the model's linked-row order is Orca's own.
- A model keeps only the packed refs its rows looked up (per packed-refs stamp), not the parsed
  file: tens of thousands of refs were megabytes per model. A name not looked up yet re-parses.
…mo plus watcher marks

The store's 1 s memo was only as fresh as the marks that cleared it, and watcher marks named the
common dir by a different spelling than the model (a repo registered through a symlink was never
marked). Reuse is now decided by one rule for finished and in-flight derivations alike: a result
is reused only if its derivation started at the model's current generation and under 200 ms ago.
Every watcher-driven read starts at least one 250 ms trailing debounce after the change, so it can
never reuse a derivation that began before the change, whatever path the watcher saw.

- The watcher-to-model marks and the four-part dirty scope are gone; the stat signature decides
  which entries to re-read. Orca's own mutation marks stay: they bump the generation and owe a
  listing re-read, and reach every registered repo sharing the changed repo's common dir.
- A model is registered before its first build starts, so a mark during the build counts; the
  separate pending-builds map is gone. Each reader awaits shared fs work under its own deadline.
- WSL repos get no model: Git answers them as on base, with the upper caches, since a Win32 stat
  over 9P can read an existing directory as absent.
- The unused `fresh` read option is removed, and a row's stat signature is read one file at a
  time since entries already run eight at once.
- The removal chain looks the removed worktree up with Git again (unshared, lenient), as the
  design keeps removal on Git; the row decides which branch gets deleted.
…-gated Git while files fail

- One derivation runs per model. A reader that cannot reuse it (older than the 200 ms window, or
  from before a mark) shares one queued follow-up that starts when it settles, so it still began
  after the reader arrived. On a slow or hung disk, readers no longer stack fs passes, and the
  separate stalled-work gate is gone (a stalled derivation already blocks the next).
- The runtime's local scan waits on the model for its own 5 s budget (`waitMs`), not the 30 s
  listing timeout; Git keeps its own timeout.
- The fleet snapshot reads every repo at once again, each with its own 5 s budget, as on base.
  With the cap, eight stalled repos (a half-open SSH host) held every slot and each later healthy
  repo answered stored rows on every snapshot. A model runs one derivation at a time and Git
  spawns go through admission, so the cap bounded nothing the fleet still needs bounded.
- A files model with no file state, or whose files just failed, is read like a Git-derived model:
  Git runs only when a stat moves, a mark owes a listing, or the floor is due, and that listing
  is a new parity attempt whose baseline is the answer if files still fail. Such a repo is not
  model-backed, so the upper caches keep their TTLs. A read error no retry cures (a HEAD naming a
  ref that is a directory) no longer runs Git twice on every read.
- Removed a leftover comment of the deleted admin-fingerprint field.
…te' into brennanb2025/worktree-membership-events
…ng attached to it

Each reader waited on shared model work (the running derivation, the queued follow-up, the first
build) with its own `.then`. On a hung mount that work never settles, so every reader that timed
out stayed attached to it: about 1.4 KB per read, growing until the mount came back. Shared work
now carries one reaction that settles a waiter set, and a reader that times out or is aborted
removes itself. A pending follow-up also counts as busy, so a reader arriving between a
derivation settling and its follow-up starting joins that follow-up instead of starting a second
derivation.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Live app QA screenshots (macOS, isolated test profile, PR build ed080aa5908)

These were taken on a separate test Mac. The app was driven only through its debugging connection, used a throwaway profile, and had no credentials. The build is PR commit ed080aa5908. The two commits after it (6c9a13064d9 hung-read cleanup, d8d09fe1048 doc removal) were not part of this run.

Sidebar after startup. A test repo has 300 worktrees plus one detached one. The sidebar and orca worktree list both show 302 rows and match git worktree list exactly, including paths, branches and HEADs.
Sidebar after startup with the 300-worktree repo

Changes made outside Orca show up about 1-3 seconds later. These were done from a separate shell: add, remove, move, lock and unlock, commits (including on a branch stored only in packed-refs), and renaming a checked-out branch. The search shows wt-34-renamed and the moved wt-31.
Worktree search after the outside changes

Worktree created from the app (Blank Terminal as the agent):
UI-created worktree with its terminal

After renaming it to "UI Renamed One" and switching worktrees. Opening terminals and switching between worktrees produced no renderer console errors.
Switched to wt-10 after the rename

A plain (non-git) folder added as a project opens with a working terminal, and Orca runs no git commands against it:
Plain folder project open with a terminal

How often git worktree list ran, base build vs this PR (same steps on each):

  • 5 minutes idle: base 0, PR 0.
  • A burst of 20 commits across 10 worktrees plus 2 outside git worktree adds: base 8, PR 0.
  • 9 outside worktree changes: base 15, PR 0.
  • A registered repo whose folder was deleted, 150 seconds: base 2 plus 2 failed launches, PR 0.
  • At startup the PR ran it once per repo.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status: ready for review (head 297fe960dbb)

The problem. Creating a worktree could take minutes because Orca competed with itself for the disk. One cause: Orca ran git worktree list over and over, and each run walked every worktree (0.2–0.5 s at ~800 worktrees). Every worktree change event and every cache expiry made several separate readers re-list the whole repo, and three repos whose folders had been deleted were listed, and failed, about every 37 s forever.

What changes for users. After startup, Orca no longer re-runs git worktree list for a repo whenever something changes; it keeps one list per repo and re-reads only the Git files that changed. The sidebar, orca worktree list / ps and mobile show the same rows as before, and changes made from a terminal show up at the next refresh, including while the window is hidden. A repo whose folder was deleted costs one file check and no Git. On Git older than 2.31, locked and prunable now follow Git's own rule instead of Orca's guess.

What the review changed (all pushed):

  • CI was red: 13 test files mocked Git at a layer the new model now sits in front of, and the new real-Git test compared Windows paths with the wrong separator. All were fixed; no behaviour regression was behind them.
  • The cache in front of the model was simplified. A result is now reused only if nothing changed since it started and it is under 200 ms old, below the 250 ms delay the file watcher waits before it triggers a refresh. So every refresh after a change re-checks the disk. This replaced a 1 s cache plus watcher-driven marks, and it removed three stale-read bugs at once: an older refresh reopening the cache, a change during the first build being lost, and a repo registered through a symlink missing change marks.
  • Reads of Git's files are now bounded by the caller's deadline. Each repo runs one re-read at a time, plus one shared follow-up, so a hung network drive cannot stack up file work, and readers that give up leave nothing attached (this reuses the existing promise-settlement-waiters helper).
  • The new limit of 8 repos per fleet scan was removed. With it, a few stalled SSH hosts could hold every slot and hide worktrees in healthy repos from the CLI and mobile.
  • WSL repos keep today's Git listing and caches. Other repos that stay on Git keep their caches. Removing a worktree asks Git directly again.
  • Smaller fixes: a symlinked HEAD goes to Git; the symbolic-ref depth limit matches Git's; the parity check ignores row order; repos sharing one Git directory get each other's change marks; only the packed-refs entries that rows use are kept in memory; repeated file-read failures re-run Git only when files change; and a stale doc and trigger list for the deleted fingerprint check were removed.

Deferred, recorded as follow-ups:

  • Publish only real row changes, so worktrees:changed fires less often.
  • Move the head-identity refresh onto the model.
  • Run the same model on the remote host for SSH repos.
  • A cheaper two-level stat check: 3 ms instead of 17 ms per read at 800 worktrees, pending checks on a loaded disk and on filesystems other than APFS.
  • core.bare changed while Orca runs: seen at the next launch.
  • Row order when core.ignorecase is set only globally.
  • An interactive read can wait behind a background Git run for repos that stay on Git.

Verified.

  • CI on 297fe960dbb: 25 checks pass, including the Windows package job, which runs the real-Git parity test. 0 failed.
  • The node typecheck passes.
  • Every concurrency and freshness fix has a test that fails when that fix alone is reverted.
  • Rows are byte-identical to git worktree list with real Git 2.50 across 13 layouts. On the 2.25 baseline, paths, heads and branches match.
  • Live QA in a built app on a second Mac, on a 300-worktree repo, comparing the base build with this PR at ed080aa5908:
  • Re-run on d8d09fe1048 (the head after the last concurrency fix): startup parity 302/302, 9 outside changes all within 3 s (CLI 304/304 after), git worktree list 3 at startup then 0 during the burst, the outside changes and the deleted-repo window, and 0 renderer console errors. The final head 297fe960dbb only swaps in the existing promise-settlement-waiters helper for the waiter list; CI is green on it.

Not verified.

  • Real WSL, and a real stalled network mount: hung-mount handling is proven with simulated file reads only.
  • Timings under heavy disk load.
  • Windows beyond CI.

Seen during QA on both builds, so not caused by this PR:

  • After adding a repo and turning on "show external worktrees" right away, the sidebar shows only the main worktree until a second refresh.
  • A worktree folder deleted outside Orca drops out of the sidebar, while orca worktree list keeps it without a prunable marker.

@brennanb2025
brennanb2025 marked this pull request as ready for review September 30, 2026 22:47
@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 3/5

[High risk] Refactors worktree listing to cache per-repo state instead of re-running git.

The PR does not yet appear safe to merge because two previously reported stale-listing cases remain unresolved.

Findings

  1. P1 Repointed gitfile leaves stale listings ▶
  2. P1 Ancestor repositories stay hidden ▶

Summary

The PR replaces repeated local git worktree list calls with a per-repository model that checks Git admin files for changes.

  • Shared membership reads serve sidebar and runtime listings while preserving Git-backed paths for unsupported layouts.
  • The current head contains no changes since the previous review.

Reviews (5) · Last reviewed commit: "ci: retrigger checks (previous runs stuc..."

Comment on lines +262 to +271
if (
!(await stat(model.commonDir).then(
() => true,
() => false
))
) {
// The repo's Git dir is gone: drop the model so the next read re-resolves the layout, and let
// Git give this read its answer (usually "not a git repository", a true empty).
dropModel()
return (await readGitWorktreeRows(model.repoPath, gitOptionsFor(options))).rows

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Repointed gitfile leaves stale listings

If a registered linked worktree’s .git file is repointed to another Git directory while the old directory still exists, this check accepts the old directory. Later reads, including the five-minute full refresh, continue listing worktrees from that directory rather than the one the registered worktree now uses. Change notifications do not correct the mapping, so the wrong listings persist until the model is dropped.

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes

This run reviewed the complete diff of #24085 against brennanb2025/create-scan-idle-gate — 112 files, 17 commits, from 48e98bf837 through 297fe960db.

  • New worktree-membership model (src/main/git/worktree-membership/): one stat-validated row model per registered local repo, built from a single git worktree list baseline plus file-derived rows. A one-time byte-for-byte parity check pins any mismatching or unsupported layout to Git; later reads re-validate by a mtimeMs:ctimeMs:size:ino signature and a 5 min full-derive floor.
  • Shared admin-file rules (worktree-admin-file-reads.ts, git-config-file-entries.ts): one reader for HEAD/refs/packed-refs/config, now backing both the model and the head-identity reader. The symref-chain fix reports the terminal ref, matching Git's add_head_info.
  • Store concurrency (worktree-membership-store.ts): one derivation per model, a 200 ms reuse window under the watcher's 250 ms debounce, a single shared follow-up, generation + listingOwed marks, per-reader deadlines via PromiseSettlementWaiters, and idle/unregistered drops.
  • Integration: the runtime resolution scan and sidebar detected scan stop storing rows for model-backed repos (isWorktreeMembershipModelBacked); listWorktreesStrict still runs Git for removal/pre-mutation; WSL, SSH, folder-workspace, and pinned-to-Git paths keep their prior caching.
  • Cleanup: the old Git-admin fingerprint module, its reference doc, and its suites are removed and their coverage moved to the new model suites.

I verified the file rules against Git's own source semantics (2.47/2.50 worktree.c should_prune_worktree, builtin/worktree.c add_head_info/symref budget, lock trimming), traced the store's build/inFlight/followUp/mark/reuse interleavings for stale-row, lost-mark, and leaked-waiter outcomes, and traced the SSH/WSL/folder/pinned/removal paths. No divergence from Git is served silently for a state reachable through normal Git commands, and no interleaving loses a marked change or leaks a waiter. Existing tests pin counts and identities rather than restating assumptions.

Two non-blocking notes, neither of which changes the verdict: (1) locked/prunable intentionally follow Git's newer rule on Git < 2.31, which is a deliberate behavior change the PR documents; (2) the freshness tradeoffs that the deleted docs/reference/worktree-scan-fingerprint.md recorded now live only in the PR body and code comments, so if the team wants that contract discoverable in docs/ a replacement would be worth adding in a follow-up.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

…te' into brennanb2025/worktree-membership-events

# Conflicts:
#	src/main/git/worktree-add-creation-config.test.ts
#	src/main/git/worktree-graph-listing.test.ts
#	src/main/git/worktree-remove-branch-deletion.test.ts
#	src/main/git/worktree-scan-cache-sharing.test.ts
#	src/main/runtime/runtime-registered-local-worktree-removal.ts
…te' into brennanb2025/worktree-membership-events

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes

This run reviewed the delta since the prior pullfrog review (297fe960db): two merges of the advanced base brennanb2025/create-scan-idle-gate — which now carries #23837's background worktree removal — plus one new test. No new PR-authored production code.

  • Merged the advanced base into the branch, reconciling the membership model's invalidation marks with the base's background-removal finish paths in remove-registered-local-worktree.ts and runtime-registered-local-worktree-removal.ts; markWorktreeMembershipDirty is retained after Git's prune, so the next membership read re-derives.
  • Added worktree-membership-background-removal.test.ts: an integration test that drives a real repo through startBackgroundWorktreeRemoval, asserts the model still lists the row (as Git does, prunable) mid-delete and that projectPendingWorktreeRemovals marks or hides it, then asserts the row is gone after the delete settles. It freezes Date.now so the 200 ms reuse window would otherwise still serve the stale listing — proving the delete's generation bump, not elapsed time, invalidates the cached read. I ran the file locally; it passes.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

…t a Git run per read

A registered folder that still exists but is no longer a repository (its `.git` deleted, or an
unmounted volume's mount point) ran `git worktree list` on every read, bounded only by the upper
5 s / 30 s caches: the same polling this change removes for missing repos.

Once Git itself answers "not a git repository", that error is kept with stamps of the folder and
its `.git`, taken before Git ran. Each later read stats those two paths and re-throws Git's own
error, so lenient, strict and true-empty readers classify it exactly as when Git answered. A moved
stamp (`git init`, a restored `.git`, an in-place bare init, the folder deleted) or the 5 min floor
(a parent directory becoming a repository, the environment) sends the next read to Git again.
Readers share one stat check under their own deadlines. WSL and folder workspaces never reach it.
Comment on lines +33 to +34
function verdictDependencies(repoPath: string): AdminStatDependency[] {
return [{ path: repoPath }, { path: join(repoPath, '.git'), noFollow: true }]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Ancestor repositories stay hidden

If a registered folder has already been cached as “not a repository” and its parent is then initialized as a Git repository, neither path checked here changes. Later reads keep returning an empty worktree list for up to five minutes, even though Git can now discover the parent repository from that folder.

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes

This run reviewed the incremental delta since the prior pullfrog review (521eb308de): commit 80e93d43, which stops re-running git worktree list for a registered folder Git says is not a repository.

  • New worktree-membership-not-repository.ts: caches Git's "not a git repository" answer per registered folder, keyed by canonical path, with a stat signature over the folder and its .git. Later reads re-check by stat within the 5 min floor, share one stat check across concurrent readers via PromiseSettlementWaiters, and drop on 30 min idle or unregistration.
  • Store wiring (worktree-membership-store.ts): readWorktreeMembership consults the verdict before building a model and rethrows the cached error; the model-build-failed path now goes through readWithoutModel, which stamps the signature before Git and only caches on a not-a-repository error, so a repo appearing mid-run is seen by the next read.
  • New test (worktree-membership-store.test.ts): asserts a plain folder answers strict/lenient/true-empty across three reuse-window rounds with no git worktree list spawn, then lists the folder once a real git init appears.

I traced the shared stat-check interleaving, the stale-write race (the pre-Git stamp corrects it), the interaction with the model and MissingRepoPathError paths, and the idle/unregistered drop wiring. The added test fails if the cache is removed — spawns would grow each round. The open Greptile thread on the new file (a parent directory becoming a repository after caching) describes the behavior the module documents as intended and bounds by the 5 min floor.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

…shared model build

The stamps a not-a-repository verdict keeps were taken after the build resolved, outside every
reader's deadline: on a mount that died in that gap the reader never settled, and each reader that
joined the build stamped on its own. The build now takes them before it resolves and hands them out
with its result.
…te' into brennanb2025/worktree-membership-events

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes

This run reviewed the delta since the prior pullfrog review (80e93d43): commit 5ac5526c4e plus a merge of the base branch (now fully merged here, merge base = base tip).

  • Moved the "not a repository" pre-Git stamp into the shared model build. buildMembershipModel now returns GitWorktreeInfo[] | UnplacedRepo instead of GitWorktreeInfo[] | null, and takes the folder's stat signature via stampUnplacedRepo before Git is asked. The store drops the model and hands the stamps to readWithoutModel, which caches only on a not-a-repo error. This closes a real hang: previously the stamp ran after awaitModelWork(building.work) returned, so a stat stalled on a hung mount was outside the reader's deadline and the read never failed.
  • Added a deadline test (worktree-membership-store-deadline.test.ts): a plain folder whose <folder>/.git lstat is hung (the layout probe uses stat, so only the stamp blocks) now fails the read with WorktreeMembershipTimeoutError. I confirmed this fails against the pre-fix shape, where that stamp sat outside the deadline.
  • Merged the base branch. No net production-code change versus 80e93d43.

I traced the stamp's ordering relative to Git (an earlier stamp is conservative — a folder that becomes a repo mid-run yields a signature mismatch on the next read, or a successful Git listing, never a stale verdict), the !Array.isArray narrowing plus dropModel in startModel, and the single readWithoutModel call site. The stamp happens inside the promise wrapped by awaitModelWork, so every reader is bounded; the build itself is not cancelled, which is the module's documented hung-read limit. worktree-membership-store-deadline.test.ts (7) and worktree-membership-store.test.ts (19) pass locally.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@nwparker

Copy link
Copy Markdown
Contributor

The traces make a strong case (~2,600 s/day of git on an 800-worktree repo), but the branch has drifted ~1,750 commits and touches 195 files, including create-preparation code main still changes. Closing. Could you re-cut just the membership model as a focused PR on current main, measured against the fingerprint gate?

@nwparker nwparker closed this Oct 14, 2026
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.

2 participants