feat(hooks): refuse a decision written into code at edit time - #541
Merged
Conversation
The wiki is the only decision index and code carries a pointer, but nothing enforced it. craftsmanship_decisions.py checks the converse, that a `source:` citation resolves, so prose written where a pointer belongs passed every gate. It just did, twice, in scripts/setup.sh: a reviewer caught one block, the owner caught the convention breach, both after the fact. CI is the wrong layer. A check in ci.yml reports the violation once it is written, committed and pushed, so the cost is already paid. Hooks refuse the action instead, which is what they are for. Placement is half the point. `.claude/hooks/` is excluded by .gitignore, so a hook written there is one machine's local config, versioned nowhere. This one is declared in .claude-plugin/plugin.json next to the other Cortex hooks and ships with the plugin. Detection is by shape, not semantics: a decision is prose, and prose in code is a long run of consecutive comment lines. The threshold comes from measuring the tracked tree, not from taste. Exempt: the file header, defined as a run with nothing executable before it so there is no line number to tune or hide inside; tests; and any block already present, so a legacy file stays editable elsewhere. Only what the call introduces is judged. CORTEX_DECISION_GATE=off releases one call and asks for a reason, because a gate with no escape hatch gets disabled wholesale. Eleven tests cover the threshold, both exemptions, the ratchet, the override, a `//` language, malformed input, and the exit-2 contract. Decision recorded in ADR-1060. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Summary
The wiki is the only decision index and code carries a pointer, never the decision itself. Nothing enforced that.
scripts/craftsmanship_decisions.pychecks the converse property, that asource:citation resolves to a real decision, so prose written where a pointer belongs passes every gate.It just did, twice, in
scripts/setup.shduring PR #539. A reviewer caught one block; the owner caught the convention breach. Both are after the fact, which is the problem this PR addresses rather than the two blocks themselves.Closes #
No issue: this came out of the #539 review conversation directly.
Why not CI
A check in
ci.ymlreports the violation once it is written, committed and pushed. The cost is already paid and the fix is a second commit. Hooks exist to refuse the action, so this is aPreToolUsehook that exits 2.Why not
.claude/hooks/.gitignoreexcludes.claude/*with onlysettings.jsonexcepted, so a hook written there is one machine's local configuration, versioned nowhere and distributed to nobody. The first attempt at this landed there and would have protected exactly one working copy. Cortex's own hooks are declared in.claude-plugin/plugin.jsonand implemented undermcp_server/hooks/, run throughscripts/launcher.py. That is the only placement where the rule ships.How it decides
Detecting a decision in general is not possible; detecting its shape is. A decision is prose, and prose in code is a long run of consecutive comment lines. The threshold is measured rather than chosen: over the tracked tree, excluding headers and tests, only a handful of files reach eight, and the two blocks that motivated this were ten and twelve.
A line carrying a
source:pointer never counts toward a run. The refusal nameswiki_adrand the pointer form to leave behind, so the correction is mechanical.Three exemptions, each load-bearing:
CORTEX_DECISION_GATE=offreleases one call and asks for a reason. A gate with no escape hatch gets disabled wholesale.Type of change
Test plan
tests_py/hooks/test_decision_gate.py, eleven tests, all passing. They cover the threshold and the line below it, all three exemptions, the override, a//language, malformed input, and the exit-2 contract through the module entry point.Two mutations they would catch that are worth naming, because both are how this rule would quietly die: raising the threshold (the "one line under the limit" test pins it from below, the block test from above), and widening the header exemption back into a line-number test (
test_allows_a_long_header_on_a_new_filepasses only because the run has nothing executable before it, and the grandfathering test puts executable code first on purpose).One mutation they would NOT catch: a change to the marker table that drops a language. That is a data table, and a test enumerating it would restate rather than verify.
Verified end to end through
scripts/launcher.py, exactly asplugin.jsoninvokes it, on a real file: exit 2 with the refusal on stderr.Audit notes
Self-review only; both blocks that motivated this were caught by the review passes on #539, whose findings are what this PR generalizes.
Coding-standards compliance
Breaking changes
None for library consumers. Behaviourally, an agent editing a file in a Cortex-enabled session can now be refused. The override and the grandfathering are what keep that from being disruptive.
Screenshots / logs
Reviewer checklist
🤖 Generated with Claude Code