Found while wiring #11514 (PR #11799). Filing rather than widening the gate in that PR: its declared surface is .github/workflows/lint.yml alone, and this is a scripts/ change.
Measured
check-step-collectors.mjs decides what to judge from two regexes:
/** A repo script path carrying `--self-test` somewhere after it on the same command. */constSELF_TEST_TARGET=/(?:^|\s)((?:\.\/)?(?:scripts|packages)\/[\w./-]+\.(?:mjs|mts|cjs|js|sh|ts))(?=\s)[^\n]*?\s--self-test\b/;
A step is only considered once selfTestTargets(step.run).length >= 2, so its population is scripts/|packages/ paths carrying a --self-test flag. The matrices in .claude/hooks/ are neither: they live under .claude/, and they are invoked as bare executables (.claude/hooks/guard-shared-stash.selftest.sh), with no flag.
Confirmed on the PR #11799 tree, which adds a real third collector to lint.yml:
✓ check-step-collectors: 329 `run:` steps across 26 workflow(s); 2 step(s) run 2+ independent self-tests, all of them through a collector.
Still 2, with three collectors in the file.
Why this is the gate's own defect class
#10814 exists because a run: block is bash -e, so a bare sequence of independent self-tests aborts at the first failure and leaves the rest unrun — neither green nor red. The new step runs two independent hermetic matrices and is written as a tolerate-and-collect block for exactly that reason; the measurement is in PR #11799 (with the first matrix mutated red, the second still runs). But nothing enforces it. Someone "simplifying" that block into
fortin"${selftests[@]}";do"$t";donerestores the #10814 defect verbatim, and every gate in the repo stays green. The step therefore carries a ⛔ comment asking reviewers not to do that — prose enforcement, which is the state #10814 was filed to leave behind.
That the gate's own header says its sweep found "exactly two" matches is not a defence: it was true when written and it is what makes the blind spot invisible now — a count that stays right while its subject grows.
Options
- Widen
SELF_TEST_TARGET's path alternation to include .claude/ and make the --self-test flag optional for a path matching *.selftest.sh (self-testing is in the filename there). Note driveBlock's SCRIPT_TOKEN stubbing needs the same widening, or a .claude/ collector reaches it and throws no repo script path in command. - Key the population on "≥2 independent self-tests" in a spelling-independent way — a path ending
.selftest.sh, or a --self-test flag, either counting. - Accept the blind spot and record it. Not recommended: this is the gate whose whole argument is that a static shape cannot vouch for itself.
Option 1 or 2 should keep the gate's --self-test harness driving the real block, since a collector that swallows its exit code is worse than the masking it replaces.
Note on --self-test reach
The .claude/hooks/ matrices are not the only family outside this gate's population — scripts/bump-objectui.selftest.sh uses the same *.selftest.sh naming and is likewise unmatched by the --self-test flag requirement. Whether that one is run anywhere is a separate question and is not asserted here.
Found while wiring #11514 (PR #11799). Filing rather than widening the gate in that PR: its declared surface is
.github/workflows/lint.ymlalone, and this is ascripts/change.Measured
check-step-collectors.mjsdecides what to judge from two regexes:A step is only considered once
selfTestTargets(step.run).length >= 2, so its population isscripts/|packages/paths carrying a--self-testflag. The matrices in.claude/hooks/are neither: they live under.claude/, and they are invoked as bare executables (.claude/hooks/guard-shared-stash.selftest.sh), with no flag.Confirmed on the PR #11799 tree, which adds a real third collector to
lint.yml:Still
2, with three collectors in the file.Why this is the gate's own defect class
#10814 exists because a
run:block isbash -e, so a bare sequence of independent self-tests aborts at the first failure and leaves the rest unrun — neither green nor red. The new step runs two independent hermetic matrices and is written as a tolerate-and-collect block for exactly that reason; the measurement is in PR #11799 (with the first matrix mutated red, the second still runs). But nothing enforces it. Someone "simplifying" that block intorestores the #10814 defect verbatim, and every gate in the repo stays green. The step therefore carries a ⛔ comment asking reviewers not to do that — prose enforcement, which is the state #10814 was filed to leave behind.
That the gate's own header says its sweep found "exactly two" matches is not a defence: it was true when written and it is what makes the blind spot invisible now — a count that stays right while its subject grows.
Options
SELF_TEST_TARGET's path alternation to include.claude/and make the--self-testflag optional for a path matching*.selftest.sh(self-testing is in the filename there). NotedriveBlock'sSCRIPT_TOKENstubbing needs the same widening, or a.claude/collector reaches it and throwsno repo script path in command..selftest.sh, or a--self-testflag, either counting.Option 1 or 2 should keep the gate's
--self-testharness driving the real block, since a collector that swallows its exit code is worse than the masking it replaces.Note on
--self-testreachThe
.claude/hooks/matrices are not the only family outside this gate's population —scripts/bump-objectui.selftest.shuses the same*.selftest.shnaming and is likewise unmatched by the--self-testflag requirement. Whether that one is run anywhere is a separate question and is not asserted here.