You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
🐛 Map panel says "gh not installed" when the followed checkout was deleted #347
The map panel reports "gh not installed" on refresh when gh is installed (/usr/bin/gh) but the checkout the panel follows has been deleted.
Repro (observed in ziptv2):
Work a ticket in its workspace /home/torvalds/Git/ziptv2-t734 (sibling clone / worktree); the panel pins that checkout.
Close #734 in the target repo, which removes the workspace directory.
The session cwd is still the deleted workspace. Refresh the panel.
The panel shows "gh not installed".
Root cause (two defects):
Misleading message.failureReason in plugins/trailhead/hooks/mod/map-pane-view.ts (~line 433) maps any ENOENT / "not found" error to gh not installed. Spawning a process with a cwd that no longer exists also throws ENOENT (spawn fails on the cwd, not on the binary), so a missing directory is reported as a missing gh.
No fallback when the followed checkout disappears. The repo pin (repo-pin.ts) re-derives the checkout from $.session.cwd() (git rev-parse --show-toplevel). When the session cwd is the deleted workspace that read fails and the panel keeps the stale pinned path, so every gh run keeps failing. It should fall back to a still-existing checkout of the same repo (the main working tree, or another entry of git worktree list / a sibling clone).
Expected: when the pinned checkout is gone, the panel re-pins to an existing checkout of the same repo and keeps working; if none is found, the message says the folder is missing (e.g. "checkout not found: "), never "gh not installed". Check the cwd exists before blaming gh.
Workaround: move the session back into the main checkout, then close and reopen the panel with /trailhead:panel.
Pin the map panel to a ticket workspace checkout (e.g. <repo>-t<n>, a sibling worktree or clone), with the session cwd inside it.
Remove that workspace directory (closing the ticket integrates the branch and deletes the workspace).
Refresh the panel, session cwd still the deleted directory.
Expected: the panel re-pins to a still-existing checkout of the same repo and keeps listing tickets; with none found, it says the checkout folder is missing (checkout not found: <path>).
Observed: the panel shows gh not installed, although gh is on the PATH.
Code path:map-pane.tsx runs gh with cwd: pin.root; spawning with a cwd that does not exist throws ENOENT, which failureReason (map-pane-view.ts) maps to gh not installed. refreshPin (repo-pin.ts) re-reads the repo from the session cwd; in a deleted directory that read fails and nextPin keeps the stale pin, so every refresh fails the same way.
Root cause (two co-occurring defects, both confirmed):
Misattribution.gh() in map-pane.tsx spawns gh with cwd: pin.root. Node reports a missing cwd as ENOENT with the message spawn gh ENOENT, byte-identical to a missing binary. failureReason (map-pane-view.ts) maps ENOENT to gh not installed. Falsifiable: if this is the cause, spawn(echo, {cwd: /nonexistent}) and spawn(nonexistent-bin, {cwd: /tmp}) yield the same code and message. Test run: both printed ENOENT spawn <bin> ENOENT. Prediction held.
Stale pin.refreshPin (repo-pin.ts) reads the repo from the session cwd. In a deleted cwd the git reads throw and are swallowed; nextPin then keeps the current pin (or the host returns the memoized repo with the same dead root). The pin never leaves the deleted root, so every refresh fails. Falsifiable: if this is the cause, a fake engine whose cwd git reads deny ENOENT keeps repoPin.root at the deleted path after a turn. Test run (fake-engine probe): the pin stayed /work/repo-t7. Prediction held.
AND-gate: fixing (1) alone gives an accurate message but no recovery; fixing (2) alone hides the message only in this path.
Eliminated: a non-zero gh exit (goes through the stderr branch, never says "not installed"); isServed (returns without spawning for github.com).
Fix direction: in gh(), on an ENOENT throw, check whether pin.root still exists before blaming gh (new reason checkout not found: <path>); in the pin refresh, when the pinned root is gone, re-pin to the first existing checkout of the same repo (main working tree, another worktree, a same-remote sibling), using only the engine process API (the mod has no fs).
I recomputed the ticket diff with the protocol command. It returned two commits, a69fadc and 70fd28b, both read. I did not run the suite. I traced the two warnings and the info finding by reading the code, and did not execute them.
Findings ledger
W1: open
W2: open
I1: open
Warnings
W1. After a relocation, the full search re-runs on every refresh. /home/torvalds/Git/trailhead/plugins/trailhead/hooks/mod/repo-pin.ts:242-244
Trigger: the host's memoized session.repo() keeps naming the deleted root. The new test rig models exactly this, and the code comment on line 243 says it happens.
First refresh:livePin moves the stored pin to the live root.
Every later refresh:
nextPin(stored = live, repo = DEAD) sees a different pin and returns DEAD again.
livePin(DEAD) finds the root missing.
It then runs the whole search again, because missed only caches misses and a hit calls missed.delete.
sameValue(stored, pin) stays true, so no move fires, which hides the repeat work.
Cost per refresh:fs.list of the parent, then for the first matching sibling rev-parse, remote get-url and worktree list, then more exists and rev-parse/remote calls per worktree.
Scale: that is about 5 or more git spawns on every pane refresh, turn complete and tracker write, for as long as the session stays in the deleted cwd. This is the cost the brief asked me to check.
Fix: cache hits too, as a Map<deadRoot, liveRoot> that is re-validated with one exists call. Alternatively, in refreshPin, keep current when found.root is a known-dead root that already relocated.
Missing coverage: no test counts spawns across a second refresh.
W2. The tests leave the new contracts unpinned. /home/torvalds/Git/trailhead/plugins/trailhead/tests/repo-pin-relocate.test.ts
No test asserts that a live pin makes no extra spawns, only the single exists call. That was the cost concern for livePin running on every refresh.
No test covers the 60s negative cache, either that a second refresh inside the window spawns nothing or that one after 60s retries.
No test covers a same-remote sibling clone as the fallback. The goal lists this case, but both tests that find a live checkout go through worktree list.
Test 3 ("probes only from a live directory") only passes because the rig's process.run handler denies spawns whose cwd is dead. It never asserts that every probe ran with cwd: '/'.
A test with a second refresh after the relocation would have exposed W1.
Info
I1. The missed cache is module-level and keyed only by the dead root.
It is never reset on session start, unlike what isStart does for the pin.
Test 3 only passes in isolation because DEAD is shared between tests and the clock is presumably fake.
Reordering the tests, or reusing DEAD in a new test, could return the cached miss.
Consider exporting a reset for tests, or keying the cache by pinKey plus root.
Checked and found fine
Other git probes: all other probes use git -C <path> with cwd: '/', so none spawn in the dead directory.
Remote matching:checkoutOf compares host/slug, so SSH and HTTPS remotes of the same repo match. remoteId also case-folds the host.
Unreadable fs:exists defaults to true when the filesystem read throws, so a read error does not move the pin.
Missing gh: in gh() (map-pane.tsx), a missing gh with an existing cwd still reports "gh not installed". missingRoot is set only when exists is false.
Runtime constraints: nothing in the diff uses Node fs or Buffer. Only $.fs.exists, $.fs.list and $.process.run are used.
Pin atom and onMove:onMove still fires only when the stored pin changes, and before the atom update, so that ordering is preserved.
Security: no injection surface. The paths go as argv, never through a shell.
Goal-backward verification
status: met
I traced each claim to the two Refs: #347 commits (a69fadc, 70fd28b) and ran the full suite: node scripts/test.js gives 84 ok/skipped, 0 failed. The commit set from the exact Ticket-diff command is those two commits.
Claim 1: re-pin to a live checkout and keep working. class: met
70fd28b adds livePin in /home/torvalds/Git/trailhead/plugins/trailhead/hooks/mod/repo-pin.ts, called from refreshPin on the pin it is about to store. The pin stays unchanged if its root exists. If the root is gone, it moves to the main working tree, else another worktree, else a same-remote sibling clone.
Every probe runs as git -C <path> from /, so nothing spawns in the dead directory. This covers the stale-pin defect from the DIAGNOSIS, including the host returning the memoized dead root.
map-pane.tsx reads the pin atom on each refresh, so the moved pin is what gh runs in afterwards.
I did not find a test that re-pins and then runs a full panel gh refresh against the new root. The relocation tests check the pin atom. Together with the atom read, I count the claim as satisfied.
Claim 2: a missing checkout says checkout not found: <path>, never "gh not installed". class: met
In a69fadc, gh() checks $.fs.exists(pin.root) after a spawn throws. If the root is gone it passes missingRoot, and failureReason returns checkout not found: <path> before the ENOENT branch.
When no live checkout exists, the pin stays on the dead root (the "no checkout left" relocate test), so this message is what shows.
The fallback is safe: the retry of the search for the same dead root is capped at once every 60 seconds.
Claim 3: a real missing gh still says "gh not installed". class: met
When the root exists, or fs.exists itself throws, missingRoot is undefined. That falls through to the existing ENOENT → gh not installed mapping.
The view test asserts failureReason({ error }) without missingRoot is still gh not installed. The existing failureReason test for that message is also unchanged.
Claim 4: regression tests exist for each. class: met
Claim 1: /home/torvalds/Git/trailhead/plugins/trailhead/tests/repo-pin-relocate.test.ts. It has three tests: re-pin to the main tree, re-pin to another worktree when the main tree is gone, and keep the pin when none is left.
Claim 2: the panel test "a pinned checkout that is gone is named, not blamed on gh", which exists for each surface, in map-pane-ui.test.ts. The view test is in map-pane-view.test.ts.
Claim 3: the unchanged-behaviour assertions in map-pane-view.test.ts.
The only weakness is the one noted under claim 1. The new README text matches the behaviour.
Tests
Base current with origin/main (0 behind). Full suite node scripts/test.js: 84 ok/skipped, 0 failed (mod tests 670 pass, 0 fail).
Next: W1, W2 and I1 are taken in a second fix round before resolving.
I recomputed the ticket diff with the protocol command. It now returns three commits: a69fadc, 70fd28b and dfb9320. I read dfb9320 with --remerge-diff. I did not run the suite. I checked the paths below by reading the code and the new tests.
Spawn counts after a relocation: later refreshes make no git -C probe and the pin does not move again.
A live pin makes no probe.
The 60s miss window: no probe at 59s, a probe again after 61s.
A plain same-remote sibling clone with no worktree link.
Every probe runs with cwd === '/'.
The rig now has a controllable clock.now, so the window test does not depend on real time.
I1: fixed.
Both caches are keyed per repo and dead root.
refreshPin clears both when isStart is true.
A test confirms a session start probes again.
Residual notes (not findings)
A cached hit stays on a sibling checkout even if the main working tree comes back later, until that sibling dies or a new session starts. This is harmless and cheap.
When clock.now fails, the miss cache is skipped and the search runs on every refresh. That is the same as the previous behaviour and unlikely.
Goal-backward verification
status: met
Round 2. The Ticket diff recomputed with the exact Refs: #347 command is three commits: a69fadc, 70fd28b and dfb9320. I read dfb9320 with --remerge-diff and re-ran the suite: node scripts/test.js gives 84 ok/skipped, 0 failed.
Claim 1: re-pin to a live checkout and keep working. class: met
dfb9320 changes livePin in /home/torvalds/Git/trailhead/plugins/trailhead/hooks/mod/repo-pin.ts. A live pin returns unchanged after one exists check.
A found checkout is cached per repo and dead root in relocated. Each later refresh revalidates it with one exists check and drops it if it has also gone. This is needed because the host's memoized repo keeps naming the dead root.
The relocation tests check the pin atom, not a full panel gh refresh against the new root. map-pane.tsx reads that atom on each refresh, so I count the claim as satisfied.
The caches are cleared when a session starts, so an earlier session's cache cannot leak. missed is now keyed by repo and dead root instead of by dead root alone.
The earlier search is unchanged. It looks for the main working tree, then another worktree, then a same-remote sibling, with every probe run as git -C from /.
Claim 2: a missing checkout says checkout not found: <path>, never "gh not installed". class: met
dfb9320 does not touch gh() or failureReason, so the a69fadc behaviour is intact.
When no live checkout exists, the pin stays on the dead root and the miss is cached for 60 seconds. The message therefore still shows.
Claim 3: a real missing gh still says "gh not installed". class: met
Unchanged by dfb9320. With the root present, missingRoot is undefined and the ENOENT mapping applies. The view tests still assert this.
Claim 4: regression tests exist for each. class: met
Claim 1 now has these tests in /home/torvalds/Git/trailhead/plugins/trailhead/tests/repo-pin-relocate.test.ts:
re-pin to the main working tree, and to another worktree when the main tree is gone;
a same-remote sibling clone as the fallback;
after a relocation, later refreshes run no git probe;
a live pin costs no probe;
a search that found nothing is not repeated within 60 seconds and is retried after;
every probe runs from /;
a session start clears the caches.
Claim 2 keeps the panel test in map-pane-ui.test.ts and the view test in map-pane-view.test.ts.
Claim 3 keeps the unchanged failureReason assertions.
I found no new gaps in round 2.
Tests and acceptance
Base current with origin/main (0 behind). Full suite node scripts/test.js: 84 ok/skipped, 0 failed.
Acceptance (guided UAT, user-driven): passed.
Started a session inside a throwaway worktree of this repo with the fixed mod installed, opened the map panel: the map showed.
Deleted that worktree while the session cwd stayed in it, then refreshed the panel: the map kept showing (re-pinned to the main checkout), no gh not installed.
Cause: two co-occurring defects (see DIAGNOSIS). Spawning gh in a deleted checkout fails with the same ENOENT as a missing binary, and failureReason reported it as gh not installed; the repo pin kept the deleted root because the session cwd reads failed and the host kept naming the dead root.
Fix:
a69fadc: gh() in map-pane.tsx checks whether the pinned root still exists after a spawn failure; failureReason (map-pane-view.ts) then says checkout not found: <path>. A real missing gh still says gh not installed.
70fd28b: repo-pin.ts re-pins a dead root to a live checkout of the same repo (main working tree, another worktree, a same-remote sibling clone), probing with git -C from /, never from the dead directory; documented in the mod README.
dfb9320: the found checkout is cached (one existence check per later refresh, no git spawns), a fruitless search is retried at most every 60 seconds, and both caches reset at session start.
Verification: the latest VERIFY comment holds the review ledger (all findings fixed, none open) and the goal check (met). Full suite green; guided acceptance passed (deleted a followed worktree, the panel re-pinned and kept showing the map).
Activity
ToRvaLDz commentedon Nov 2, 2026
REPRO
<repo>-t<n>, a sibling worktree or clone), with the session cwd inside it.Expected: the panel re-pins to a still-existing checkout of the same repo and keeps listing tickets; with none found, it says the checkout folder is missing (
checkout not found: <path>).Observed: the panel shows
gh not installed, althoughghis on the PATH.Code path:
map-pane.tsxrunsghwithcwd: pin.root; spawning with a cwd that does not exist throwsENOENT, whichfailureReason(map-pane-view.ts) maps togh not installed.refreshPin(repo-pin.ts) re-reads the repo from the session cwd; in a deleted directory that read fails andnextPinkeeps the stale pin, so every refresh fails the same way.ToRvaLDz commentedon Nov 2, 2026
DIAGNOSIS
Root cause (two co-occurring defects, both confirmed):
gh()inmap-pane.tsxspawnsghwithcwd: pin.root. Node reports a missing cwd asENOENTwith the messagespawn gh ENOENT, byte-identical to a missing binary.failureReason(map-pane-view.ts) mapsENOENTtogh not installed.Falsifiable: if this is the cause,
spawn(echo, {cwd: /nonexistent})andspawn(nonexistent-bin, {cwd: /tmp})yield the samecodeand message. Test run: both printedENOENT spawn <bin> ENOENT. Prediction held.refreshPin(repo-pin.ts) reads the repo from the session cwd. In a deleted cwd the git reads throw and are swallowed;nextPinthen keeps the current pin (or the host returns the memoized repo with the same dead root). The pin never leaves the deleted root, so every refresh fails.Falsifiable: if this is the cause, a fake engine whose cwd git reads deny
ENOENTkeepsrepoPin.rootat the deleted path after a turn. Test run (fake-engine probe): the pin stayed/work/repo-t7. Prediction held.AND-gate: fixing (1) alone gives an accurate message but no recovery; fixing (2) alone hides the message only in this path.
Eliminated: a non-zero
ghexit (goes through the stderr branch, never says "not installed");isServed(returns without spawning for github.com).Fix direction: in
gh(), on anENOENTthrow, check whetherpin.rootstill exists before blaminggh(new reasoncheckout not found: <path>); in the pin refresh, when the pinned root is gone, re-pin to the first existing checkout of the same repo (main working tree, another worktree, a same-remote sibling), using only the engine process API (the mod has no fs).ToRvaLDz commentedon Nov 2, 2026
VERIFY (round 1)
status: clean
critical: 0 · warning: 2 · info: 1
I recomputed the ticket diff with the protocol command. It returned two commits, a69fadc and 70fd28b, both read. I did not run the suite. I traced the two warnings and the info finding by reading the code, and did not execute them.
Findings ledger
Warnings
W1. After a relocation, the full search re-runs on every refresh.
/home/torvalds/Git/trailhead/plugins/trailhead/hooks/mod/repo-pin.ts:242-244session.repo()keeps naming the deleted root. The new test rig models exactly this, and the code comment on line 243 says it happens.livePinmoves the stored pin to the live root.nextPin(stored = live, repo = DEAD)sees a different pin and returns DEAD again.livePin(DEAD)finds the root missing.missedonly caches misses and a hit callsmissed.delete.sameValue(stored, pin)stays true, so no move fires, which hides the repeat work.fs.listof the parent, then for the first matching siblingrev-parse,remote get-urlandworktree list, then moreexistsandrev-parse/remotecalls per worktree.gitspawns on every pane refresh, turn complete and tracker write, for as long as the session stays in the deleted cwd. This is the cost the brief asked me to check.Map<deadRoot, liveRoot>that is re-validated with oneexistscall. Alternatively, inrefreshPin, keepcurrentwhenfound.rootis a known-dead root that already relocated.W2. The tests leave the new contracts unpinned.
/home/torvalds/Git/trailhead/plugins/trailhead/tests/repo-pin-relocate.test.tsexistscall. That was the cost concern for livePin running on every refresh.worktree list.process.runhandler denies spawns whose cwd is dead. It never asserts that every probe ran withcwd: '/'.Info
I1. The
missedcache is module-level and keyed only by the dead root.isStartdoes for the pin.pinKeyplus root.Checked and found fine
gitprobes: all other probes usegit -C <path>withcwd: '/', so none spawn in the dead directory.checkoutOfcompareshost/slug, so SSH and HTTPS remotes of the same repo match.remoteIdalso case-folds the host.existsdefaults to true when the filesystem read throws, so a read error does not move the pin.gh()(map-pane.tsx), a missing gh with an existing cwd still reports "gh not installed".missingRootis set only whenexistsis false.fsorBuffer. Only$.fs.exists,$.fs.listand$.process.runare used.onMove:onMovestill fires only when the stored pin changes, and before the atom update, so that ordering is preserved.Goal-backward verification
status: met
I traced each claim to the two
Refs: #347commits (a69fadc, 70fd28b) and ran the full suite:node scripts/test.jsgives 84 ok/skipped, 0 failed. The commit set from the exact Ticket-diff command is those two commits.Claim 1: re-pin to a live checkout and keep working. class: met
livePinin/home/torvalds/Git/trailhead/plugins/trailhead/hooks/mod/repo-pin.ts, called fromrefreshPinon the pin it is about to store. The pin stays unchanged if its root exists. If the root is gone, it moves to the main working tree, else another worktree, else a same-remote sibling clone.git -C <path>from/, so nothing spawns in the dead directory. This covers the stale-pin defect from the DIAGNOSIS, including the host returning the memoized dead root.map-pane.tsxreads the pin atom on each refresh, so the moved pin is whatghruns in afterwards.ghrefresh against the new root. The relocation tests check the pin atom. Together with the atom read, I count the claim as satisfied.Claim 2: a missing checkout says
checkout not found: <path>, never "gh not installed". class: metgh()checks$.fs.exists(pin.root)after a spawn throws. If the root is gone it passesmissingRoot, andfailureReasonreturnscheckout not found: <path>before theENOENTbranch.Claim 3: a real missing
ghstill says "gh not installed". class: metfs.existsitself throws,missingRootisundefined. That falls through to the existingENOENT→gh not installedmapping.failureReason({ error })withoutmissingRootis stillgh not installed. The existingfailureReasontest for that message is also unchanged.Claim 4: regression tests exist for each. class: met
/home/torvalds/Git/trailhead/plugins/trailhead/tests/repo-pin-relocate.test.ts. It has three tests: re-pin to the main tree, re-pin to another worktree when the main tree is gone, and keep the pin when none is left.map-pane-ui.test.ts. The view test is inmap-pane-view.test.ts.map-pane-view.test.ts.The only weakness is the one noted under claim 1. The new README text matches the behaviour.
Tests
Base current with
origin/main(0 behind). Full suitenode scripts/test.js: 84 ok/skipped, 0 failed (mod tests 670 pass, 0 fail).Next: W1, W2 and I1 are taken in a second fix round before resolving.
ToRvaLDz commentedon Nov 2, 2026
VERIFY (round 2)
status: clean
critical: 0 · warning: 2 · info: 1
I recomputed the ticket diff with the protocol command. It now returns three commits: a69fadc, 70fd28b and dfb9320. I read dfb9320 with
--remerge-diff. I did not run the suite. I checked the paths below by reading the code and the new tests.Findings ledger
No new findings.
Verification of each prior finding
W1: fixed.
/home/torvalds/Git/trailhead/plugins/trailhead/hooks/mod/repo-pin.ts(livePin)livePinnow keeps the hit in arelocatedmap keyed byrelocationKey(pin), the repo plus the dead root.nextPinreturns it again.exists(DEAD)is false, solivePintakes therelocatedhit and runs oneexists(known)check.fs.existscalls and nogitspawn, and the stored pin is unchanged, soonMovedoes not fire.existscheck, with no probe.W2: fixed.
/home/torvalds/Git/trailhead/plugins/trailhead/tests/repo-pin-relocate.test.tsgit -Cprobe and the pin does not move again.cwd === '/'.clock.now, so the window test does not depend on real time.I1: fixed.
refreshPinclears both whenisStartis true.Residual notes (not findings)
clock.nowfails, the miss cache is skipped and the search runs on every refresh. That is the same as the previous behaviour and unlikely.Goal-backward verification
status: met
Round 2. The Ticket diff recomputed with the exact
Refs: #347command is three commits: a69fadc, 70fd28b and dfb9320. I read dfb9320 with--remerge-diffand re-ran the suite:node scripts/test.jsgives 84 ok/skipped, 0 failed.Claim 1: re-pin to a live checkout and keep working. class: met
livePinin/home/torvalds/Git/trailhead/plugins/trailhead/hooks/mod/repo-pin.ts. A live pin returns unchanged after one exists check.relocated. Each later refresh revalidates it with one exists check and drops it if it has also gone. This is needed because the host's memoized repo keeps naming the dead root.ghrefresh against the new root.map-pane.tsxreads that atom on each refresh, so I count the claim as satisfied.missedis now keyed by repo and dead root instead of by dead root alone.git -Cfrom/.Claim 2: a missing checkout says
checkout not found: <path>, never "gh not installed". class: metgh()orfailureReason, so the a69fadc behaviour is intact.Claim 3: a real missing
ghstill says "gh not installed". class: metmissingRootisundefinedand theENOENTmapping applies. The view tests still assert this.Claim 4: regression tests exist for each. class: met
/home/torvalds/Git/trailhead/plugins/trailhead/tests/repo-pin-relocate.test.ts:/;map-pane-ui.test.tsand the view test inmap-pane-view.test.ts.failureReasonassertions.I found no new gaps in round 2.
Tests and acceptance
Base current with
origin/main(0 behind). Full suitenode scripts/test.js: 84 ok/skipped, 0 failed.Acceptance (guided UAT, user-driven): passed.
gh not installed.fix: name a missing checkout folder, not gh, in the map panel
fix: re-pin to a live checkout when the pinned one was deleted
fix: cache the re-pin hit and reset the caches at session start
ToRvaLDz commentedon Nov 2, 2026
Resolution
Cause: two co-occurring defects (see DIAGNOSIS). Spawning
ghin a deleted checkout fails with the sameENOENTas a missing binary, andfailureReasonreported it asgh not installed; the repo pin kept the deleted root because the session cwd reads failed and the host kept naming the dead root.Fix:
gh()inmap-pane.tsxchecks whether the pinned root still exists after a spawn failure;failureReason(map-pane-view.ts) then sayscheckout not found: <path>. A real missingghstill saysgh not installed.repo-pin.tsre-pins a dead root to a live checkout of the same repo (main working tree, another worktree, a same-remote sibling clone), probing withgit -Cfrom/, never from the dead directory; documented in the mod README.Verification: the latest VERIFY comment holds the review ledger (all findings fixed, none open) and the goal check (met). Full suite green; guided acceptance passed (deleted a followed worktree, the panel re-pinned and kept showing the map).