# Enforce hard rules — design (2026-05-25 night)

**Status:** In progress (autonomous overnight implementation)
**Origin:** End of brain factor-analysis 4-passes session (HEAD `58784b18`). Honest retrospective showed brain-governance / observer / classifier architecture is observe-only — no enforce. Controller (Claude) rationalized 4 skill bypasses + single coverage tag for 6 hours of varied activity without any hook blocking the behaviour.
**Goal:** Convert soft warnings to hard `exit 2` blocks at the only enforce-able layer Claude Code exposes — PreToolUse + Stop hooks. Substance-of-skill compliance translates to artifact-checks.

## Non-goals

- Constraining Claude's text output (impossible by architecture — LLM generation).
- Enforcing test quality (substance). Future LLM-judge epic.
- Enforcing skill content interpretation. Best-effort via artifact gates.
- Replacing the classifier / observer / brain-retro infrastructure. This is enforcement layer on top.

## Architectural premise

Claude Code hook surface:
- **UserPromptSubmit** — can inject `<system-reminder>` text into the next turn's context. CAN'T block.
- **PreToolUse** — `exit 2` blocks the tool call. Stderr returns to Claude.
- **PostToolUse** — observes, can write state. CAN'T block (tool already ran).
- **Stop** — `exit 2` denies turn completion. Stderr returns to Claude on next continuation.

This proposal uses all four. Output text remains uncontrolled by design — but every consequential ACTION (tool call, turn completion) passes a gate.

## The 10 rules (priority + risk ordered)

### Rule #1 — Mandatory re-classification per prompt

**Mechanism:** UserPromptSubmit hook (`tools/enforce-prompt-classify.mjs`) runs after the existing classifier, then injects a `<system-reminder>` listing:
- Classification + confidence
- 1-3 recommended skills/nodes
- Forced `coverage:` line requirement (first line of response)

**Effect:** Each turn starts with explicit coverage expectation visible to Claude in context.

