# JIT plan — model-invoked skills get their operative body (backlog item #8)

**Scope.** Claude Code routes to a skill two ways: a leading slash-command (UserPromptSubmit sees
the prompt) and a **description match through its `Skill` tool** (`Skill(skill: "sptc:knock")`).
Only the first reaches `skill_key`, so every model-invoked `/sptc:*` invocation gets the thin
skeleton and no `<sptc_skill>` body — all 12 skills, since v0.2.x. Fix the second path at the
PreToolUse seam. Adapter-binary change only; the plugin skeletons are untouched, so this release
does **not** take the cplugs skeleton step.

## Measured before building (this node, 2026-08-21)

1. **Payload shape — OBSERVED.** Three field transcripts carry model-invoked calls:
   `{"skill": "sptc:commune", "args": "--across"}`, `{"skill": "sptc:knock"}`,
   `{"skill": "sptc:knock", "args": "mint a mutual (two-way) invite code…"}`. The name is always
   PLUGIN-QUALIFIED (`sptc:<id>`); `args` is free prose, not a flag string.
2. **PreToolUse fires on the `Skill` tool — OBSERVED, and it took two probes.** The first probe
   (a bare tag whose confirm rides PreToolUse) produced NOTHING on the Skill call and confirmed on
   the next Bash call — which reads like "the hook never fired" but cannot distinguish that from
   "the hook fired before the tag reached the transcript". The discriminating probe used a SPOOLED
   inbound message (no transcript timing in the path): sent, then a Skill call, and the message
   was delivered ON the Skill call under a `PreToolUse:Skill` header. The hook runs; it simply
   never injected the body. The inconclusive probe is recorded here so the conclusion is not read
   back as one clean measurement.
3. **The bug itself — REPRODUCED live.** A model-invoked `sptc:version` in this session returned
   the skeleton with no `<sptc_skill name="version">` block.
4. **No double-injection risk.** Every observed `Skill` call was model-initiated; a TYPED
   `/sptc:*` still arrives as a prompt (UserPromptSubmit) and does not route through the tool. So
   no cross-hook suppression state is needed — and none is added, since a stale suppression record
   would recreate exactly the silent-no-op class this fixes.

## Design

- `skill_key_from_tool(tool_name, tool_input.skill)` → skill key, empty otherwise.
  - `tool_name` must be exactly `Skill`.
  - The value must carry the `sptc:` qualifier. A BARE name is deliberately NOT matched: a user's
    own project skill called `send` or `version` would otherwise pull our body into a foreign
    skill. Our skills are only ever offered to the model qualified.
  - The tail must be a whole member of `SPTC_SKILLS` (same drift guard as the bare-shortname arm).
- Injected in `handle_pre_tool_use`, and placed **before both early returns**:
  - before the subagent gate — that gate exists to stop a subagent stealing the PARENT's inbound
    mail (REQ-HAZARD-SUBAGENT-DRAIN-STEAL). A body for the skill the subagent itself just invoked
    is not the parent's mail, and the Skill tool is the ONLY way a subagent can reach an sptc
    skill at all;
  - before the identity gate — `/sptc:version` and `/sptc:setup` are valid with no perch, exactly
    as the UPS path already reasons.
- The body LEADS the envelope (mirrors UPS: body, then deliveries), and rides the existing
  `emit_capped_pretool` cap/spill path.
- `live` composes per substrate through `inject_skill_for` (`$SPT_ENDPOINT_ID` = spt-hosted),
  same signal UPS uses.
- NOT replicated from the UPS path, deliberately: the `role` tagged-input round-trip (mode A) and
  the `ready`/`live` late-activation bringup override. A model-invoked `role` gets the
  agent-mediated body (mode C), which is the correct arm when no operator typed the command.

## Tasks

1. `traceable-reqs.toml`: mint + activate `REQ-SKILL-TOOL-INJECTION` (`impl`, `unit`).
2. `tools/claude-spt/src/hook.rs`: `skill_key_from_tool` + the PreToolUse leg.
3. Unit tests: qualified match, bare name refused, foreign plugin refused, unknown id refused,
   non-Skill tool refused, subagent still gets the body while its inbound drain stays skipped,
   perchless session still gets the body.
4. `docs/KNOWN-HAZARDS.md`: fold into the skill-injection-silent-no-op family (a second routing
   path that no-ops silently while the transcript reads healthy).
5. Version bump + CHANGELOG, `sh ci/run-gates.sh` PASS + `traceable-reqs check` exit 0, commit.
6. Release per `docs/RELEASE-RUNBOOK.md` (3 triples FRESH, pack, tag, gh release, public mirror).
   No cplugs skeleton step.
7. Field-verify on this node after `spt adapter update claude-spt`: model-invoke `sptc:version`
   and read for the `<sptc_skill name="version">` block.

## Gate

`sh ci/run-gates.sh` PASS and `traceable-reqs check` exit 0 BEFORE the commit lands.
