Six defects, each one a place where two files that an author reads in the same sitting told them different things — or where the trim dropped a rule and nothing noticed because no gate covers prose. **"Use proactively" contradicted itself across the pair.** All three agent templates said to add it where the runtime should delegate unprompted, while `agent-audit`'s `KyberforgeCopilot.ProactivePhrase` rule grades it a hard FAIL in any `*.agent.md` — which is the Copilot half of every project/user pair *and* the vendor-neutral plugin-scope file, since that compiles to a real Copilot agent downstream. Following the template produced a file the repo's own gate rejects. The phrase is now permitted in exactly one place, the Claude Code `.md`, and `references/contract.md` carries the per-file table plus the consequence authors ask about next: a pair whose CC half has it and whose Copilot half does not is correct, because `agent-audit` checks that both halves describe the same job, not that they match word for word. **The output-schema rule contradicted itself inside one file.** `contract.md` said any content only one branch reaches moves to `references/`, and then offered an "Output format template" body pattern with no qualification. Stated once now, so it is not re-litigated: an output schema stays in the body only when every flow produces it and it is roughly 50 words or less. No third option. **Gotchas tiers disagreed with the script.** `validate.sh` emits the entry count through `suggest()` and exits 0, while `skill-author` and `skill-audit` both called more than five entries a FAIL. Whether a given gotcha earns its place is judgment, so the prose moves to the script's tier rather than the reverse. The paraphrase rule stays a FAIL and is explicitly marked as the auditor's call — no script detects it. **The dispatch exemplar was cited at the wrong number.** `apm-workflow`'s body is 421 words; 554 is its whole-file count. Both `contract.md` and `body-discipline.md` cited 554 while describing a body budget, so an author calibrating against the exemplar overshot by ~30% — the exact whole-file/body-only conflation those two sections exist to warn against, reproduced inside the warning. **"Error handling" came back as a required body element.** It was one of four and is the one that gets dropped, and dropping it is not neutral: an agent handed malformed input with no instruction invents a recovery, and a subagent's invented recovery is invisible to its caller until the output is wrong. Restored in `agent-audit`'s rubric as a SUGGESTION, in `agent-author`'s contract and both scope checklists as a required element, and as an `## Errors` section in all three templates. **`skill-author` Step 4 gains the one check the audit misses.** An empty body reports `PASS SKILL.md body word count 0` — a word gate cannot tell "concise" from "absent". Step 4 now hand-checks for a non-empty section, and its commit verification is conditioned on actually being inside a git worktree, which a skill under `~/.claude/skills/` is not. Also here: absolute repo paths removed from `skill-author`'s SKILL.md and contract.md in favour of naming the skill (`zoom-out`'s description is quoted inline instead of pointed at), the boundary-target universe documented to match the resolver, a two-hops-from-SKILL.md limit on reference chains, and `new-agent.sh`'s next-steps output naming the description budget and the deliberate absence of an agent body gate. Refs: ADR-0020
9.6 KiB
source_keys
| source_keys | |||
|---|---|---|---|
|
The description and body contract
House contract, set by ADR-0020. Every rule here is enforced by /skill-audit —
scripts/validate.sh for the counts and the boundary targets, the bundled Vale styles for the
prose patterns, and its reference files for the judgment calls.
Why the budget exists
A skill's name and description are loaded into every agent's context at the start of every
session, whether or not the skill is ever invoked. The body is loaded only on invocation, and then
competes with the caller's live conversation. Those are two different costs, so they get two
different ceilings — and a fat description is not merely expensive. A description that summarizes
the workflow gets followed instead of the body: a description saying "code review between tasks"
produced one review from a skill whose flowchart specified two.
Description
A description carries exactly three things:
- Trigger clause — when to invoke, imperative: "Use when ...", never "This skill ...". Focus on user intent, not the skill's internal mechanics.
- At most one capability clause — what it does, one clause, no enumeration. Be specific ("parses and validates OpenAPI specs", not "helps with APIs").
- Boundary clause — form:
Not <thing> -> <skill-name>.Add one only where a near-miss skill could steal activations.
Banned from a description; move it to the body or to README.md:
- Capability enumeration or feature lists
- Output-format detail ("Produces a compact findings report with Why and Fix per finding")
- Composition or architecture notes ("composes X rather than duplicating Y", "This is a cross-cutting shared skill", "the human-facing entry point")
- Implementation detail ("Self-validates via a bundled deterministic script")
- Restating the same trigger twice in two registers — a verb list, then the same verbs re-quoted as user phrasings. This is a FAIL, not a suggestion.
Indirect triggers are conditional, not mandatory. Add "even if the user doesn't mention X
explicitly" only where the user's natural phrasing genuinely omits the domain word — true for the
gitea-* family, because people say "create an issue" rather than "create a Gitea issue"; false
for git-commits, where the user says "commit". Adding one everywhere is what inflated this
corpus, and it was deleted as a blanket rule.
Boundary targets must resolve. Both forms are checked — the arrow and the prose form ("do not
use for X, use y instead") — so a typo dangles either way. Targets resolve against a universe
built by walking up from the SKILL.md itself: the nearest ancestor holding
plugins/*/.apm/{skills,agents} (or, failing that, the nearest ancestor holding .git) contributes
every skill and agent under <root>/plugins/*/, plus the skill's own apm package and the packages
that package declares in apm.yml under dependencies.apm. A sibling plugin in the same monorepo
therefore resolves; a skill in an unrelated repo does not. A boundary clause naming a target
outside that universe sends the router nowhere and fails the audit. Check the target exists before
writing it — do not invent a plausible sibling name.
Length. 250 characters SUGGESTION, 400 characters FAIL, counting the frontmatter value only with YAML folding resolved. The agentskills.io 1,024-character spec limit is unchanged and sits above both. The SUGGESTION tier is the one that moves the average; treat 250 as the target and 400 as the outlier stop.
Hand-invoked skills are exempt. A skill carrying disable-model-invocation: true is absent
from the model-visible listing and is reached only by the user typing /name. It takes one plain
human-facing sentence — no trigger clause, no boundary clause, no indirect triggers. Worked
example — the whole description of the zoom-out skill, which carries disable-model-invocation:
Tell the agent to zoom out and give broader context or a higher-level perspective. Use when
you're unfamiliar with a section of code or need to understand how it fits into the bigger
picture.
Body
The body carries the decision procedure only: ordered steps, decision branches, gates, and
which reference to load when. Everything else moves to references/.
Ask of every sentence: "Would the agent get this wrong without it?" Cut anything that answers "no."
Include:
- Non-obvious sequences or ordering constraints — the agent may skip or reorder steps without this
- Domain conventions the agent cannot infer from general knowledge — the core value a skill adds
- One default per decision point, plus one escape hatch — never a menu; menus cause the agent to pause or pick arbitrarily
- Gotchas — facts that defy reasonable assumptions
Exclude:
- Concepts the agent already knows (what JSON is, how HTTP works) — tokens without behavior change
- Exhaustive option lists — pick a default; the agent does not benefit from choosing
- Steps the agent handles independently — over-specifying leads agents down unproductive paths
- Restatements of the description — it is already in context
Move to references/: lookup tables, spec restatements, output schemas, templates, example
blocks, rationale prose, and any content only one branch reaches. Each reference file is
self-contained for its concern, and every one is wired from the body with the literal conditional
form:
The one exception, stated once so it is not re-litigated: an output schema stays in the body
only when it applies to every flow and is short — roughly 50 words or less, which is the "Output
format template" pattern below. An output schema that is longer than that, or that only one flow
produces, moves to references/ like any other schema. No third option exists, and the two rules
do not disagree.
If <condition>, read `references/<file>.md`.
A generic pointer ("see references/ for details") is a Vale error — the agent cannot act on it.
Dispatch is mandatory at two or more mutually exclusive flows. The body carries the dispatch
table and the gates common to every branch; each flow gets its own self-contained references/
file. Exemplar: the apm-workflow skill — a 421-word body dispatching to 3,006 words of
references. Calibrate against 421: that file's whole-file count is 554 words, and aiming at that
number instead overshoots the body budget by ~30%.
Length. 600 words SUGGESTION, 900 words FAIL, counting the body only — everything after
the frontmatter's closing ---.
Gotchas section
- Each entry must state a fact that contradicts a reasonable default — something the agent gets wrong by acting sensibly. "Never commit secrets" is not one; the agent already knows.
- More than five entries is a SUGGESTION — five is the guideline, not a ceiling.
- A Gotcha that paraphrases a step in the body below it is a FAIL. If the rule is already a step, it is not a gotcha.
- A Gotchas section exceeding 25% of the body is a SUGGESTION.
- Place the section near the top — a gotcha read after the mistake is worthless.
Two size gates, two measurements
| Gate | SUGGESTION | FAIL | Counts |
|---|---|---|---|
| description | 250 chars | 400 chars | the description: value only |
| body | 600 words | 900 words | the body only, after the closing --- |
| spec backstop | — | 1,024 chars | the description: value only |
| spec backstop | — | 2,770 words / 500 lines | the whole file, frontmatter included |
The 600/900 pair and the 2,770/500 pair are not the same measurement and must not be unified: the first is a quality gate on what the caller's context absorbs, the second a conformance backstop on the file. A skill can sit well inside one and fail the other.
When a body approaches its ceiling, relocate rather than delete — move reference material to
references/<topic>.md behind a conditional trigger, and bundle repeated executable logic into
scripts/ rather than reinventing it each run.
Body patterns
Default with escape hatch (not a menu):
Use <X> for <task>. For <edge case>, use <Y> instead.
Prescriptive sequence (when order is critical or fragile):
Run exactly:
```bash
<command>
```
Do not modify flags.
Checklist (multi-step workflows):
- [ ] Step 1: ...
- [ ] Step 2: ...
Dispatch table (two or more mutually exclusive flows):
| Condition | Flow | Reference |
|---|---|---|
| <condition> | <flow> | `references/<file>.md` |
Output format template (when the skill produces structured output on every flow, and the
schema is roughly 50 words or less — see the exception under Body above; anything longer or
flow-specific belongs in references/):
Output format:
```
<field>: <value>
```
For longer templates, place them in references/<topic>.md or assets/<name>.md and reference
conditionally.
Embedding org-specific policy
If a skill encodes a rule sourced from an org convention file (e.g. core/instructions/*.md),
inline that content directly into the skill (SKILL.md or a references/ file) rather than pointing
to the file's path. Plugins must be self-contained and portable — the org file may not exist
wherever the plugin is installed, and in this repo such files are meant to be deleted once their
content is fully embedded downstream. Tag the inlined content with a source_keys entry using the
same references/sources.md schema as the create flow's Step 6, noting in the Research doc:
field that the source is an org convention rather than a plugin research corpus entry, so
provenance survives after the source file is gone.