ðŸ›Ą 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)

FileRole
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)

LayerWhatStatus
1 — Unit11 cases on linter + 2 on safelyMatches✓ 13/13
2 — Registry coverageschema + per-rule test count✓ 7/7
3 — Integrationstdin → run-hook.mjs → stdoutdeferred (covered indirectly by Layer 5)
4 — Perfp99 < 5ms under loaddeferred — 1-rule registry, no perf risk
5 — Regressionfull hook suite✓ 8351/8351
6 — Dogfoodverify in live session post-mergepost-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)