RUL-006 — Fix on sight — seeing a defect is fixing it, automatically
Ruling
"Any time you see an error, fixing it should be automatic." (verbatim, 2026-07-22). Extends to: fix EVERY bug/defect/failing test/lint/type error encountered, regardless of authorship or scope — never "that's pre-existing"/"not mine"/"out of scope".
Evidence
- Global memory
feedback-fix-on-sight-automatic: a wirer subagent correctly stopped on 3 pyright errors per its charter; relaying them with "I'll fold them in if you want" was itself the violation — the orchestrator must fix immediately, not offer. - Project memory
feedback-fix-everything-own-everything(session 192): "fix EACH AND EVERY error/bug/failing test/lint/type/warning I come across, anywhere, regardless of who wrote it" — trigger words "out of scope"/"pre-existing"/"style debt" are named as diversions, not valid reasons to skip. - Project memory
feedback-fix-bugs-autonomously(session 2026-06-20): fix immediately, then RECORD (changelog/session doc) and TEST (regression guard) it.
Enforcement
- codetools-hook no-offer-instead-of-fix
- none (judgment)
Verification
Partially mechanized — the two bullets above are deliberate, and the split is exact.
codetools hook no-offer-instead-of-fix (formerly .claude/hooks/no-offer-instead-of-fix.sh) is a Stop hook (registered in claude/project-hooks.json alongside forbidden-doubles-stop and the other Stop guards) that reads the last assistant turn and blocks the stop on two families. Matching is per sentence — the permission/deferral construction must co-occur with a repair verb inside ONE sentence — and both families require a defect noun in that sentence or the one immediately before it, because the defect is routinely named first and referred to anaphorically after ("…ruff emits two E501 warnings. Leaving those for another pass."). The lookback is exactly one sentence, so a defect named early cannot arm every later clause in a long turn. Bare test/tests is deliberately not a defect noun — it is ordinary vocabulary in this project; "failing test" and "broken test" still match via failing / broken. The families:
- (A) permission-seeking about a repair — "Do you want me to correct the lint errors?", "Shall I fix the failing test?", "There are three pyright errors. I can fix them if you want.", "Let me know if you want me to resolve the type errors.", "Two lint failures remain. I'll fold them in if you want."
- (B) self-authorised deferral of a KNOWN defect — "Leaving that warning for a follow-up pass.", "I'll fix the failing test in a separate PR."
This is exactly the shape the ruling's own evidence names: the wirer subagent surfaced 3 pyright errors and relayed them with "I'll fold them in if you want" — the offer was the violation, and it is textual, in the assistant's own turn, and mechanically detectable. Verified red on that literal turn; verified silent on a completed repair ("I found three pyright errors and fixed all three"), a forward plan ("I can fix this in one edit — doing it now."), a non-repair offer ("Do you want me to also add a wiki page?"), feature deferral, RUL-024 vocabulary, and the phrase quoted inside a code fence, inline code, or a blockquote.
False-positive measurement, corrected 2026-08-05. The original "0 over 835 tracked markdown files and the last 50 commit messages" was measured on the wrong corpus — this hook's input is an assistant turn, not repo content. Re-measured over all 253 real session transcripts for this project, driving the live Stop payload path: the hook as first written would have blocked 2 turns, and both were false positives — "Want me to verify it's correct and green, then commit — or address something else first?" (no defect deferred) and a completed-repair report asking commit permission, which this project's own "clean the index before scoped commits" rule requires it to ask. It caught zero real violations. Root cause: family A had no defect guard, so a bare repair verb also matched ordinary scope negotiation ("address something else", "the corruption fix"). With the guard extended to family A and bare tests? dropped from the defect vocabulary: 0 blocks over the same 253 transcripts, with every true-positive shape still firing. NO_DEFECT_OFFERS in tests/scripts/test_no_offer_instead_of_fix.py pins that class so the unguarded form cannot return, and test_defect_guard_reaches_back_exactly_one_sentence pins the lookback bound in both directions.
An earlier independent re-verification (2026-08-05) had already closed a false negative: a same-sentence-only guard let the most natural phrasing of family B through, where the defect is named and then deferred anaphorically in the NEXT sentence. That is why the guard looks back one sentence rather than zero.
Known residual over-report, accepted: a turn that completes a repair and then surfaces a genuinely irreversible follow-on decision ("…all three fixed; do you want me to force-push or open a new branch?") is lexically indistinguishable from the violation and can still fire. The block message routes exactly that case to continuation (2) — do the safe part, then state the irreversible step as a named decision — so the hook tells the model how to proceed rather than demanding an impossible repair.
The residual stays judgment, and the hook does not cover it: a defect an agent saw and never mentioned at all leaves no textual trace, so no mechanism can detect it. That half remains enforced by session review and the standing instruction in global ~/.claude/CLAUDE.md ("Engineering standard — fix everything, own everything"), loaded every session. This page deliberately does not claim the hook covers the whole ruling.