**Override:** User says one of the override-vocab phrases (see Rule #9). Then injection is suppressed for that prompt.

### Rule #2 — Coverage tag verified against artifacts

**Mechanism:** Stop hook (`tools/enforce-coverage-verify.mjs`). Reads the assistant's last response, parses `coverage: <channel>:<id>`. Then:
- `channel=skill` → check transcript for `Skill` tool_use with `input.skill === id` in this turn. If absent → `exit 2`.
- `channel=node` → check for tool_use matching the node's canonical tool (e.g., #19 frontend-design → check for matching skill or canonical command). If absent → `exit 2`.
- `channel=direct` → no artifact check, but classifier-recommendation must align with non-direct fallback (handled by Rule #8).
- No `coverage:` line at all → `exit 2`.

**Override:** Override-vocab phrase in previous user prompt.

### Rule #3 — TDD-gate on production code

**Mechanism:** PreToolUse hook on `Edit`/`Write`/`MultiEdit` (`tools/enforce-tdd-gate.mjs`). For paths matching production patterns:
- `tools/**/*.mjs` (not `*.test.mjs`)
- `app/app/**/*.php` (not `app/tests/**`)
- `resources/js/**` (not `**/*.spec.ts`, not `**/*.test.ts`)

Reads transcript of current turn so far. Requires:
1. Earlier `Edit`/`Write` on a corresponding test path within the same turn, OR
2. Test artifact already exists (Bash `test -f` could verify, but we read git status)

AND:
3. Earlier `Bash` with `vitest` / `pest` in command, AND
4. The `Bash` stdout in transcript contains a "fail" / "FAIL" marker (RED phase confirmed)

If any check fails → `exit 2` with explanation.

**Override:** Override-vocab phrase + sentinel file `~/.claude/runtime/tdd-bypass-<session_id>.flag` (auto-created from override).

### Rule #4 — Git commit/push requires verification artifact

**Mechanism:** PreToolUse hook on `Bash` (`tools/enforce-verify-before-push.mjs`). Pattern-matches command for `git commit` or `git push`. If matched:
- Check for sentinel file `~/.claude/runtime/verify-pass-<session_id>.json`
- Sentinel contains `last_full_run_at` timestamp, `result: pass|fail`, `command_run`, `tests_total`, `tests_passed`
- Sentinel must be written by Rule's companion PostToolUse hook on Bash, when Bash command matches vitest/pest full-run pattern AND stdout indicates success
- Sentinel age < 600s required; missing or stale → `exit 2`

**Override:** Override-vocab phrase or `RECOVERY-INTENT:` marker in previous response.

### Rule #5 — Memory write requires memory-sync coverage

**Mechanism:** PreToolUse hook on `Edit`/`Write` (`tools/enforce-memory-coverage.mjs`). Path-match:
- `**/memory/*.md`
- `**/MEMORY.md`
- `C:\Users\*\.claude\projects\**\memory\*.md`

Reads last assistant message for `coverage: direct:memory-sync` or `coverage: skill:<memory-related-skill>`.
If coverage absent or stale (matches non-memory channel) → `exit 2` with re-announce instruction.

### Rule #6 — Writing-plans enforce for feature/bugfix/refactor

**Mechanism:** PreToolUse hook on production-code `Edit`/`Write` (folded into Rule #3 hook). Before first production-code edit of a turn classified as `feature`/`bugfix`/`refactor`:
- Either invoke `superpowers:writing-plans` skill (Skill tool_use) in this turn so far, OR
- Plan file exists at `docs/superpowers/plans/<date>-<slug>.md` referenced in transcript, OR
- Override-vocab phrase

If none → `exit 2`.

### Rule #7 — Branch-switch detection before commit

**Mechanism:** PreToolUse on `Bash` matching `git commit`. Hook runs `git branch --show-current`. Compares to expected branch (from `~/.claude/runtime/expected-branch-<session_id>`, written at session start or when user explicitly mentions a branch).

If actual ≠ expected → `exit 2`: «Branch switched silently. Verify via `BRANCH-SWITCH-CONFIRMED` or `RECOVERY-INTENT`.»

### Rule #8 — Classifier-mismatch enforce

**Mechanism:** Stop hook (chained after Rule #2). Reads classifier output:
- If `classifier_output.recommended_node !== null` AND
- `confidence >= 0.7` AND
- No `Skill`/`Task` tool_use matching the recommendation in this turn AND
- No `override: <reason>` line in response

→ `exit 2`.

### Rule #9 — Override-vocabulary hard-coded

**Mechanism:** Config file `tools/enforce-override-vocab.json` with allowed phrases. Hooks read this once at startup. Phrases (initial):
- `без скилов` → all skill-related rules suppressed for this prompt
- `direct ok` → coverage=direct allowed without justification
- `срочно` → Rule #4 (verify) suppressed
- `быстрый коммит` → Rule #3+#4 suppressed for one commit
- `recovery` → Rule #7 suppressed
- `memory dump` → Rule #5 acknowledged auto

Match is literal substring against user's last prompt. Case-insensitive. Each use logged to `~/.claude/runtime/override-usage.jsonl`.

### Rule #10 — Rationalization flags (post-fact audit)

**Mechanism:** PostToolUse on `Bash` (`tools/enforce-rationalization-audit.mjs`). After each prod-code Edit/Write or git commit:
- Scan turn so far for indicators: weak test (≤2 expects), commit message lacking TDD evidence, "just this once" / "for now" / "пока без" / "сейчас быстрее" phrases.
- Each flag appended to `~/.claude/runtime/rationalization-flags-<session_id>.jsonl`.
- Next UserPromptSubmit hook reads this file and injects into context: «Previous turn flagged: X — adjust behavior.»

Soft (no block), but visible to Claude on next turn.

## Anti-self-block strategy during development

Implementing the rules inside the very project they will enforce creates a chicken-and-egg problem. Mitigation:

1. **Develop on feature branch `feat/enforce-hard-rules`** (already created).
2. **Hook scripts are inert until wired into `.claude/settings.json`.** All implementation commits don't trigger them.
3. **Final commit atomically wires all hooks** in settings.json.
4. **First push and test must happen ON main after wire-up commit** — by then all rules are committed AND satisfied (because each new turn after wire will start under enforced rules naturally).

## Test strategy per rule

Per-rule unit tests in `tools/enforce-*.test.mjs`:
- Hook receives fake stdin (event JSON)
- Hook decision verified by exit code + stderr message
- Sentinel file behavior tested with mkdtemp baseDir override
- Override-vocab integration tested by injecting phrase in prev-prompt fixture

Target ~60-100 tests total for all hooks.

## Out of scope (deferred, may revisit morning)

- LLM-judge on test quality
- Confidence threshold tuning (default 0.7, hand-tune via brain-retro)
- Multi-prompt session-level reasoning (each prompt evaluated standalone)
- Conflict resolution if multiple override-vocab phrases stack
- UI for override-usage retro (just JSONL file; brain-retro will read)
