feat: recover sibling worktrees via git worktree registration (#3)#27
Merged
Conversation
resolveMainRepo walked up from a session cwd to the nearest directory .git,
which missed SIBLING worktrees — a checkout beside the main repo (e.g.
`…/<repo>-wt-cli` next to `…/<repo>`), the layout `git worktree add` produces
for a sibling clone. ~29% of sessions on worktree-heavy repos were dropped as
unresolvable_cwd.
resolveRepo now returns RepoResolution { repo, checkoutRoot }. At each ancestor
lacking a .git dir it scans SIBLING dirs for a directory .git and reads each
candidate's .git/worktrees/<name>/gitdir (which git writes as <checkout>/.git —
the trailing /.git is stripped); if a registered checkout is an ancestor of the
cwd, that sibling is the main repo. Paths are best-effort canonicalized (handles
/var ↔ /private/var and deleted cwds). The recovered checkout root is threaded
through runScan and runReflect into relativization, so sibling-worktree files
(absolute, outside the repo prefix) collapse onto the main checkout's areas
instead of fragmenting into absolute-path area keys.
Sandbox contract preserved: production code only stat/readdir/reads tiny gitdir
text files — no git invocation, shell, network, or writes.
Two code-review rounds; the first caught a Critical gitdir-format bug that
synthetic tests had masked (fixed: real <checkout>/.git format + canonicalization
+ relativization threading). 403 tests + a real `git worktree add` probe green.
Closes #3.
Co-Authored-By: Claude <noreply@anthropic.com>
This was referenced Jul 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
resolveMainRepowalked up from a session cwd to the nearest directory.git, which missed sibling worktrees — a checkout beside the main repo (e.g.…/<repo>-wt-clinext to…/<repo>), the layoutgit worktree addproduces for a sibling clone. The main repo is a sibling, not an ancestor, so the walk hit the shared parent (no.git) →null→ the session was dropped asunresolvable_cwd. ~29% of surviving sessions on worktree-heavy repos were lost this way (the priorgit rev-parse --show-toplevelpath dropped them too). Closes #3.Changes
src/git.ts—resolveReponow returnsRepoResolution { repo, checkoutRoot }. At each ancestor lacking a directory.git, it scans sibling dirs for a directory.gitand reads each candidate's.git/worktrees/<name>/gitdir. Git writes<checkout>/.gitthere (not the checkout root), so the trailing/.gitis stripped and the registered checkout is checked as an ancestor of the cwd. Paths are best-effort canonicalized (handles/var↔/private/varand since-deleted cwds).resolveMainRepo/walkToRepoare thin wrappers.src/relativize.ts—relativizeFilePath/relativizeEnvelopeFilestake an optionalworktreeCheckoutRoot; sibling-worktree files (absolute, outside the repo prefix) are stripped of the checkout root first, so they aggregate with the main checkout instead of fragmenting into absolute-path area keys.src/pipeline.ts— threadscheckoutRootthroughrunScanandrunReflect(both--transcriptand--latest) into relativization.git worktree add(authentic<checkout>/.gitgitdir format): live + deleted sibling recovery, false-positive and string-prefix-not-ancestor negatives, sibling relativization, and end-to-endrunScan+runReflecttests proving recovery and aggregation.Sandbox contract
Preserved: production code only
stats /readdirs / reads the tinygitdirtext files under candidate.git/worktrees/. Nogitinvocation, no shell, no network, no writes. (Tests shell out togitonly to build fixtures.) Uses git's own worktree registration — not a-wt-naming heuristic (the locked decision in #3).Review
Two code-review rounds via the
requesting-code-reviewskill (3 parallel reviewers → merged findings → re-review of fixes).<checkout>/.gittogitdir, not<checkout>, so the feature recovered zero real worktrees. Fixed: real gitdir parsing (C1), canonicalization (C2), relativization threading (I1), and rewrote tests against realgit worktree add(C3).git worktree addprobe incl. the macOS/var↔/private/varsymlink case and false-positive rejection) and confirming the tests are load-bearing (reverting the source makes the sibling tests fail:session_count1 → 2).Verification
npm run build— clean (tsc, no errors)npm test— 403/403 passinggit worktree addprobe — 8/8: live sibling resolves + relativizes; deleted sibling resolves + relativizes (the dogfood case); no false positives; main checkout / nested worktree behavior unchanged.🤖 Generated with Claude Code