# Stack skill routing  -  letting the toolkit plugin choose its own skills

> **TLDR**  -  When a toolkit plugin is enabled, Phase 3 asks that plugin's own `index` skill which of its skills apply to this task, loads them before writing code, and records each into `state.telemetry.skillCalls[]`. The routing table lives in the plugin; the pipeline copies none of it, and does not keep a stack-to-plugin table either - it reads the enabled set.
>
> The same routing applies outside a run: `rules/outside-the-pipeline.md` carries it for ordinary sessions. One discipline, two callers; only the recording is run-specific.

## Why this exists

Phase 3 dispatched to the toolkit plugin for exactly one case, `taskType === "component"` (see `component-dispatch.md`). Every other task  -  `bugfix`, `feature`, `refactor`, `chore`  -  had no skill dispatch at all: whichever skills the host happened to surface by description match were the ones that got used, and nothing recorded or required any of them.

That is the dev-side half of the gap `features/skill-conformance.md` closes on the review side. Review now asks "was this built to the rules it was supposed to follow"; without this step, the answer for a non-component task was "there were no declared rules, because nobody chose any".

The fix is not a routing table in the pipeline. Each `ai-<platform>-toolkit` already ships one: an `index` skill whose description says *"Load this first when unsure which skill applies"*, holding a 30-plus row intent-to-skill map maintained alongside the skills it points at. A second copy in this repo would drift the moment the plugin shipped a new skill, and the pipeline's copy would be the stale one.

So the pipeline's job is to **ask**, not to know.

## When it runs

Phase 3 pre-flight, before any code is written, for **every** `taskType`. Component tasks keep their dedicated dispatch in `component-dispatch.md`; this step runs in addition, because the reference skills (architecture, naming, file placement, tokens) apply to a component build too.

## Resolution

**Read the enabled set; do not keep a stack table.** Every `@multi-agent-plugins`
toolkit enabled for this repo is a candidate, and each one ships its own `index`.
The effective set is the repo's `.claude/settings.json` `enabledPlugins` over
`~/.claude/settings.json`; `/multi-agent:stack` is what writes it.

This replaced a `stack -> toolkit` table, and the reason is worth keeping. That table
mapped `ios` and `android` and sent everything else to "no toolkit" - but six plugins
ship an `index`, so a React or backend repo was told it had no toolkit while its
plugin sat enabled with a routing table inside it. A table has to be widened every
time a toolkit ships; the enabled set never goes stale, because it IS the answer to
"what is on here".

`ai-common-toolkit` and `ai-analyst-toolkit` are enabled everywhere and are always
candidates - neither is stack-specific. A corporate variant (`ai-ios-engineering-toolkit`
and friends) is a candidate too when a repo enables only it: it is in `enabledPlugins`
like any other, which is the whole point of reading the set instead of naming names.

Nothing enabled is not an error: a repo whose stack was never selected legitimately has
no toolkit. Record the no-op with the enabled set that was read, so "none applied" is
distinguishable from "never looked".

**Not enabled is not an error here**, unlike component dispatch: a repo whose stack was never selected legitimately has no toolkit, and halting would make the pipeline unusable there. Record the no-op and continue. (This paragraph used to say "a backend or web repo legitimately has no toolkit" - that stopped being true when the frontend and backend toolkits shipped, and the sentence outlived the fact by several releases.)

Two marketplaces may ship the same toolkit name (a public one and a corporate one). Resolve whichever is enabled and record its **name and version** in the ledger entry, because the routing table and the skill set differ between versions  -  a finding that cites a skill has to be traceable to the version that defined it.

## The call

```text
Skill(<toolkit>:index, args: "<task title + one-line intent>")
```

The index returns which `reference/` and `workflow/` skills apply. Load each via the Skill tool before writing code. A typical task pulls one workflow skill plus one or more reference skills.

Emit one progress line per loaded skill per `progress-contract.md`, so the user can see which standards the run bound itself to rather than inferring it afterwards.

## Recording  -  what makes this checkable

Append one `state.telemetry.skillCalls[]` entry per skill actually loaded:

```json
{"skill": "ai-ios-toolkit:reference/architecture", "phase": 3,
 "targetFiles": ["Domains/Checkin/Sources/CheckinScene.swift"],
 "routedBy": "ai-ios-toolkit:index@0.13.0", "timestamp": "<ISO-8601>"}
```

`routedBy` names the index and version that chose it. That is the difference between "the model happened to read a skill" and "the toolkit said this skill governs this task".

What Phase 4 actually does with it, precisely: Step 1.78 lists these entries in the manifest under `ledger.routedByToolkit`, so a reviewer and the Phase 7 report can see which skills the project's own toolkit selected. It does **not** give them extra weight in the coverage maths. The deterministic resolver stays primary because an unrecorded load and no load are indistinguishable in state, and no `routedBy` tag changes that  -  the tag says who chose the skill, not that the code honoured it.

## Failure modes, and why none of them halt

| Situation | Behaviour |
|---|---|
| No toolkit for this stack | recorded no-op, continue |
| Toolkit not enabled in this repo | recorded no-op, continue (component dispatch still halts for its own case) |
| `index` resolves but routes to a skill that does not exist in this version | record the miss with the version, load the rest, continue. A stale row in a plugin's table must not stop a run |
| `index` itself does not resolve | record and fall back to the host's own description matching, which is the pre-v14.1.0 behaviour  -  no worse than before |

Nothing here blocks Phase 3. What is downstream is visibility, not enforcement: routed skills appear in the manifest's `ledger.routedByToolkit`, and a task that recorded nothing shows up as `ledgerSource: derived` with its coverage gap stated. Enforcement over rule IDs is the registry's job (`features/skill-conformance.md`), not this step's.

## What this deliberately does NOT do

- It does not decide which skills apply. Copying the plugin's routing into this repo would put the authoritative table in the wrong place and guarantee drift.
- It does not fail a run for a missing skill. The pipeline's contract is to ask and record, not to require that a third-party plugin be complete.
- It does not replace `component-dispatch.md`. That path owns the component build itself, including the `figma-validate` pre-check and the halt-on-incomplete-state rule.
