# Skill authoring conventions

Rules for every skill under `plugin/skills/`. Read before adding or editing one.

## One-way dependencies — no reverse references

Skill cross-references form a one-way graph. If any file in skill **A** references skill **B** — a markdown link to B's files, or a documented dependency on B's internals — then **no file in B may reference A back**. Reference *downward*, toward the more general / lower-level skill you depend on; pass anything the other direction needs as input, not as a link.

A reverse reference (A → B and B → A) couples the depended-on skill to its caller, creates a cycle no one can reason about in isolation, and makes every edit ripple both ways. The lower-level skill must stay reusable by callers it has never heard of.

**Runtime dispatch is not a doc reference.** A dumb-pipe skill may *fire* another skill's slash command at runtime (hand off and forget) — that is an action, not a dependency. What the rule forbids is a procedure file **linking to** or **encoding the internals of** the skill it hands off to.

**Worked example.** `muggle-pr-followup` (the dumb-pipe watcher) is lower-level than `muggle-do` (the executor that orchestrates it). `muggle-do` references `muggle-pr-followup`; `muggle-pr-followup`'s files must not link back to `muggle-do`. A watcher tick still dispatches `/muggle-do …` at runtime — allowed — but no watcher file links a `do/` file or restates its steps, and shared primitives like `muggle-pr-followup/finalize.md` stay dispatch-free so any caller can reuse them.

When you feel the urge to link "up" to a caller, that is the smell — restructure so the caller passes what is needed in.

### Enforcement

`scripts/check-skill-deps.mjs` derives the cross-skill link graph and fails on any cycle. A "reference" is a markdown file-link into another skill's directory — runtime slash-command dispatch is not a link and is not counted. It runs three ways: the `skill-deps` CI job on every PR, a `PreToolUse` hook (`.claude/settings.json`) that blocks the write mid-session with the offending link named, and `pnpm run verify:skill-deps` locally.

`plugin/skills/skill-deps.config.json` declares support dirs grouped into their owning skill (`do/` → `muggle-do`), shared namespaces exploded to per-file nodes (`_shared`), and `knownReverseDeps` — pre-existing violations grandfathered so CI stays green. That list is debt: fix each link and delete its entry. A new reverse dependency is blocked whether or not it is on the list.

## Mandatory stages

A `SKILL.md` links out to the files that carry its steps, and nothing forces those links open. Read as a single page, a skill silently loses whatever its stage files required — the step is improvised instead of skipped visibly.

Declare the files a skill cannot be run without in its frontmatter, paths relative to the skill directory:

```yaml
mandatoryStages:
  - ../_shared/failure-mode-handling.md
  - ../_shared/debug-failed-run.md
```

Invoking the skill names those files as required reading, and a Stop gate holds the turn open until each is opened (`guardrail-skill-stages.sh` / `guardrail-stage-gate.sh`, see [`../hooks/README.md`](../hooks/README.md)). The declaration is explicit rather than inferred from the prose: a gate guessing which links matter blocks on the wrong ones.

The bar is "must be **read** before the skill runs", not "must be executed". A failure protocol qualifies: by the time a run fails, a skill that never read it is already improvising. What does not qualify is a file only one branch consults for its content — the gate cannot tell that the branch was not taken, so it would block work that legitimately never needed it. Aliases declare nothing; they invoke the canonical skill, which carries the declaration.

A skill whose router sends runs down different routes declares per route. Its `SKILL.md` lists only what every run opens — the router — and each route's entry file carries its own `mandatoryStages` frontmatter, paths relative to that file. Opening the entry file while the skill runs adds its set; a `SKILL.md` declares only when invoked, never when read. A route lists only the files every run of that route opens, so a branch inside a route still stays out.

## Host compatibility

Skills are written against Claude Code's tools and ship unchanged to Codex. [`_shared/host-tool-equivalents.md`](_shared/host-tool-equivalents.md) maps each Claude Code name a skill uses — `AskUserQuestion`, the `Skill` tool, `${CLAUDE_PLUGIN_ROOT}`, the PR watcher's monitor — to what plays its part in Codex. Every top-level `SKILL.md` links it right after its telemetry line, and the Codex SessionStart hook names it too.

