skill-router · diff
git:20260907.31583de to git:20260907.20693dc
45 added, 377 removed. Audit A to A.
---
name: skill-router
- description: INVOKE BEFORE EVERY NON-TRIVIAL TASK — before writing code, before using any tool, before answering. Do not skip. Produces the required Skill + Agent + Model for the task. Routing engine for 400+ installed skills.
- ---
-
- # Skill Router
-
- **Output always:** `Skill + Agent + Model`
-
- Routing runs automatically. A `UserPromptSubmit` hook classifies every prompt
- and injects a `[skill-router]` announcement before your first action, so most
- of the time your job is to *follow* an announcement, not compute one. This file
- is the contract that announcement is written against, and what you fall back on
- when the hook is silent.
-
- **First, is it healthy?** If you have not seen a `[skill-router]` line in a
- while, routing may be dead rather than quiet:
-
- ```bash
- python3 ~/.claude/skills/skill-router/scripts/doctor.py
- ```
-
- ---
-
- ## THE 3-QUESTION TRIAGE (run now, takes 5 seconds)
-
- ```
- Q1: Is something BROKEN / WRONG / FAILING?
- Error, crash, test fail, unexpected output, user correction
- YES → BROKEN PATH
-
- Q2: Is this CREATE / BUILD / ADD something new?
- New feature, file, component, integration, page, script
- YES → BUILD PATH
-
- Q3: Everything else (improve, ship, configure, automate, research)
- → OPERATE PATH
-
- AMBIGUOUS? → Default to HIGHER-COMPLEXITY path
- ```
-
- Before the table, two layers get first refusal:
-
- 1. **Project routes** — `SKILL.personal.md` maps names that mean exactly one
- project (`@economicalai`, `testflight`, `jobhunt`) straight to that
- project's skill. Generic triage cannot tell one project from another; this
- is the only layer that can.
- 2. **The routing table** below.
-
- ---
-
- ## BROKEN PATH
-
- | Signal | Skill | Agent | Thinking |
- |--------|-------|-------|----------|
- | Error / crash / exception | `superpowers:systematic-debugging` | general-purpose | think |
- | Test failing | `superpowers:systematic-debugging` | test-runner | think |
- | Performance regression | `perf` | optimizer | think |
- | Security issue found | `security` | security-auditor | think-hard |
- | Deploy / build failed | `superpowers:systematic-debugging` | general-purpose | think |
- | User says "no" / "wrong" | STOP → `superpowers:systematic-debugging` | general-purpose | think |
- | Production incident | `superpowers:systematic-debugging` | general-purpose | **ultrathink** |
-
- ---
-
- ## BUILD PATH
-
- **Multi-file / new feature:** `writing-plans` → domain skill
- **Single file / trivial add:** go directly to the domain skill
-
- | What | Skill | Agent | Thinking |
- |------|-------|-------|----------|
- | UI component / page | `frontend-design:frontend-design` | feature-dev:code-architect | none |
- | API endpoint | `feature-dev:feature-dev` | feature-dev:code-architect | think |
- | Database schema | `supabase:supabase` | db-expert | think |
- | Auth / permissions | `security` | security-auditor | **ultrathink** |
- | AI / RAG / agent feature | `superpowers:writing-plans` | feature-dev:code-architect | think-hard |
- | 3rd-party integration | `connect-apps` | integration-specialist | none |
- | Mobile screen | `frontend-design:frontend-design` | feature-dev:code-architect | none |
- | CLI / automation script | `superpowers:writing-plans` | general-purpose | think |
- | Skill / Claude skill file | `superpowers:writing-skills` | general-purpose | think |
-
- ---
-
- ## OPERATE PATH
-
- | Signal | Skill | Agent | Thinking |
- |--------|-------|-------|----------|
- | Refactor / clean up | `refactor` | code-simplifier:code-simplifier | none |
- | Add tests / coverage | `superpowers:test-driven-development` | test-runner | none |
- | Code review | `superpowers:requesting-code-review` | code-reviewer | think-hard |
- | Got review feedback | `superpowers:receiving-code-review` | general-purpose | think |
- | Deploy | `superpowers:verification-before-completion` → `vercel:deploy` | general-purpose | none |
- | Merge / PR / push | `superpowers:finishing-a-development-branch` | general-purpose | none |
- | 2+ independent tasks | `superpowers:dispatching-parallel-agents` | general-purpose | none |
- | Resume previous work | `superpowers:executing-plans` | general-purpose | none |
- | Architecture / scope call | `superpowers:writing-plans` | general-purpose | **ultrathink** |
-
- ---
-
- ## THE MODEL COLUMN IS `inherit`
-
- Steps run **in this session, at this session's model**, unless there is a
- reason to do otherwise. There are two:
-
- | Value | Meaning | When |
- |---|---|---|
- | `inherit` | run in-session at the parent's model | the default, nearly always |
- | `haiku` | dispatch to a sub-agent on the cheap model | bulk read-only scans whose output is a list of paths, not a judgment |
-
- The table used to name `sonnet` on most rows and `opus` on hard ones. That was
- written when the parent was always Sonnet, so `sonnet` silently meant
- `inherit`. It stopped meaning that — and because the dispatch protocol reads
- "step model ≠ parent model" as "fan out to a sub-agent", every routed step on a
- Fable or Opus session was being shipped to a *weaker* model than the user chose.
-
- Depth now comes from `Thinking`, which composes with any model. Never
- reintroduce a frontier model name into the table.
-
- **The same rule applies to agent frontmatter.** `model: sonnet` in an agent
- file wins over the session, so dispatching to it from an Opus or Fable session
- buys sub-agent overhead at lower capability. Measured on 2.1.263 from an Opus
- parent: a pinned dispatch used `claude-opus-5` *and* `claude-sonnet-5`; an
- `inherit` dispatch used only `claude-opus-5`. `doctor.py` checks this;
- `scripts/fix_agent_models.py` fixes it. `haiku` is exempt — the one deliberate
- downgrade.
-
- ---
-
- ## SPECIALIST LAYER — 400 skills, not 20
-
- The tables above name ~20 process skills. This machine has ~395 invokable ones,
- and the rest are the domain specialists that make a task go faster. They cannot
- be enumerated in a table — new skills land weekly — so the router ranks them at
- query time and appends one advisory line:
-
- ```
- [skill-router] Specialist available: seo-technical — Technical SEO audit across 9 categories…
- ```
-
- Advisory, never enforced. Load it alongside the announced step when it fits;
- ignore it when it doesn't. When no specialist clears the confidence bar, the
- line is absent — which is the common case and the correct one.
-
- ```bash
- # what would match, and why
- python3 ~/.claude/skills/skill-router/scripts/catalog_match.py "your prompt"
- ```
-
- The catalog rebuilds at every `SessionStart`, so a skill you install today is
- routable today. Non-invokable skills (`~/.agent/skills`, `~/.composio-skills`)
- are catalogued but never announced — they can only be suggested for install.
-
- ---
-
- ## SUB-AGENTS GET ROUTED TOO
-
- A `SubagentStart` hook injects the same kind of brief into every dispatched
- sub-agent, naming the skills that fit that agent's job:
-
- ```
- [skill-router] Skills for seo-technical: Skill(skill="seo-technical"), Skill(skill="seo")
- ```
-
- Pairings come from `agent_skills.json` when hand-set, and are otherwise derived
- by ranking the agent's own description against the catalog — so a new agent
- gets sensible skills with no registration step. Generic agents
- (`general-purpose`, `Explore`, `researcher`) get nothing, because they have no
- domain and any suggestion would be noise.
-
- The hook is the mechanism, deliberately. Agent frontmatter also documents a
- `skills:` field that preloads full skill text at startup; on Claude Code
- 2.1.263 it was tested here and the content did not arrive in the sub-agent's
- context, while the hook brief did — verified by asking a dispatched agent what
- it could see. Revisit `skills:` when a release notes a fix; until then the hook
- is the only path that demonstrably works.
-
- **The IRON RULE does not apply inside sub-agents.** The parent's pending skill
- belongs to the parent's turn and a sub-agent cannot satisfy it, so enforcement
- stands down at the boundary. Sub-agents are told what to use; they are never
- blocked.
-
- When you dispatch an agent yourself, name the skill in the prompt:
-
- ```
- Agent(subagent_type="db-expert",
- prompt="think. Use Skill: supabase:supabase. Task: … Context: …")
- ```
-
- ---
-
- ## IT LEARNS — AND THE LEARNING LIVES OUTSIDE THE REPO
-
- Everything above is generic. What makes it yours accumulates in one file that
- is never committed and never hand-edited:
-
- ```
- ~/.claude/skill_router_learned.json regenerated by scripts/learn.py at every session start
- ```
-
- The engine is versioned and shared; the overlay is personal and regenerated.
- Nothing learned is ever written into SKILL.md or SKILL.personal.md — those
- stay hand-curated. Earlier tooling appended learned chains into the personal
- file, which made every relearn a merge; that path is retired.
-
- Four signals, each with a support-and-precision floor (a pattern seen twice is
- a coincidence), each surfaced where it is useful rather than in a report:
-
- | Learned | From | Surfaces as |
- |---|---|---|
- | **Triggers** — keyword → skill | prompts where you then invoked a skill | `[skill-router] Learned from your history: prompts with testflight, submit usually use scrollbook-deploy.` — only when the table and matcher are both silent |
- | **Handovers** — what follows what | consecutive Skill calls in a session | after a Skill runs: `[skill-router] After writing-plans you usually run test-driven-development (71%, n=7).` |
- | **Chains** — recurring 3–4 step flows | n-grams over sessions | at announcement: `Your usual flow from here: tdd → verification` |
- | **Follow rates** — announced vs invoked | announcement / invoke joins by prompt id | the router refuses to resurrect a skill you routinely ignore |
-
- Plus two forms of **discovery**, shown as at most three lines at session start:
-
- ```
- [skill-router] New since last session: scrollbook-qa, seo-hreflang (+3) — the router can announce these now.
- [skill-router] Fits your recent work, not installed: stripe-webhooks (antigravity) ← stripe, checkout, webhooks
- [skill-router] Your usual flow: superpowers:writing-plans → test-driven-development → verification (×7)
- ```
-
- - **Installed** — the catalog is diffed against the last run, so a skill you
- add is announced at your next session and then never again.
- - **Online** — the four known catalogs (2,400 skills) are refetched weekly in
- the background and ranked against *your* recent prompt keywords, not offered
- as a list. Nothing is suggested until 20 prompts of signal exist, because a
- profile built from skill names alone recommends frameworks to everyone.
-
- All of it is advisory. Learned associations never carry the IRON RULE: they
- are statistics about your habits, and enforcing a habit turns a nudge into a
- cage. What gets logged is prompt **keywords**, never prompt text; set
- `SKILL_ROUTER_NO_LEARN=1` to log nothing.
-
- ```bash
- python3 ~/.claude/skills/skill-router/scripts/learn.py # relearn now, print what it found
- python3 ~/.claude/skills/skill-router/scripts/learn.py --show # dump the overlay
- ```
-
- ---
-
- ## WHEN NO SKILL IS NEEDED
-
- Single-line fix · reading code · one factual question · one command · under 3
- trivial steps.
-
- **Silence is an answer.** When no triage signal matches, the router emits
- nothing. A missing `[skill-router]` line means the prompt was conversational,
- exploratory, or trivial — not that you should invent a route.
-
- **Explicit slash-commands stand down.** If the prompt is itself an invocation
- (`/gstack`, `/ship prod`), the user already chose. Never reclassify it.
-
- ---
-
- ## IRON RULE
-
- When the router announces, a `PreToolUse` hook denies `Edit`/`Write`/`Task`/
- `NotebookEdit`/`MultiEdit` until that skill is invoked, and a `Stop` hook blocks
- turn end if it never was. `Read`/`Glob`/`Grep`/`Bash`/`TodoWrite`/`Skill` stay
- allowed, so context-gathering, shell work, and the override below keep working.
-
- **Two escape hatches:**
-
- 1. **User opt-out** — the *user* includes `[no-router]` in their next message.
- Writing it in your own response does nothing.
- 2. **Reasoned override** — if you judge the route wrong, don't fight the rule:
-
- ```bash
- python3 ~/.claude/skills/skill-router/scripts/router_override.py "<reason>"
- ```
-
- This clears the rule for the turn, logs your reason, and counts against that
- skill. Past `OVERRIDE_THRESHOLD` the router defers it — **for
- `DEFER_TTL_DAYS`, then it comes back**. Demotions expire on purpose:
- permanent ones once killed five core skills here and left the router
- answering SKIP to everything while looking perfectly healthy.
-
- ---
-
- ## COMPLETION GATE
-
- Before any "done" claim → `superpowers:verification-before-completion`
-
- ```
- □ Code actually runs correctly
- □ TypeScript passes (tsc --noEmit)
- □ Tests pass
- □ Original request fully met (re-read it)
- ```
-
+ description: Use when a [skill-router] route card appears in the turn, when no card appeared on a non-trivial task, or when a route looks wrong. Routes every prompt to the right installed skill, pairs it with a process skill, tiers enforcement, and briefs sub-agents. 79 local skills + plugin skills indexed.
---
- ## ANNOUNCEMENT FORMAT — output VERBATIM (substitute only `<vars>`)
-
- The announcement is the testable contract: `grep '\[skill-router\]'` shows what
- fired. When the hook already produced one, follow it — do not reprint it.
-
- **Single-domain:**
- ```
- [skill-router] This is a <BROKEN|BUILD|OPERATE> task → <skill> → <agent>.
- [skill-router] Model: <in-session|haiku> · Thinking: <thinking>
- [skill-router] Invoke now:
-
- ▶ <skill> (<inherit|model>, <in-session | via Agent>)
- ```
-
- **Multi-domain:**
- ```
- [skill-router] This touches <N> domains: <d1>, <d2>.
- [skill-router] Chain: <s1> → <s2> + <s3>
- [skill-router] Models: inherit (this session) · Thinking: <max-thinking>
- [skill-router] Invoke step 1/<N> now:
-
- ▶ <s1> (inherit, in-session)
- ▶ <s2> + <s3> (inherit, parallel via Agent)
- ```
-
- Rules:
- - `Thinking:` is the deepest of any step. Omit when every step is `none`.
- - Each `▶` line ends with `in-session`, `via Agent`, or `parallel via Agent`.
- - After each step: `[skill-router] Step <n>/<N> done.` On completion:
- `[skill-router] Chain done.`
+ # Skill Router (v4)
- ---
+ Routing runs from hooks. A `UserPromptSubmit` hook classifies every prompt
+ and injects a **route card** before your first action. Your job is to
+ *follow the card*, not to compute one.
- ## DISPATCH PROTOCOL
+ ## The route card
```
- For each step:
- IF step.model == "inherit" AND not a parallel fan-out:
- → Skill(<skill>) in-session (same context, no dispatch cost)
- ELSE:
- → Agent(subagent_type=<agent>, model=<model>,
- prompt="<thinking-keyword>. Use Skill: <skill>. Task: <slice>. Context: <files>")
- Sequential `→`: wait. Parallel `+`: one message, multiple Agent calls.
+ [skill-router] This is a BROKEN task — 2-step chain.
+ [skill-router] Chain: mac-doctor → superpowers:systematic-debugging
+ ▶ mac-doctor (inherit, in-session)
+ ▶ superpowers:systematic-debugging (inherit, in-session)
+ [skill-router] Gates before done: simulator screenshot
+ [skill-router] Memory: airbook_crash_root_cause (read from ~/.claude/projects/-Users-airbook/memory/)
+ [skill-router] IRON RULE: call Skill(skill="mac-doctor") before any Edit/Write/Task.
```
- Parallel steps go through `Agent` even at `inherit` — not for the model, but
- because they need independent contexts to run at once.
-
- Full event schema and verification: [`references/dispatch-protocol.md`](./references/dispatch-protocol.md).
+ - **First `▶` is the domain skill** — one of yours, or a plugin's. Load it first.
+ - **Second `▶` is the process skill** (debugging, planning, TDD). Load it next.
+ - **Gates** are completion conditions from the project. Check every one before "done".
+ - **Memory** names the memory file for this project. Read it before deciding.
- ---
+ ## Two tiers
- ## THINKING DEPTH
+ | Line on the card | Meaning |
+ |---|---|
+ | `IRON RULE: …` | **hard** — Edit/Write/Task are denied until the skill is loaded. BROKEN path, and project routes with gates. |
+ | `Soft route: …` | **soft** — nothing is blocked. If you finish without loading it, the Stop hook asks once; answer by loading it or by one line `[skill-router] skipped <skill>: <reason>`. |
- Pre-pend the row's `Thinking` value as the literal first word of the dispatch
- prompt. Do not paraphrase.
+ Wrong route either way: `python3 ~/.claude/skills/skill-router/scripts/router_override.py "<reason>"`.
+ The user can also write `[no-router]` in their message.
- | Value | Pre-pend |
- |-------|----------|
- | `none` | (nothing) |
- | `think` | `think.` |
- | `think-hard` | `think hard.` |
- | `ultrathink` | `ultrathink.` |
+ ## How the card is decided
- Full rules: [`references/thinking-depth.md`](./references/thinking-depth.md).
+ 1. **Project route** in `SKILL.personal.md` (`@economicalai`, `capgo`, …) — deterministic.
+ 2. **Enriched index** of every invokable skill (`~/.claude/skill_index.json`): name, "use when" triggers, keywords, project aliases. Lexical rank, ~50 ms.
+ 3. **Small model** (Gemini Flash-Lite, ~1 s, cached) settles low-confidence ties.
+ 4. **Process table** (`references/routing-tables.md`) supplies the second leg.
- ---
+ Questions, discussion and harness noise get no card. Silence is an answer.
- ## COMPLEXITY RULE
+ ## Sub-agents
- ```
- 1 domain → 1 skill → single-domain announcement
- 2+ domains → announce chain → multi-domain announcement
- ```
+ Every `Agent(...)` dispatch gets the parent's route appended to its prompt
+ (skill, gates, memory), and `SubagentStart` briefs the agent with the skills
+ paired to its type. Enforcement never reaches inside a sub-agent.
- `→` sequential (B depends on A) · `+` parallel (no shared state).
- Chain shapes: [`references/multi-domain-chaining.md`](./references/multi-domain-chaining.md).
+ ## When there is no card
- ---
+ Non-trivial task, no `[skill-router]` line? Run the fallback yourself:
+ `python3 ~/.claude/skills/skill-router/scripts/router.py <<< "the prompt"`.
+ If that is silent too, routing may be dead: `python3 ~/.claude/skills/skill-router/scripts/doctor.py`.
- ## MAINTENANCE
+ ## Maintenance
| Command | When |
|---|---|
- | `scripts/doctor.py` | routing feels dead, or after any Claude Code upgrade |
- | `scripts/install_hooks.py` | doctor reports missing hooks |
- | `scripts/build_catalog.py` | you installed a skill and want it routable now |
- | `scripts/fix_agent_models.py` | after adding an agent, so it follows your session model |
- | `scripts/catalog_match.py "<prompt>"` | a specialist suggestion looked wrong |
- | `scripts/learn.py` | relearn the personal overlay now (runs itself at every session start) |
- | `scripts/learn.py --show` | inspect what has been learned |
- | `python3 -m pytest tests/ -q` | after editing any routing logic |
- | `tests/calibration.py --min-accuracy 95` | accuracy gate over 109 curated prompts |
-
- Personal routes and project guardrails: [`SKILL.personal.md`](./SKILL.personal.md).
-
- ---
-
- ## RED FLAGS — signs you're about to skip this
+ | `scripts/doctor.py` | routing feels dead, or after a Claude Code upgrade |
+ | `scripts/build_index.py --enrich` | you installed or edited a skill |
+ | `scripts/index_match.py --all "<prompt>"` | a route looked wrong — see the ranking |
+ | `scripts/learn.py --show` | what has been learned |
+ | `scripts/check.sh` | after editing any routing logic |
- ```
- "This is simple" → Simple things take 5s to route.
- "I know what to do" → Then routing confirms it. 5s cost, 0 downside.
- "No match in table" → Check the specialist line before giving up.
- "Ambiguous task" → Default to the higher-complexity path.
- "I'll just paraphrase" → No. The [skill-router] format is the contract.
- "The announcement is wrong" → Overrule it with a reason. Don't ignore it.
- ```
+ Project routes, projects, gates: `SKILL.personal.md`. Full tables and protocols: `references/routing-tables.md`.