Separate the two enforcement regimes: CI bears trust, authoring-time does not
Starting with the incident I caused
Commit 5581b65f ("Stage-3 REQ-1: the @bv clause tag, parse-gated") — mine — removed three hook entries from .claude/settings.json: the PostToolUse/Read recorder and the PreToolUse/Write|Edit pair invoking spec-discipline.py and anti-pattern-gate.py. The commit message is entirely about the @bv clause tag and never mentions the settings change. The crosslink-generic entries survived; the project-specific ones vanished — the signature of crosslink init regenerating a tracked-but-machine-authored file.
I did not do that deliberately, and I did not notice. Both agent-facing gates were then dormant for the entire Stage-3 arc, while README.md:172 as it stood at that commit ("PreToolUse/PostToolUse events, so they enforce automatically — no setup" — the passage has since been removed, so that line number points elsewhere today), goal.md's R-XLATE-1/2/3, and all four .claude/agents/acto-*.md continued to assert they fired.
#93 caught it, restored the entries byte-identical, and added tooling/control-plane-check.py so the class can't recur silently. That was good work and this issue is not a complaint about it.
But #93 fixed the instance. The instance had a cause, and the cause is still here.
The cause
Two different enforcement regimes are being treated as one.
|
CI enforcement |
authoring-time enforcement |
| runs on |
the merge candidate |
one contributor's editing loop |
| posture |
adversarial |
formative |
| harness |
must be agnostic |
inherently harness-specific |
| blocks merge |
yes |
no |
| bears trust |
yes |
never |
spec-discipline.py (read goal.md + the design doc + a reference before editing a routed .rs) and anti-pattern-gate.py (regex-ban todo!, unimplemented!, unreachable!, .unwrap(, .expect(, panic!, Arc<Mutex<, Rc<RefCell<, #![allow() are authoring-time mechanisms. They shape how work proceeds inside one editing loop.
They were then documented as though they were gates — "they enforce automatically — no setup." That produced two failures, both realized:
- The claim was silently falsifiable, and it silently falsified. An entire stage ran with the asserted enforcement absent. Nothing detected it because the assertion lived in prose and the mechanism lived in a file a tool regenerates.
- A contributor on a different harness gets neither regime. No hooks, because wrong editor. And no CI equivalent, because those rules were never expressed as CI. So the rules are simultaneously mandatory in the docs and unreachable in practice for anyone outside one setup.
The rule
Anything load-bearing for a trust claim must be CI-enforced and
harness-agnostic. Authoring-time tooling may exist, may be opinionated, may be
the project default — but it may never be cited as the reason a property
holds.
To be explicit about what this does not say: the repository is entirely free to be opinionated about which authoring-time tools it supports and ships defaults for. That is a separate question from what determines trust, and keeping the two separate is what makes contribution possible for someone running a different setup, or none.
This is not a proposal to remove the ACToR apparatus. It is a proposal to stop letting it carry weight it cannot hold.
Concretely
1. Route the checkable parts to CI. The anti-pattern rules are lint rules. .design/tooling/control-plane.md OQ-2 already records this — "no clippy.toml backstopping the anti-pattern rules." A clippy.toml plus the existing -D warnings in CI enforces them for every contributor, with real semantics instead of regex-on-replacement-text, and with no harness dependency.
2. Demote the rest to opt-in. spec-discipline.py is inherently interactive ceremony — it cannot be a CI check, because "was this file read first" is not a property of a merge candidate. Keep it, default it on if you like, but it stops being a stated precondition for contributing.
3. Fix the file that started it. .claude/settings.json is tracked and machine-authored, which is the actual defect — crosslink init can rewrite it and git will happily record the result as an ordinary edit. Options, roughly in order of preference: generate it from a declared source and untrack the generated artifact; or keep it tracked and have control-plane-check.py verify it against that declared source rather than against prose.
4. Hooks shell out to targets. Where an authoring-time hook and a CI check want the same rule, the hook should invoke the CI target rather than reimplementing it. One definition, two entry points.
A worked example
I've been applying this in a downstream repository, and it's cheap: bulla-systems/bulla — CONTRIBUTING.md carries the regime table and the rule, and every trust-bearing check is a make target that CI runs:
make check-fork every forked file matches the upstream pin, or is a
listed divergence with a reason
make image the image builds from this tree through forge at the pin
make verify the receipt's bindings re-check against current source
make determinism two independent builds are byte-identical
make boot-matrix 1/2/4/8 CPUs, AP-start-failure, reboot, under QEMU/OVMF
make check all of the above
A contributor with no agent tooling satisfies every requirement by running those. The process tooling that repo uses (day/kan) is explicitly classified as authoring-time and non-trust-bearing — someone who doesn't use it can still land changes; they just won't have the process log.
That's the whole shape. It cost one table and one section, and it makes the enforcement claim true by construction rather than by assertion.
Why now
Thermite's thesis is degrade loudly, never launder. The harness had an "asserted enforcement that isn't" failure — the exact shape the harness exists to prevent — and it went undetected for a stage because the claim and the mechanism were in different places with nothing tying them together.
control-plane-check.py now ties them for the hooks that exist. This issue is about making sure the ones that matter aren't hooks at all.
Separate the two enforcement regimes: CI bears trust, authoring-time does not
Starting with the incident I caused
Commit
5581b65f("Stage-3 REQ-1: the @bv clause tag, parse-gated") — mine — removed three hook entries from.claude/settings.json: thePostToolUse/Readrecorder and thePreToolUse/Write|Editpair invokingspec-discipline.pyandanti-pattern-gate.py. The commit message is entirely about the@bvclause tag and never mentions the settings change. The crosslink-generic entries survived; the project-specific ones vanished — the signature ofcrosslink initregenerating a tracked-but-machine-authored file.I did not do that deliberately, and I did not notice. Both agent-facing gates were then dormant for the entire Stage-3 arc, while
README.md:172as it stood at that commit ("PreToolUse/PostToolUseevents, so they enforce automatically — no setup" — the passage has since been removed, so that line number points elsewhere today),goal.md's R-XLATE-1/2/3, and all four.claude/agents/acto-*.mdcontinued to assert they fired.#93 caught it, restored the entries byte-identical, and added
tooling/control-plane-check.pyso the class can't recur silently. That was good work and this issue is not a complaint about it.But #93 fixed the instance. The instance had a cause, and the cause is still here.
The cause
Two different enforcement regimes are being treated as one.
spec-discipline.py(readgoal.md+ the design doc + a reference before editing a routed.rs) andanti-pattern-gate.py(regex-bantodo!,unimplemented!,unreachable!,.unwrap(,.expect(,panic!,Arc<Mutex<,Rc<RefCell<,#![allow() are authoring-time mechanisms. They shape how work proceeds inside one editing loop.They were then documented as though they were gates — "they enforce automatically — no setup." That produced two failures, both realized:
The rule
To be explicit about what this does not say: the repository is entirely free to be opinionated about which authoring-time tools it supports and ships defaults for. That is a separate question from what determines trust, and keeping the two separate is what makes contribution possible for someone running a different setup, or none.
This is not a proposal to remove the ACToR apparatus. It is a proposal to stop letting it carry weight it cannot hold.
Concretely
1. Route the checkable parts to CI. The anti-pattern rules are lint rules.
.design/tooling/control-plane.mdOQ-2 already records this — "noclippy.tomlbackstopping the anti-pattern rules." Aclippy.tomlplus the existing-D warningsin CI enforces them for every contributor, with real semantics instead of regex-on-replacement-text, and with no harness dependency.2. Demote the rest to opt-in.
spec-discipline.pyis inherently interactive ceremony — it cannot be a CI check, because "was this file read first" is not a property of a merge candidate. Keep it, default it on if you like, but it stops being a stated precondition for contributing.3. Fix the file that started it.
.claude/settings.jsonis tracked and machine-authored, which is the actual defect —crosslink initcan rewrite it and git will happily record the result as an ordinary edit. Options, roughly in order of preference: generate it from a declared source and untrack the generated artifact; or keep it tracked and havecontrol-plane-check.pyverify it against that declared source rather than against prose.4. Hooks shell out to targets. Where an authoring-time hook and a CI check want the same rule, the hook should invoke the CI target rather than reimplementing it. One definition, two entry points.
A worked example
I've been applying this in a downstream repository, and it's cheap: bulla-systems/bulla —
CONTRIBUTING.mdcarries the regime table and the rule, and every trust-bearing check is amaketarget that CI runs:A contributor with no agent tooling satisfies every requirement by running those. The process tooling that repo uses (
day/kan) is explicitly classified as authoring-time and non-trust-bearing — someone who doesn't use it can still land changes; they just won't have the process log.That's the whole shape. It cost one table and one section, and it makes the enforcement claim true by construction rather than by assertion.
Why now
Thermite's thesis is degrade loudly, never launder. The harness had an "asserted enforcement that isn't" failure — the exact shape the harness exists to prevent — and it went undetected for a stage because the claim and the mechanism were in different places with nothing tying them together.
control-plane-check.pynow ties them for the hooks that exist. This issue is about making sure the ones that matter aren't hooks at all.