← board

The no-full-suite hook refuses a dry run, and lets half the long tiers through

The hook is a good idea and it works. Verified against the whole Track T command set: --tier quick, --pin, gate.sh quick, make compiler/pascal26, make lib-test and make demos all pass; make test*, gate.sh full|limited and --tier full|limited are refused; both escape hatches work, including the inline PXX_TRACK=T <cmd> form. The watcher daemon is untouched, as it must be — it is a systemd unit, so no Claude Code hook ever sees it (confirmed: it published native, opt and slow tiers after the hook landed). Two defects only.

1. make -n is refused, and it is a DRY RUN

DENY   make -n test-nilpy
DENY   make -n --no-print-directory test

make -n prints the recipe and executes nothing. Measured cost on this repo: 0.08 s. It is not a regression suite by any definition the hook's own comment offers ("the same ten minutes wearing a different hat" — there are no minutes here).

It is also load-bearing:

It is also bypassable by accident, which makes the rule leaky

DENY   make -n --no-print-directory test
ALLOW  make -n --no-print-directory PXX_TMP=/tmp/PINNED test-nilpy

Same command, same effect, different verdict. The regex is make[[:space:]]+(-[^[:space:]]+[[:space:]]+)*(test|check)… — it allows a run of -flag tokens and then requires the target, so any VAR=value between them breaks the match. A rule that a stray make variable defeats will be defeated by accident before it is defeated on purpose.

Fix: exempt dry runs explicitly — -n, --dry-run, --just-print, --recon — and let the VAR=value form fall through the same exemption rather than through a gap.

2. It blocks two long tiers and allows three others

DENY   testmgr.py --tier full        DENY   testmgr.py --tier limited
ALLOW  testmgr.py --tier native      ALLOW  testmgr.py --tier slow
ALLOW  testmgr.py --tier opt

The pattern is --tier[[:space:]]+(full|limited). But native is what the watcher runs continuously, slow exists precisely because it holds test-uforth#blocktest (594 s in one job), and opt is the optdiff sweep. Any of the three costs a non-T lane the ten minutes the hook was written to prevent.

This is convenient for Track T and wrong for everyone else. Either the tier list should be "everything except quick", or the rule should key on something other than an enumeration that has to be maintained as tiers are added — the slow tier was created today, which is exactly how such a list goes stale.

Not a defect, but worth knowing

Track T's exemption is opt-in and no session sets it. PXX_TRACK is unset in a normal Track T agent session, so T meets a refusal on its own documented gate (--tier full) and learns the hatch from the refusal text. That is self-documenting and arguably correct — an explicit opt-in beats a lane auto-detected from something forgeable. Flagged only so it is a decision rather than a surprise; a scheduled/autonomous T worker must export PXX_TRACK=T, because it has no human to read the refusal and could silently skip its gate instead.

Gate

.claude/hooks/no-full-suite.sh is not on any suite's path, so the gate is the table above re-run: feed representative commands to the hook on stdin as {"tool_input":{"command":"…"}} and check the verdicts. Include make -n in both the bare and VAR=value forms, and every tier name testmgr.py --list-tiers knows about.

Resolution (2026-08-15)

Both defects fixed, plus one more of the same shape found while testing.

1. Dry runs are exempt — by REMOVING them from the scanned text, not by exiting early. -n, --dry-run, --just-print, --recon, and the clustered short form (-kn, -j8 -n). The obvious implementation — exit 0 the moment a dry run appears anywhere — would have waved through make -n test && make test-nilpy, which is one dry run and one suite. So the command is split on the shell separators and the dry-run segments are dropped; the rules then scan what is left. Splitting is coarse (a | inside a quoted string over-splits) but that can only ever expose MORE text to the rules, never less.

The bug in the first cut of this, since it is a good trap: the surviving segments were rejoined with ;. Every rule ends its match with ([[:space:]]|$), so make test; matched nothing and the hook allowed the exact command it exists to refuse — caught only because the verdict table was re-run rather than eyeballed. grep is line-oriented; the segments stay on their own lines.

2. VAR=value no longer breaks the target match. The pre-target token run now accepts -flag and NAME=value alike, in both the test|check rule and the stabilize rule (which had the identical hole and nobody had tried it).

3. Tiers are "everything except quick". The enumeration full|limited had allowed native, slow and opt — and slow exists precisely to hold test-uforth#blocktest at 594 s in one job. Keying on the exception rather than on a list means a tier added tomorrow is refused by default, which is the right default for a rule about time.

4. Found while testing: the read-only-first-word escape did not survive a CHAIN. echo 'make -n' && make test was allowed because the first word was echo. It now requires a single command — with git deliberately keeping its exemption through a chain, because a commit MESSAGE routinely contains both a semicolon and a quoted command, and refusing that is the noisy false positive that block was written to stop. A pipe stays fine either way.

Gate

The ticket's own table, re-run against the hook on stdin (37 commands, verdicts in the resolution commit). Every row is as specified: all four dry-run spellings and the VAR=value form allowed; the two chained forms refused; make test, check, stabilize (bare and with a variable), gate.sh full|limited, all five non-quick tiers, bare --pin and the test-glob loop refused; lib-test, demos, compiler/pascal26, pin, stabilize-fast, gate.sh quick, --tier quick, --pin --tier quick, the grep/commit mentions and a single named test allowed.

Left as the ticket describes it

Track T's exemption stays OPT-IN. The ticket argues that is correct — an explicit opt-in beats a lane auto-detected from something forgeable — and the refusal text names the hatch, so a supervised T session learns it on contact. A scheduled T worker must export PXX_TRACK=T; that belongs in the worker's own setup, not in the hook.

Log