fix(harness): re-wire the de-wired gates + route the control plane (#93) - #94
Merged
Merged
Conversation
Commit 5581b65 ("Stage-3 REQ-1: the @bv clause tag, parse-gated") removed three hook entries from .claude/settings.json: the PostToolUse/Read recorder and the PreToolUse/Write|Edit pair invoking tooling/spec-discipline.py and tooling/anti-pattern-gate.py. Its message is entirely about the @bv clause tag and never mentions the settings change; the crosslink-generic entries survived while the project-specific ones vanished — the signature of `crosslink init` regenerating a tracked-but-machine-authored file. Both agent-facing gates were therefore dormant for the entire Stage-3 arc, while README.md:172 ("they enforce automatically — no setup"), goal.md's R-XLATE-1/2/3, and all four .claude/agents/acto-*.md kept asserting they fire. The "asserted enforcement that isn't" failure mode, in the harness built to prevent it. It stayed hidden because no route covered the control plane: doc-drift.py pins freshness for files reachable from spec-routes.toml, and .claude/settings.json was not one of them. The design layer governed everything except the file that decides whether the governance runs. - .claude/settings.json: restore the three entries, byte-identical to the pre-clobber c7dc32b revision. - tooling/control-plane-check.py: NEW. Asserts every hook the docs claim is live is wired and its script present. Matcher COVERAGE not equality (Write|Edit|Bash satisfies Write|Edit; Write alone does not). Exit 0/1/3 mirroring doc-drift.py — an unparseable settings.json is a FINDING (Claude Code loads no hooks from it) and exit 3 is reserved for the gate's own environment failing, so it never fails open. A finding prints the exact JSON to paste back. - tooling/tests/test_control_plane.py: NEW. Nine hand-authored oracle fixtures (R-CHAR-3). O-2's fixture is the verbatim post-5581b65f settings.json, so the suite goes red if the gate ever stops catching this exact regression. - tooling/spec-routes.toml: route .claude/settings.json, .claude/agents/*.md and the new gate to .design/tooling/control-plane.md. DECLARATIVE for spec-discipline (is_gated_path needs .rs + a thermite-/forge crate + src/), ENFORCED for doc-drift — the same split as the doc-drift.py dogfood route. - .design/tooling/control-plane.md: NEW governing doc, REQ-1..REQ-7 + AC-1..7, content-pinned. OQ-1..OQ-3 record the three gaps found alongside this one and deliberately left open (agent-def model:fable vs "Opus — always"; no clippy.toml backstopping the anti-pattern rules; critic read-only-ness conventional rather than capability-enforced). - CI + Makefile: `control-plane gate (hook wiring)` step, `make control-plane` / `control-plane-test`. Not in `make audit` — wiring is a development-discipline invariant, not a link in the proof-trust chain (doc-drift decision 5); scripts/audit.sh is byte-identical. - README.md: the enforcement claim now names the check that keeps it true. Verified: control-plane 0, doc-drift 0 (control-plane.md CURRENT), req-status 0, req-registry 0 (453 reqs), 46 tooling tests OK. Dogfooded — removing the anti-pattern entry again fires BOTH gates (doc-drift DRIFT + two MISSING-WIRING findings). Closes #93 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Collaborator
|
For the record: I disabled these intentionally when I was doing my work because I found the default process flow on the repo quite annoying haha. I think it is worth a design note for how to collaborate between folks who use different agent-dev / gating strategies -- what is actually in the repo vs. kept locally for individual devs' preferences. |
Member
Author
|
Yeah that was a problem with crosslink and still is, individual preferences overriding the defaults :p |
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.
Closes #93.
What was broken
5581b65f("Stage-3 REQ-1: the @bv clause tag, parse-gated") removed three hook entries from.claude/settings.json— thePostToolUse/Readrecorder and thePreToolUse/Write|Editpair invoking bothtooling/spec-discipline.pyandtooling/anti-pattern-gate.py. The commit message is entirely about the@bvclause tag and never mentions the settings change. The crosslink-generic entries survived; only the project-specific ones vanished — the signature ofcrosslink initregenerating a file that is tracked but machine-authored.Both agent-facing gates were dormant for the entire Stage-3 arc, while
README.md:172("they enforce automatically — no setup"),goal.md's R-XLATE-1/2/3, and all four.claude/agents/acto-*.mdkept asserting they fire.It stayed hidden because no route covered the control plane.
doc-drift.pypins freshness for files reachable fromspec-routes.toml, and.claude/settings.jsonwas not one of them. The design layer governed everything except the file that decides whether the governance runs.What this does
.claude/settings.jsonc7dc32berevision.tooling/control-plane-check.py(new)tooling/tests/test_control_plane.py(new)tooling/spec-routes.toml.claude/settings.json,.claude/agents/*.md, and the new gate..design/tooling/control-plane.md(new)Makefilecontrol-plane gate (hook wiring)step;make control-plane/control-plane-test.README.mdDesign choices worth a reviewer's attention:
crosslink initis silent repair of a control plane — you stop noticing something keeps breaking it. This fails loudly and prints the exact JSON to paste back.Write|Edit|Bashsatisfies aWrite|Editrequirement;Writealone does not. Equality false-fails on a harmless reorder, substring matching passes a matcher that silently droppedEdit.settings.jsonis a FINDING (exit 1), not INCONCLUSIVE — Claude Code loads no hooks at all from one, so it is maximally gate-dead. Exit 3 is reserved for the gate's own environment failing (doc-drift.pyREQ-9 / R-HONEST-3: a gate that fails open is a silent pass).if [ -f "$HOOK" ]-guarded, so a wiring naming a missing script degrades to a silent no-op.MISSING-SCRIPTis separate fromMISSING-WIRINGbecause the fix differs.is_gated_pathrequires.rs+ athermite-/forgecrate dir + asrc/component, so the hook structurally cannot gate a.json/.mdfile — the same honest limitationdoc-drift-tripwire.mdREQ-11 records fortooling/*.py. Same split as the existingdoc-drift.pydogfood route.make audit. Hook wiring is a development-discipline invariant, not a link in the proof-trust chain (doc-drift decision 5).scripts/audit.shis byte-identical.Verification
Dogfooded. Removing the anti-pattern entry again fires both gates:
doc-driftreportsDRIFToncontrol-plane.md, andcontrol-plane-checkreports twoMISSING-WIRINGfindings with paste-ready restore snippets.O-2's test fixture is the verbatim post-5581b65fsettings.json, so the suite goes red if the gate ever stops catching this exact regression.Deliberately left open (recorded as OQ-1..OQ-3 in the design doc)
These are the other findings from #93; each is a judgment call I did not want to make silently inside a wiring fix:
acto-critic.md/acto-doc-author.mddeclaremodel: fablein frontmatter while their bodies read "Opus — always. … Never substitute." Routing them means any future edit re-pins the doc, but which side is correct is a harness-behavior decision for the maintainer.clippy.toml;ci.yml:95is stockcargo clippy -D warningswith nodisallowed_*/unwrap_used. The anti-pattern rules are still hook-only, so a clone that never runs Claude Code gets none of them.acto-criticwithholdsEditbut retainsWrite+ unrestrictedBash, so "cannot modify production code" is prose-patched. The critic genuinely needsWritefor test files, so the fix is a read-only mount or path-scoped allowlist, not droppingWrite.🤖 Generated with Claude Code