# Project skill discovery & intent vocabulary

W-mq1cczi90006b21f (follow-on to PR #82, W-mq16xtdx001a347e).

Minions' project-local **skill discovery layer** surfaces purpose-built tooling that a project ships (`.claude/skills/`, `.claude/commands/`, slash-command mentions in `CLAUDE.md` / `.github/copilot-instructions.md`) so dispatched agents steer toward the right skill instead of reinventing the flow from first principles.

This document is the contract every playbook author and project teammate needs to know.

## Where the code lives

| File | Role |
|---|---|
| `engine/discover-project-skills.js` | Bounded filesystem walk + classification heuristic. The crash-safe primitive. |
| `engine/playbook-intents.js` | Single source of truth for the playbook → intent set mapping. |
| `engine/playbook.js` | Calls discovery + filter at render time, injects `{{project_skills_block}}` into the rendered prompt. |
| `engine/discover-review-skills.js` | Thin re-export shim. Backward-compat only — new code uses `discover-project-skills.js`. |

## What gets discovered

Per project worktree, the discovery walk visits:

- `<projectRoot>/.claude/skills/*/SKILL.md` — top-level skill packs.
- `<projectRoot>/.claude/commands/*.md` — top-level slash-commands.
- `<projectRoot>/<area>/.claude/skills/*/SKILL.md` — **nested per-area skills** (one level deep). This is how OCM / loop / officemobile organize their skills in a monorepo.
- `<projectRoot>/<area>/.claude/commands/*.md` — same, for nested commands.
- `<projectRoot>/.github/copilot-instructions.md` — slash-command mentions (top 32 KB).
- `<projectRoot>/CLAUDE.md` — slash-command mentions (top 32 KB).

Each discovery entry has shape:

```js
{
  kind: 'skill' | 'command' | 'slash-command',
  name: string,                   // e.g. "code-reviewer" or "/review-swarm"
  path: string,                   // relative to projectRoot
  oneLineDescription: string,     // ≤200 chars
  intents: string[]               // closed enum, see below
}
```

The walk is **cheap and bounded** — preserve every guardrail in
`engine/discover-project-skills.js`:

- ≤ 50 files per surface
- 8 KB per skill / 32 KB per docs file
- 250 ms walltime budget — bails rather than blocks dispatch
- ≤ 50 nested areas probed
- Missing worktree / unreadable → `[]` (never throws)

## The intent vocabulary (CLOSED)

| Intent | What belongs here |
|---|---|
| `review` | code review, PR review, swarm review (`code-reviewer`, `/review-swarm`, `/code-review`, etc.) |
| `build` | scaffold, generate, bump, migrate, codemod, refactor (`bump-fmf-android`, `workspace`, `vhd`, etc.) |
| `fix` | debug, triage, repro, regression, flake-killing (`triage-flake`, `cover-pr` for missing tests on PR…) |
| `test` | author tests, increase coverage, validate (`integration-tests`, `run-tests`, `qa-validate`) |
| `plan` | PRD authoring, ADR, design-doc, decompose (`substitute-scheduler-template-vars`, `decompose`) |
| `research` | investigate, gather-impact, telemetry-query, audit (`gather-impact`, `audit-deprecated`) |
| `deploy` | release, publish, buddy-build, rollout, ship (`buddy-build`, `track-pr`) |
| `observability` | telemetry, logs, kusto, monitoring, metrics dashboards (`kusto-telemetry-lmf-quality-report`, `weekly-repo-summary`) |
| `meta` | meta-tooling about the skill system itself. **Do NOT use as a catch-all** — empty `intents: []` is the honest signal when the heuristic doesn't fire. |

### How intents are assigned

For each discovered entry, `classifyIntents(...)` does:

1. **Honor explicit author intent first.** A skill's `SKILL.md` frontmatter may declare `intents:` (or `tags:` / `intent:`) — only values that match the closed vocabulary above are accepted. Example:
   ```yaml
   ---
   name: bump-fmf-android
   description: Bump the FMF SDK version pin in OCM Android
   intents: [build, deploy]
   ---
   ```
2. **Fall back to keyword heuristics.** The skill's `name`, frontmatter `description`, first heading, and filename are matched against the per-intent keyword regexes in `INTENT_KEYWORDS` (engine/discover-project-skills.js). A skill matching multiple regexes carries multiple intents.
3. **Empty intents are honest.** Zero matches → `intents: []`. The entry is still discovered and returned, but no intent-filtered playbook will surface it. NEVER default to `meta` just to satisfy the field.

### Slash-commands scraped from docs

For `/foo` mentions in `CLAUDE.md` / `.github/copilot-instructions.md`, the discovery layer is more conservative: it only surfaces slash-commands whose **name itself** matches at least one intent keyword. This keeps doc-scrape noise out of every prompt (a doc may name 50 `/foo` commands; we don't want to dump them all into an `implement` prompt).

## The playbook → intent map

Defined in `engine/playbook-intents.js`. The default mapping at time of writing:

| Playbook | Intents surfaced |
|---|---|
| `review` | `['review']` |
| `fix` | `['fix', 'test', 'review']` |
| `implement` / `implement-shared` | `['build', 'test', 'research']` |
| `plan` / `plan-to-prd` / `decompose` | `['plan', 'research']` |
| `docs` | `['research']` |
| `verify` | `['test', 'review']` |
| `build-and-test` | `['build', 'test']` |
| `test` / `qa-validate` | `['test']` |
| `explore` | `['research']` |
| (all others) | `[]` — no block rendered |

When a playbook's intent set is empty (or maps to none), the `{{project_skills_block}}` template variable resolves to the empty string and the splice fence collapses cleanly (no stray header, no padding).

The `followup-dispatch` template **inherits** its parent dispatch's intent set via `vars.parent_dispatch_type` — there's no row in the map for it.

## Adding a new playbook

1. Add a row to `PLAYBOOK_INTENTS` in `engine/playbook-intents.js` mapping your playbook name to the intent set it should surface.
2. Splice the conditional template fence into your playbook markdown at the position where the agent should see purpose-built tooling — typically between the task-intake step and the first "actually do the work" step:
   ```md
   {{#project_skills_block}}
   {{project_skills_block}}

   {{/project_skills_block}}
   ```
3. If the playbook should opt out for some dispatches, the dispatcher can set `meta.skipProjectSkills: true` (or the PR-82 alias `meta.skipProjectReviewSkills: true`) on the work item.

## Adding a new intent

1. Add a regex to `INTENT_KEYWORDS` in `engine/discover-project-skills.js`. Use `\b` word boundaries.
2. Add the value to `INTENT_VOCABULARY` so explicit frontmatter `intents:` entries with the new name are accepted.
3. Add a row to the table above, with a one-line description of what belongs.
4. Update one or more playbook entries in `engine/playbook-intents.js` to include the new intent.

Keep the vocabulary tight — every additional intent is noise risk in every prompt that surfaces it.

## Completion-report contract

When an agent acts on a discovered skill, record the outcome in the completion report's `meta.skill` block. See [docs/completion-reports.md](completion-reports.md#project-skill-outcomes) for the full schema. The legacy `meta.review.skill*` fields (PR #82) remain accepted as aliases — new code should prefer `meta.skill.*`.

## Tests

- `test/unit/discover-project-skills.test.js` — discovery + classification + intent filtering + render + per-playbook splice coverage.

When you change discovery, classification, or the playbook→intent map, extend or update this test file.
