Uh oh!
There was an error while loading. Please reload this page.
ci(lint): run the .claude/hooks self-test matrices in the required job - #11799
Conversation
`.claude/hooks/` holds the enforcement behind worktree-first and the stash ban, and each guard ships a hermetic self-test matrix whose header says to re-run it after touching the hook. Nothing ran them — a grep for `selftest` over `.github/workflows/` matched nothing — so a guard that had silently stopped guarding landed green. Adds one step to the `lint` job (`Lint & Repo Gates`, a required status context) that DISCOVERS every `.claude/hooks/**/*.selftest.sh` at run time and runs it. No hook is modified: `.claude/**` is governed surface and this workflow is not. - discovered, never listed, so a matrix added tomorrow is picked up with no edit here - an empty discovery is red, not green (#4690) - tolerate-and-collect (#10814): a bare loop under `bash -e` would abort at the first red matrix and leave the rest unrun, neither green nor red - a step of the required job rather than a job of its own, so a red guard stops the merge queue instead of publishing an advisory context (#5617) Baseline measured on main before wiring: guard-main-checkout-bash 121/0, guard-shared-stash 32/0. Non-vacuity: with the bash guard's sole BLOCK verdict mutated to `exit 0` (scratch, restored byte-identical), the step exits 1, names the failing matrix, and the second matrix still runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
os-steve
commented
Aug 24, 2026
ACCEPT. Verified by content on Both guards are green — and that is a measurement, not a relief
The zeros are falsifiable because of the mutation pair, and the ambient-independence check settles my Zone 2 worry properly: both re-run with cwd = primary checkout, detached HEAD, and a non-repo dir — ⭐ Your measured set correction, verifiedTwo, not three. #11800 is the right filing and it matters more than it looks. Three design points I did not specify, each on evidence⭐ (a) A step of the already-required job, not a new job."a new job publishes a context in no ruleset, so a red guard would be advisory and the merge queue would not stop for it (#5617)". My dispatch said "lint.yml family" and stopped there. A new job would have been green-looking, correctly discovered, correctly failing — and unable to block anything. That is the same defect class as the card itself, one layer up: a guard that runs and cannot enforce. (b) Tolerate-and-collect, because a (c) Empty discovery is RED (#4690). I ⛔'d hard-coded lists; you went further and closed the mirror-image hole — a renamed or moved directory can no longer make the step pass by running nothing. Non-vacuityRed/green driven through the real extracted block as Discovery guarantee proven with a scratch fixture: #11801 is you reporting a gap in your own work
Finding that your own new block sits outside the gate that would police it, and filing it rather than leaving the count unexplained, is the report I most want to see. Graded next round. The declared narrowing on Flipping to ready. Arming once every check is green. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11514
.claude/hooks/holds the enforcement behind the two rules whose violation is most expensive in this repo — worktree-first and the stash ban — and each guard ships a hermetic self-test matrix whose header tells you to re-run it after touching the hook. Nothing ran them:grep -rn 'selftest' .github/workflows/matched nothing. So those headers were the enforcement, and a guard that had silently stopped guarding landed green — the worse half of a broken guard, because everyone keeps behaving as though it works.One step is added to the
lintjob. No hook is modified —.claude/**is governed surface, this workflow is not.Baseline on
main— measured before wiring anythingThe first time anything measured these. Both healthy, so this wires an alarm rather than fixing an outage:
.claude/hooks/guard-main-checkout-bash.selftest.sh121 passed, 0 failed.claude/hooks/guard-shared-stash.selftest.sh32 passed, 0 failedMeasured set, rather than the card's description:
.claude/hooks/holds two self-tests, not three.guard-main-checkout.sh— theEdit/Writehalf of the pair — ships no self-test at all; only the Bash half does. That gap is reported, not closed here (authoring one is a.claude/**change).Non-vacuity — the step goes RED when a guard breaks
The deliverable, both directions, driving the real block extracted from this file under a real
bash -e, the way Actions runs it.guard-main-checkout-bash.sh's sole BLOCK verdict was mutatedexit 2→exit 0— the guard stops guarding — as a scratch mutation undertrap … EXIT INT TERM, never committed, proven on disk by anchor counts in both directions (exit 21→0,exit 00→1, sha moved) and restored byte-identical (sha5db89551c9090bdbbefore and after;git diffon.claude/empty).discovered 2· bothPASS·all 2 ran and passedFAIL .claude/hooks/guard-main-checkout-bash.selftest.sh(70 passed, 51 failed, every onewant=block got=allow)The mutated guard is the first discovered, and the second still ran and passed (
32 passed, 0 failed) in that same red run — so the collector really does defeat thebash -emasking, rather than merely being written to.Discovery is the contract, not a list
A hard-coded list would be this card's own defect one level up: add a hook with a matrix tomorrow, it silently is not run, nothing goes red. Proven with a scratch fixture (uncommitted, removed;
git statuson.claude/clean afterwards):discovered 3— picked up with no edit hereFAIL … zz-scratch-fixture.selftest.sh— it is really executed, not just listedDISCOVERED NOTHING … verified nothing, which is a failure and not a pass (#4690)That last row is the direction a glob normally fails in: a renamed or moved directory would otherwise make the step pass by running nothing — the same green line this whole family exists to distrust.
Three placement decisions, each on evidence
A step of the
lintjob, not a job of its own.Lint & Repo Gatesis a required status context (registered incheck:required-contexts). A new job publishes a context in no ruleset, so a red guard would be advisory and the merge queue would not stop for it — #5617 verbatim, the exact shape this card exists to close.Tolerate-and-collect, not a bare loop (#10814). A⚠️
run:block isbash -e, so a bare loop aborts at the first red matrix and leaves the rest unrun.check:step-collectorscannot see this block — its population isscripts/|packages/paths carrying--self-test, and these are.claude/hooks/*.selftest.sh. Confirmed empirically: with this step in the tree that gate still reports2 step(s) run 2+ independent self-tests. The collector shape here is held by review, so the step carries a ⛔ note against "simplifying" it.Dependencies verified on the runner's terms, not assumed. Needs
jqandgitand nothing else — no pnpm, no node, no build, no network.jqis used bare onubuntu-latestbycut-rc.ymlandrelease.ymlalready. Hermeticity was measured rather than read off the headers: each matrix builds its own git repo and linked worktree under$TMPDIR, and both were re-run from a primary checkout, a detached HEAD, and a non-repo cwd —121/0and32/0in all three. So neither this checkout's depth nor its branch is an input. ~2 s.Gates
Union re-run after the final commit, at
a05a5f961. The 13 families derived bynode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(which reads the change set itself), pluscheck:nul-bytes: all green —check-step-collectors✓ 329 run: steps across 26 workflow(s),check-workflow-status-functionsOK (scanned 26 workflow file(s), 49 job(s)),check-nul-bytesOK (scanned 6573 text file(s)),check:required-contexts,check:aggregator-roster,check:shard-attestation,check:type-check-coverage,check:agent-test-spelling,check:node-version,check:pnpm-filter-targets,ci-failure --self-testall exit 0.One declared narrowing.
check:type-check-debt(--self-test && --re-measure) did not complete locally: its--re-measurehalf refuses on an unbuilt dependency closure by design (#6376, "measuring now would not fail, it would silently measure a DIFFERENT WORLD"), which is a property of a fresh worktree, not of this diff. Its--self-testhalf passed (182 cases), and the half that readslint.yml—observed()— is shared withcheck:type-check-coverage, which passed with this edit in place, so every assertion this gate makes aboutlint.ymlholds. This diff changes 1 file and 0 TypeScript files, so no ledger number is reachable from it; CI builds the closure before that step and measures there.No changeset: this PR edits one CI workflow and releases nothing — the textbook
skip-changesetcase by this file's own prescription.Sequencing: #11779 also edits
.github/workflows/lint.yml, at line ~1654 (afterNode-version drift guard). This edit is at line ~787. Verified by content, not assumed — #11779 had not landed onmainat branch time.Generated by Claude Code