Found while mirroring the origin/main enumeration guard for objectstack-ai/objectstack#13305 (objectui PR for that card); out of scope for it, filed rather than redesigned there.
The gap
.github/workflows/hook-selftests.yml names itself "The standing caller for .claude/hooks/*.selftest.sh". It is not one. Its steps are a hand-kept enumeration, so a new self-test ships uncalled until someone remembers to edit this file:
- name: guard-main-checkout-bash self-test
run: .claude/hooks/guard-main-checkout-bash.selftest.sh
- name: guard-shared-stash self-test
run: .claude/hooks/guard-shared-stash.selftest.sh
objectstack's twin does not have this failure mode. Its collector in lint.yml discovers:
mapfile -t selftests < <(find .claude/hooks -type f -name '*.selftest.sh' | sort)
if [ "${#selftests[@]}" -eq 0 ]; then
echo "... DISCOVERED NOTHING under .claude/hooks/."
exit 1 # verifying nothing is a failure, not a pass
fi
That shape adds a new matrix with no workflow edit, and it fails loudly if the search ever stops matching — so it cannot silently degrade to a no-op.
Measured
On the card's branch, adding guard-tree-enum.selftest.sh to both repos:
| repo |
runner shape |
new self-test picked up? |
| objectstack |
find .claude/hooks -name '*.selftest.sh' in lint.yml |
yes, automatically — 4 discovered, no workflow edit |
| objectui |
two hardcoded run: steps in hook-selftests.yml |
no — had to be added by hand |
Why this is worth a card rather than a shrug
This workflow's own header argues at length that it is a RUNNER whose matrices "are meant to grow in .claude/** WITHOUT this file being touched", and it removed per-step case counts specifically to keep that independence safe. The enumeration in its steps: is the remaining half of exactly the drift it set out to avoid: the independence holds for cases inside an existing matrix, and breaks for a new matrix file.
It also interacts with #6451 (guard-main-checkout.sh has no self-test at all): whoever writes that missing matrix must remember this file too, or their new self-test is added and never run. Distinct mechanism though — #6451 is a missing self-test FILE, this is the runner that would not call it. Not a duplicate.
Suggested fix
Port objectstack's collector, including its two load-bearing properties: the empty-discovery branch is RED (a step that verified nothing is not a pass), and the loop tolerates-and-collects so one red matrix does not hide the others. scripts/dependabot-merge-gate.mjs's OPTIONAL_CONTEXTS description of this workflow should stay count-free (it was de-counted on the #13305 branch for exactly this reason).
Unassigned and unlabelled — grading and domain:* are triage's.
Generated by Claude Code
Found while mirroring the
origin/mainenumeration guard for objectstack-ai/objectstack#13305 (objectui PR for that card); out of scope for it, filed rather than redesigned there.The gap
.github/workflows/hook-selftests.ymlnames itself "The standing caller for.claude/hooks/*.selftest.sh". It is not one. Its steps are a hand-kept enumeration, so a new self-test ships uncalled until someone remembers to edit this file:objectstack's twin does not have this failure mode. Its collector in
lint.ymldiscovers:That shape adds a new matrix with no workflow edit, and it fails loudly if the search ever stops matching — so it cannot silently degrade to a no-op.
Measured
On the card's branch, adding
guard-tree-enum.selftest.shto both repos:find .claude/hooks -name '*.selftest.sh'inlint.ymlrun:steps inhook-selftests.ymlWhy this is worth a card rather than a shrug
This workflow's own header argues at length that it is a RUNNER whose matrices "are meant to grow in
.claude/**WITHOUT this file being touched", and it removed per-step case counts specifically to keep that independence safe. The enumeration in itssteps:is the remaining half of exactly the drift it set out to avoid: the independence holds for cases inside an existing matrix, and breaks for a new matrix file.It also interacts with #6451 (
guard-main-checkout.shhas no self-test at all): whoever writes that missing matrix must remember this file too, or their new self-test is added and never run. Distinct mechanism though — #6451 is a missing self-test FILE, this is the runner that would not call it. Not a duplicate.Suggested fix
Port objectstack's collector, including its two load-bearing properties: the empty-discovery branch is RED (a step that verified nothing is not a pass), and the loop tolerates-and-collects so one red matrix does not hide the others.
scripts/dependabot-merge-gate.mjs'sOPTIONAL_CONTEXTSdescription of this workflow should stay count-free (it was de-counted on the #13305 branch for exactly this reason).Unassigned and unlabelled — grading and
domain:*are triage's.Generated by Claude Code