When a skill comes to depend on another Claude Code-only tool, add its row there; never branch a skill's steps per host. An alias also carries `agents/openai.yaml` with `allow_implicit_invocation: false`, which is how Codex reads `disable-model-invocation`. `src/test/skills/codex-skill-compat.test.ts` holds all three.

## Model tiers

Each skill sets a `model:` in its `SKILL.md` frontmatter sized to its cognitive load. `model:` is a native Claude Code field — the override applies while the skill is active and reverts to the session model when it exits. Cheaper, faster models run the mechanical skills; the default (Opus) is reserved for the ones that actually reason. Cost and latency scale with the model, and these skills run often, so the tier is a real lever, not cosmetics.

| Model | Skills | Why this tier |
|-------|--------|---------------|
| `haiku` | `muggle`, `muggle-status`, `muggle-repair`, `muggle-upgrade`, `muggle-preferences`, `muggle-feedback`, `muggle-init`, `muggle-ci-install`, `muggle-login`, `muggle-logout`, `muggle-run-view`, `muggle-cleanup` | Routers and dumb pipes. They follow an explicit procedure with no open-ended reasoning: route intent to a downstream skill, run a fixed CLI sequence, CRUD a config file, or format a status report. |
| `sonnet` | `muggle-pr-visual-walkthrough` (executes via the `visual-walkthrough-builder` agent), `muggle-test-regenerate-missing`, `muggle-pr-followup` | Multi-step orchestration with light judgment, short of deep reasoning: assemble run data and build a PR section with fit-vs-overflow handling; scan, filter, bulk-dispatch, and classify per-item failures into buckets; run a watcher tick across a dozen procedure files and fire the one `/muggle-do` directive its verdict calls for. More moving parts than a router, but each step is well-defined. The watcher moved up from `haiku`: its tick eval passed 3–4 of 23 scenarios there, describing the procedure instead of running it, and a tick now runs only when its token-free watch loop exits with something to act on. |
| `opus` (explicit pin) | `muggle-test-prepare` (executes via the `test-prepare-runner` agent) | Pinned for **reliability**, not raw reasoning load: it's flaky on smaller models, and since other skills gate on the environment it readies, a wrong call is expensive. Pin explicitly rather than leaving `model:` unset so it stays on Opus even when the user's session runs a cheaper model. |

| default (Opus) — no `model:` set | `muggle-do`, `muggle-test`, `muggle-test-feature-local`, `muggle-browser-task`, `muggle-test-import` | Reasoning-heavy. Authoring code to a PR, mapping a code diff to affected user flows and interpreting E2E results, reasoning about an arbitrary website's flow to drive a browser, translating Playwright/Cypress/PRD artifacts into Muggle test cases. Leave `model:` unset so the skill inherits the session model. |

**A `SKILL.md` `model:` only bites when the skill starts a fresh session; invoked mid-session the session model keeps running.** A skill whose pin must hold regardless resolves its user interaction in the `SKILL.md`, then dispatches a `plugin/agents/*` agent that carries the pin — the harness applies an agent's `model:` on every dispatch. That is why `muggle-pr-visual-walkthrough` and `muggle-test-prepare` execute through agents. Agents have no user channel; an unresolved decision comes back as `needs-input:` for the skill to resolve.

**Choosing a tier for a new skill.** Ask what the skill actually does. Pure routing / fixed procedure / CRUD / reporting → `haiku`. Several well-defined steps with some judgment or classification → `sonnet`. Open-ended reasoning, code authoring, or interpreting ambiguous real-world state → leave `model:` unset (Opus). When unsure between two tiers, pick the cheaper one and watch for misbehavior — the likeliest to need a bump is anything doing AI-based classification. If a skill proves flaky on its tier and reliability matters more than cost (other skills depend on it, or a wrong call is expensive), pin it up explicitly — `model: opus` — rather than leaving it unset, so the floor holds regardless of the user's session model.

**Never set `model:` on aliases or commands.** The alias skills (`m`, `mstatus`, …) and `plugin/commands/*.md` are thin routers that re-invoke the canonical skill via the `Skill` tool. The canonical `SKILL.md`'s `model:` takes effect once it loads, so a model on the alias would only apply to the negligible one-line hand-off — and risks drifting from the canonical value.
