0052 — Rule 16's gap closes through the shfmt walker, not a second regex

The problem

Rule 16 keeps a test / typecheck / build sweep off the shared mini (ADR-0040 layer 1). Codex's round 6 on dotclaude #219 found spellings it ALLOWS, and the matrix measures all six as allowed on origin/main:

Command origin/main would be refused by
`printf npm` test allowed cmdsubst (new)
env -S "`printf npm` test" allowed env -S (#219's existing rule)
bash -c 'env -S "npm test"' allowed env -S (existing)
ssh mini 'env -S "npm test"' allowed env -S (existing)
n\pm test allowed cmdescape (new)
env FOO=a\ b npm test allowed the plain suite rule (existing)

Each one runs the suite on a machine whose whole point is that the suite runs in the cloud. Two PRs exist, and they are different shapes of fix, not two drafts of one:

#234 also states the intent that rule 16 "joins the same walker rather than gaining another regex". The walker cannot close the gap today: #234 is unmerged, so rule-16 shadow has zero lines of log, and rule 15's draft cutover is explicitly allow-only. So the question is which shape closes the gap, and in what order.

Options considered

The three options as the receipt put them, verbatim in substance:

The decision

  1. #245 does not merge. Rule 16's gap closes through the shfmt walker.
  2. #234 merges first, in shadow, as rules 20 and 15 did. It changes no verdict and refuses nothing live; it produces the disagreement log the cutover needs.
  3. The cutover bar is evidence, and elapsed time is not evidence. The ask was framed as "accrue shadow days", and that framing is corrected here: authorization to let the walker add a refusal requires classified shadow disagreements — each regex=allow ast=refuse line read and labelled a real miss or walker noise — plus a deny/allow replay over the case set. A date on a calendar authorizes nothing. (Rule 15 is the measurement behind the correction: #235 rested its draft on nine days and 870 lines of log on 2026-10-12, ~/.claude/jobs/host/rule15-shadow.jsonl reads 1,044 lines as of 2026-10-14T04:1xZ — a count that goes stale by design, which is the point — and the cutover is still unwritten, because days were never the missing input. The missing input is that log's allow → fenced class, 50 lines, still carrying the label "Unclassified: real misses or walker noise.")
  4. This needs its own written cutover decision; rule 15's precedent does not authorise it. ⚠️ This paragraph is this ADR's reading of an UNMERGED DRAFT, not settled rule 15 doctrine — #235 is open and its cutover is explicitly labelled DRAFT, so what follows is an argument about why a draft cannot be borrowed, not a citation of a rule in force. Rule 15's draft cutover is allow-only — "the walker never removes a fence the regex asked for unless (1) holds, and never adds one in this step" — and rule 16's gap needs the walker to ADD a refusal, the opposite direction. The quote's "in this step" scopes the restraint to rule 15's own first step rather than forever — which is exactly why it cannot be stretched sideways onto rule 16: a step that may only widen what passes licenses nothing about narrowing it, in its own rule or in another. New decision shape, so ADR-0004 wants its own decision at that point, and this ADR is not it: this ADR authorizes the sequence, not the cutover.
  5. #245 is preserved as the evidence and the case set, not abandoned. It is the measurement that justifies this decision, and whatever closes rule 16 through the walker inherits its matrix: 94 cases — 42 denies, 40 of which flip (allow on origin/main, deny on the branch) and 52 allow controls, including the one sanctioned local run (npx vitest run <changed file>, ADR-0040 layer 1's single exception) and the six line continuations that 1,306 prior cases never exercised. A walker that cannot pass that matrix has not closed this.

Why a second regex layer was rejected — the construction record

Codex (gpt-5.6-sol, high) ran four rounds on #245: 9 → 16 → 7 → 1 findings, and its standing verdict on the PR as a whole is BLOCK. (The PR's own Review header says "three rounds" and then lists r1–r4; the counts are the measured figures.)

The round shape is the signal, not the count. The author's own words name rounds 2 and 3 as each fix creating the next defect in the same mechanism, and r4's single finding (\. test, a command name that is only a marker byte) is a residue of r3's continuation fix. The repo's shipping doctrine names that shape as the one that needs a design change rather than another patch.

The branch shipped and then fixed its own false refusals during construction, and each is pinned as an allow control so the next widening trips on it:

The honest conclusion, which the PR's own body reaches: a regex over a blanked copy cannot be made into a shell lexer by adding arms. That is the argument for the walker, and it is a better argument for #234 than #245 being absent.

What is out of scope

Refusing every command name the walk cannot read — Codex's "lexical floor" — is not authorized here. It exceeds this row's policy, and it flips three long-standing ALLOW cases in the matrix: `echo git` push, $(echo git) push and `echo git` log — three commands rule 16 has no business judging. ADR-0040 scopes the fence to heavy work.

The floor is real and stays open either way: a substitution can synthesise bytes that appear nowhere in the command text, so $(printf %s n p m) $(printf %s t e s t) is allowed on origin/main and on #245's branch alike, and a lexical relevance bound cannot close it. Whether to pay the three flipped cases to close it is a separate decision for a human, and nothing here pre-empts it.

Consequences

The cost this decision accepts, in the decider's own words: "The six bypasses remain available while shadow evidence accumulates, permitting heavy runs that other enforcement layers may miss. A slow or stalled cutover prolongs shared-mini contention. Shadow deployment must therefore remain an open prerequisite, never count as closing the defect."

Read as rules:

SYSTEMS.md needs no edit, and this was derived rather than assumed. Neither guard-bash.sh nor driver-reach-ast.py is a catalog entry — no id in the catalog is a guard-bash system (the count is deliberately not quoted: this ADR and its own PR body disagreed about it, and the reviewer's grep returned a third figure, so the number is the unreliable part while the claim is checkable by reading the id list); rule 16 appears only inside the pacer entry's notes as a caller of the shared pace.json policy, and guard-kill-rate measures the hook's kills. Both of those are unchanged by this decision. The relation this ADR changes is intra-dotclaude (which layer of one hook emits rule 16's refusal), so no system enters, changes or leaves the catalog. Rule 16's anchor in the ledger is ADR-0040's Systems line, which is why this ADR links it rather than minting a new entry.