Repository navigation
ci(hook-selftests): discover the self-test matrices instead of enumerating them - #7019
Merged
Merged
Conversation
…ating them
`.github/workflows/hook-selftests.yml` calls itself "the standing caller for
`.claude/hooks/*.selftest.sh`" and was not one: its `steps:` were a hand-kept
enumeration, so a new matrix shipped uncalled until someone remembered to edit
this file. Measured, not argued — `guard-tree-enum.selftest.sh` landed in both
repos on the same card, objectstack's collector picked it up with no workflow
edit, and here it had to be added by hand.
Ports objectstack's collector (its `lint.yml`, "Claude hook guard self-tests")
into a single step that runs `find .claude/hooks -type f -name '*.selftest.sh'
| sort`, keeping both load-bearing properties:
- an EMPTY discovery is RED, so a renamed or moved directory cannot degrade
the gate into a step that verified nothing and reported green;
- the loop TOLERATES AND COLLECTS rather than sequencing, so one red matrix
cannot abort the run under `bash -e` and hide the others; the step still
fails, naming every matrix that failed.
Coverage is unchanged and measured: discovery returns exactly the three
self-tests the removed steps ran (set difference "hardcoded minus discovered"
is empty). Does not touch `.claude/**` — governed surface, and this workflow is
the runner, not the matrices.
Paired write: `content/docs/guide/ci-cd-pipeline.md`, whose section for this
workflow hand-listed two of the three matrices.
`scripts/dependabot-merge-gate.mjs` was read and left unchanged — its
`OPTIONAL_CONTEXTS` description of this check is already count-free.
yinlianghui
pushed a commit
that referenced
this pull request
Sep 2, 2026
`guard-main-checkout.sh` was the only PreToolUse guard in this repo with no self-test — and it is the one both CLAUDE.md and AGENTS.md §9 name by path as the mechanism that enforces worktree-first. Its Bash-side follow-on and guard-shared-stash both ship hermetic matrices that `hook-selftests.yml` runs on every PR touching `.claude/hooks/**`; nothing exercised the primary guard at all, in either direction (a fail-open regression stops guarding the shared checkout in silence; a fail-closed one reads to an agent as a broken environment). Ported from objectstack's copy of the same matrix. The port is verbatim: the two repos' hooks differ by 9 diff lines, all inside one comment block, no executable line differs, and both settings.json route the same Edit|Write|NotebookEdit matcher — the sibling file was run here byte-for-byte unmodified first and returned 87 passed, 0 failed. The only edits are the two KNOWN HOLE sections, re-pointed at this repo's own cards for the same defects. 87 cases, 0 failed. No wiring edit is owed: `hook-selftests.yml` discovers matrices with `find` since #7019, and its header states that its recorded case counts are a dated measurement that must not be refreshed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
akarma-synetal
pushed a commit
to akarma-synetal/objectui
that referenced
this pull request
Sep 9, 2026
….sh (objectstack-ai#7264) `guard-main-checkout.sh` was the only PreToolUse guard in this repo with no self-test — and it is the one both CLAUDE.md and AGENTS.md §9 name by path as the mechanism that enforces worktree-first. Its Bash-side follow-on and guard-shared-stash both ship hermetic matrices that `hook-selftests.yml` runs on every PR touching `.claude/hooks/**`; nothing exercised the primary guard at all, in either direction (a fail-open regression stops guarding the shared checkout in silence; a fail-closed one reads to an agent as a broken environment). Ported from objectstack's copy of the same matrix. The port is verbatim: the two repos' hooks differ by 9 diff lines, all inside one comment block, no executable line differs, and both settings.json route the same Edit|Write|NotebookEdit matcher — the sibling file was run here byte-for-byte unmodified first and returned 87 passed, 0 failed. The only edits are the two KNOWN HOLE sections, re-pointed at this repo's own cards for the same defects. 87 cases, 0 failed. No wiring edit is owed: `hook-selftests.yml` discovers matrices with `find` since objectstack-ai#7019, and its header states that its recorded case counts are a dated measurement that must not be refreshed. Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b Co-authored-by: Claude <noreply@anthropic.com>
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.
Fixes #6906
What changed
.github/workflows/hook-selftests.ymlnames itself "The standing caller for.claude/hooks/*.selftest.sh" and was not one: itssteps:were a hand-kept enumeration, so a new matrix shipped uncalled until someone remembered to edit this file.guard-tree-enum.selftest.shis the receipt — it landed in both repos on the same card, objectstack's collector picked it up with no workflow edit, and here it had to be added by hand (the removed step even carries a comment saying so).The three
run:steps are replaced by one step that discovers its matrices:Ported from objectstack's collector in its
lint.yml(step "Claude hook guard self-tests"), keeping both load-bearing properties..github/workflows/hook-selftests.ymlcontent/docs/guide/ci-cd-pipeline.md.changeset/6906-hook-selftests-collector.mdsrc/changed).claude/**is not touched — governed surface, and this job is the runner, not the matrices. Verified mechanically against the base sha recorded at branch creation (b458300ca), not against a movingorigin/main:The two load-bearing properties, exercised rather than read
Both were exercised against the step's real
run:block, extracted from the YAML by a parser (not retyped) and executed as GitHub executes it, underbash -e. Every negative case ran in a scratch directory; nothing under the repo's real.claude/was deleted, moved or disabled.1. An empty discovery is RED
A step that verified nothing is not a pass: a renamed or moved directory cannot degrade this gate into a silent green no-op.
2. Tolerate-and-collect
Scratch
.claude/hooks/with three stubs —aaa-deliberately-red(exit 1),mmm-deliberately-red(exit 3),zzz-green(exit 0, prints a marker):The later matrix still ran, both failures are named, and the step is still red.
Counter-proof that the shape matters. The same three stubs under a naive
for f in ...; do "$f"; done:bash -eaborts at the first red matrix and the rest are neither green nor red — which is exactly what this collector must not do. Please do not "simplify" the loop.find, not a flat glob. A stub dropped at.claude/hooks/nested/deep.selftest.shin the same scratch tree is discovered;.claude/hooks/*.selftest.shwould miss it.Coverage is not reduced — measured, not asserted
The hardcoded
run:commands were parsed out of the workflow as it exists on the base sha, and set-differenced against what discovery returns on this tree:Every self-test CI executes today is still executed. No self-test file was moved, renamed, skipped or quarantined.
Every discovered self-test, run locally
Exit codes captured individually, before any pipe:
.claude/hooks/guard-main-checkout-bash.selftest.sh0121 passed, 0 failed.claude/hooks/guard-shared-stash.selftest.sh041 passed, 0 failed.claude/hooks/guard-tree-enum.selftest.sh036 passed, 0 failedAnd through the new collector, from the repo root:
exit 0,discovered 3 hook self-test(s),Hook self-tests - all 3 ran and passed. None was failing onmain, so there is no finding to report there.scripts/dependabot-merge-gate.mjs— read, unchangedIts
OPTIONAL_CONTEXTSentry for Hook Self-Tests is already count-free ("the PreToolUse guard self-test matrices must pass"), and itspathssentence is still accurate after this change. Nothing was edited there. The workflow header now records why it must stay count-free, next to the existing argument for count-free step names.Checks run (at
91eb09282, the head of this branch)bash -non the extractedrun:blocktimeout-minutesunchanged; syntax OKpnpm exec vitest run scripts/__tests__/ci-cd-pipeline-doc,merge-queue-reporting,dependabot-merge-gate,lint-workflow, the fourcheck-changeset-*suites)pnpm check:control-bytespnpm check:doc-fences/check:doc-types/check:doc-linkspnpm check:shell-escape-residuepnpm check:skills-pathspnpm check:action-forward-paritypnpm check:governed-queue-guardpnpm check:entry-guardnode scripts/check-changeset-presence.mjsNarrowings, declared
pnpm check:doc-snippetswas NOT run to a verdict. It exits 2,PRECONDITION NOT MET- it needs every package built, and its own output says that is "I could not run", not a failure. It reads TypeScript blocks in docs; this diff adds and removes zero fenced blocks (git diff b458300ca...HEAD -- content/docs/guide/ci-cd-pipeline.md | grep -c '^[+-].*\``'is0`), so there is nothing here it could judge. CI runs it either way.pnpm lintwas narrowed away, and the narrowing is measured, not assumed. Population read fromeslint.config.jsitself: everyfiles:selector in it is.ts/.tsx. This diff's.ts/.tsxcount is0(3 changed files: one.yml, two.md), and it touches noeslint.config.*and notsconfig*, so no untouched file's lint verdict can move either. CI runs the farm regardless.Draft on purpose - the PM lands this. #6451 remains open and is a different mechanism (a missing self-test FILE); this PR is the runner that will call it once it exists.
Generated by Claude Code