# 🛡 tool-invocation-linter — registry-driven defense

Closes #1883. The seed rule catches `Agent(isolation:"worktree")` — a built-in CC param that doesn't reliably isolate, costing operators 3× tool-use bloat. This PR ships the framework so the next bug in this class is a 1-line registry update, not a hook PR.

## Before — every bug walks alone

    🐛 Bug bites
       ↓
    😖 Operator burned (3× tool-uses)
       ↓
    📝 feedback_*.md saved to memory
       ↓
    📚 Docs PR — passive teaching
       ↓
    🪦 Memory is orphan: never enforces

    ⇒  Cost: 1 PR per bug, forever.

## After — defense compounds

    🐛 Bug bites
       ↓
    📒 1-line registry entry +
       2 unit tests
       ↓
    🪝 PreToolUse linter fires at the
       moment of mistake, every session
       ↓
    ⚠️ Loud advisory pointing at workaround

    ⇒  Cost: 1 line per next bug.

## How it works (3 files, 1 wiring)

File | Role
--- | ---
`src/hooks/src/lib/tool-invocation-rules.ts` | Schema + registry. Seed rule: `agent-isolation-worktree`. Adding a rule = appending one object literal.
`src/hooks/src/pretool/tool-invocation-linter.ts` | The hook. Reads registry, filters to `tool_name` matches, evaluates predicates safely, emits one consolidated advisory.
`src/hooks/src/__tests__/lib/ tool-invocation-rules.coverage.test.ts` | Coverage gate. Every `rule.id` must appear in the linter unit-test file at least twice (positive + negative). Forgetting tests for a new rule fails CI.
`pretool/task/sync-task-dispatcher.ts` | One wiring change — adds linter to the Agent-matcher dispatcher. No new hooks.json entry; reuses the existing Agent route.

## Anatomy of a rule

    {
      id: 'agent-isolation-worktree',
      tool_name: 'Agent',
      predicate: (input) => input.isolation === 'worktree',
      severity: 'warn',
      message: "Agent(isolation:'worktree') is known broken — ...",
      see: 'docs/parallel-primitives.md + #1883',
    }

Predicates are pure functions. If one throws (bug), the linter treats it as no-match and keeps going — a buggy rule never crashes the hook.

## The captured advisory

    ⚠️ [agent-isolation-worktree] Agent(isolation:'worktree') is known broken — it flips primary HEAD and races node_modules. Use the manual pre-create pattern: git worktree add ../<repo>-<task> -b <branch> origin/main BEFORE the Agent call, then prefix the agent's prompt with FIRST: cd <path>. (see: docs/parallel-primitives.md + #1883)

## Testing (5 layers of the plan; 3 in this PR)

Layer | What | Status
--- | --- | ---
1 — Unit | 11 cases on linter + 2 on safelyMatches | ✓ 13/13
2 — Registry coverage | schema + per-rule test count | ✓ 7/7
3 — Integration | stdin → run-hook.mjs → stdout | deferred (covered indirectly by Layer 5)
4 — Perf | p99 < 5ms under load | deferred — 1-rule registry, no perf risk
5 — Regression | full hook suite | ✓ 8351/8351
6 — Dogfood | verify in live session post-merge | post-merge (no way to test pre-merge)

Layers 3-4 were planned but provided diminishing return for v1 (the behavioral surface is small, and Layer 5 covers wiring). They're tracked in the follow-up issues.

## What ships next (filed as follow-up issues)

- Memory-driven lint — SessionStart scans `feedback_*.md` for structured `block:` / `warn:` directives and merges them into the registry. Memory becomes runtime defense without a PR.
- /ork:safe-spawn skill — declarative replacement for `Agent(isolation:"worktree")`. Linter's `see:` field will point operators at it.