fix(rollup): discover git worktrees - .git tested for existence, not directory-ness

The catalog half of a shared gap that was previously PINNED rather than fixed.
This reader of ~/repos tested `.git` with a directory check, so a plain
`git worktree add ~/repos/feature-x` produced a depth-1 sibling that can carry
its own STATE.md and was dropped SILENTLY - no warning, no count,
indistinguishable from a repo with nothing to say. A submodule fails the same
way for the same reason.

It was pinned, not fixed, because diverging would break the very
same-name-from-both-readers invariant discoverRepos exists to hold. ~/repos
answered YES to the gated question and reported their half landed. Verified
here rather than taken on report: board.sh's Discovery block tests `-e`, and
has done since board.sh was introduced (repo-mailbox 61e224c, first released
v0.9.0) - so the released reader has no `-d` era at all, and only this side
was ever the outlier. The two move together from here - change one, change the
other.

THE ACCEPTANCE TRAP, MEASURED RATHER THAN ASSUMED. This changes NO number
against real ~/repos. Old and new builders were run back to back against the
live tree and diffed: stdout and stderr byte-identical, both exit 0. Directly
measured why: 0 directories at depth 1 or 2 currently carry .git as a file.
A number standing still is not ambiguous here, it is the ONLY possible outcome,
and it is why a fixture is not the best way to test this but the only way.

FIXTURE GATED BEFORE BEHAVIOUR. The worktree fixture uses a real `git worktree
add`, not a hand-written `.git` file - faking it would assert against our guess
at git's on-disk format instead of against git. A separate test asserts the
fixture itself: .git exists, is a file, is not a directory. Without it, the day
git stops writing worktree .git as a file the behaviour tests would go green
while measuring nothing, and a green run cannot distinguish that from success.
The gate says which one it was, and says the premise of the -e rule is gone
rather than inviting a test tweak.

Two behaviour tests: a worktree at depth 1, and one under a polyrepo container
at depth 2 - the same rule at both depths board.sh walks.

Suite 90/90 across the six files (rollup 31 -> 34). check-versions 11 OK,
0 WARN, 0 ERROR. Real run unchanged at 8 repos, 17 markers, Output B 0 lines,
1 V6 warning, exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSX2v1m7Fz22BKrGJZuUpQ
This commit is contained in:
Kjell Tore Guttormsen 2026-07-31 15:41:08 +02:00
commit 2efe98ba02
2 changed files with 91 additions and 6 deletions

View file

@ -207,13 +207,20 @@ export function buildRegister({ repos }) {
//
// Not descending into a git repo also makes the known nested-name trap
// (claude-code-100x/claude-code-100x, both git repos) structurally unreachable rather than
// merely absent. SHARED, DELIBERATE GAP: like board.sh, "is a repo" means `.git` is a
// DIRECTORY, so a git worktree or submodule (where `.git` is a file) is not seen by either
// reader. Pinned here rather than fixed one-sidedly — diverging would break the very
// same-name-from-both-readers invariant this function exists to hold.
// merely absent.
//
// "Is a repo" tests `.git` for EXISTENCE, not directory-ness: a worktree or submodule has
// `.git` as a FILE. A plain `git worktree add ~/repos/feature-x` lands a depth-1 sibling
// that can carry its own STATE.md, and a directory test dropped it SILENTLY — no warning,
// no count, indistinguishable from a repo with nothing to say. This was pinned rather than
// fixed one-sidedly because diverging would break the very same-name-from-both-readers
// invariant this function exists to hold. board.sh's side is in place — MEASURED, not
// reported: its Discovery block tests `-e`, and has since board.sh was introduced
// (repo-mailbox 61e224c, first released v0.9.0). This is the other half. The two move
// together or not at all — change one, change the other.
function isGitRepo(dir) {
const dotGit = join(dir, '.git');
return existsSync(dotGit) && statSync(dotGit).isDirectory();
return existsSync(dotGit);
}
function subdirs(dir) {