# Runtime PreToolUse Hook for Companion-Preference Override

**Date:** 2026-05-13
**Slug:** `runtime-hook-companion-preference`
**Operator:** Naim
**Reason:** PR #133 shipped the `continuous_improvement.companion_preference` settings key as documented routing protocol — the model reads it at task time, but nothing on the runtime layer enforces it. The skill explicitly disclaims this: "an agent operating in good faith honors the flag the same way it honors any other documented dispatcher rule; an agent that ignores it is a discipline failure the instinct system records, not a runtime denial." This PR closes that gap with a single new PreToolUse hook (`hooks/companion-preference.mjs`) — the runtime-hook WILD promoted from the 2026-05-13 review.

## Single-concern scope

| PR | Title | Scope (files) | Test strategy | Merge order |
|---|---|---|---|---|
| 3 | `feat(hook): runtime companion-preference enforcement` | `src/hooks/companion-preference.mts`, `src/test/companion-preference-hook.test.mts` (RED-then-GREEN under TDD), `plugins/continuous-improvement/hooks/hooks.json` (register), `skills/superpowers.md` (update "What this does not do" paragraph), `src/bin/check-docs-substrings.mts` (lock the hook reference) + the generated mirrors (`hooks/`, `bin/`, `test/`, plugin mirror) | `npm test` runs the 8 hook tests; `npm run verify:all` runs 8 invariants | Independent |

## TDD plan

RED state — write tests before any production code, watch them fail:

1. `tool_name !== "Skill"` → allow, no stderr.
2. Missing `~/.claude/settings.json` → defaults to `ci-first`, allow, no stderr.
3. Mode `ci-first` + mapped skill (`tdd-workflow`) → allow, no stderr.
4. Mode `companions-first` + mapped CI skill → allow, advisory stderr names the preferred companion.
5. Mode `companions-first` + non-mapped skill (`gateguard`) → allow, no stderr.
6. Mode `strict-companions` + mapped CI skill + companion installed → block, reason names companion.
7. Mode `strict-companions` + mapped CI skill + companion not installed → block, reason has install hint.
8. Malformed JSON on stdin → fail-open, allow.

GREEN state — implement `src/hooks/companion-preference.mts` until all 8 pass. The override map mirrors the `## Companion-Preference Override` table in `skills/superpowers.md` row-for-row: `tdd-workflow → superpowers:test-driven-development`, `verification-loop → superpowers:verification-before-completion`, `context-budget → agent-skills:context-engineering`, `ralph → oh-my-claudecode:ultrawork`, `learn-eval → oh-my-claudecode:retrospective`.

## Out of scope

- Adding new override pairs beyond the 5 currently in the dispatcher table (`planning-with-files` row is intentionally absent — `superpowers:writing-plans` already wins under both settings).
- Rewriting Task / Agent tool calls (only `Skill` is wired; Task routing requires a different observation surface and ships separately if telemetry shows demand).
- A `--minimal-hooks` install mode to opt out of the new hook (separate follow-up; for now, the hook is no-op under `ci-first` which is the default).
- Auto-detecting installed companions beyond `existsSync(~/.claude/plugins/<plugin>)`. False negatives degrade to "advisory" or "install hint," both safe failure modes.

## Past Mistake Acknowledgment Gate

- **PR #66 wiped by tsc** — active in scope this time (touches `.mts` source). Mitigation: all edits land in `.mts`; `.mjs` regenerated by `npm run build`.
- **Direct push to main on 2026-05-05** — using feature branch + PR flow.
- **PR #26 bundling unpushed local-main commits** — branched from `origin/main` (`37711de`, post-#134 merge); the stray `b5fdaf6` was moved to `recovery/stray-skill-table-sync-20260513` for separate handling.

**Will NOT repeat:** shipping a runtime claim in skills/superpowers.md without the `hooks/<name>.mjs` anchor that `verify:doc-runtime-claims` requires — the 2026-05-13 fix on PR #133 caught exactly this when the "runtime gate" disclaimer needed the gateguard anchor.

## Verification ladder

- `npm test` — new hook test file adds 8 cases.
- `npm run verify:all` — 8 invariants must pass. `verify:doc-runtime-claims` will pass because the updated paragraph now cites `hooks/companion-preference.mjs` directly.
- Manual: spawn the hook with a synthetic payload to confirm allow/block decisions match the routing-table contract.

## End-state behavior

After PR 3 merges, the override flag is enforced at the PreToolUse layer:

- `ci-first` (default) — no behavior change.
- `companions-first` — agents see a stderr advisory whenever they route to a CI skill that has a companion equivalent. Agent continues; advisory is observational discipline, not blocking.
- `strict-companions` — agents are blocked from invoking the CI skill at the runtime layer. Hook decision is `block`; the model receives the reason and is forced to route differently or to install the missing companion.

The dispatcher table in `skills/superpowers.md` and the hook's override map are now the same source of truth — drift between them surfaces as either a test failure (test asserts pair-by-pair) or a `verify:doc-runtime-claims` regression.
