Files
holocron/docs/adr/0020-skill-description-and-body-context-contract.md
Defame1297 1c6eababb0 docs(adr): set a context contract for skill and agent authoring
Every installed skill's name+description is preloaded every session -
23,612 chars (~6,200 tokens) across 39 skills. The authoring rules
optimised for triggering reliability with no counter-pressure on size:
skill-author:104 and description-quality.md:21 both mandate padding,
while skill-author:102 (the rule that would deflate it) is
judgment-only and absent from description-quality.md's FAIL conditions.

ADR-0020 sets the description shape (trigger + one capability +
boundary), two-tier size gates sitting below the unchanged
agentskills.io ceilings, a mandatory dispatch pattern for multi-branch
bodies, a Gotchas constraint, an agent-side delegation check, and
invocation as a design axis. Gates ship blocking with no baseline.

Adds five CONTEXT.md glossary entries: preload tax, skill context
contract, dispatch body, hand-invoked skill, delegation discipline.

Refs: ADR-0020
2026-08-14 19:39:14 +00:00

240 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Skills and agents are authored against a context budget, not a spec ceiling
Every installed skill's `name` and `description` sits in every agent's context from the first token
of every session, whether or not the skill is ever invoked. Across this repo's 39 skills that is
23,612 characters — roughly 6,200 tokens — and the authoring rules that produced it optimised for
triggering reliability with no counter-pressure on size. This ADR sets the budget, the shape, and the
gates that hold them.
**Status: accepted (2026-08-14).**
## Context
Measured before any change:
| | |
|---|---|
| 39 skill `name` + `description` | 23,612 chars, ~6,200 tokens, **preloaded every session** |
| 4 agent `name` + `description` | 1,325 chars, ~350 tokens, preloaded every session |
| skill bodies | median 684 words, mean 815, p90 1,349 |
| `MAX_WORDS` gate (`skill-audit/scripts/validate.sh:147`) | **2,770** — 2× p90 |
Three findings drove this, none of which is "the descriptions drifted".
**The rules mandate the bloat.** `skill-author/SKILL.md:104` requires indirect triggers ("even if the
user doesn't mention X explicitly") and `skill-audit/references/description-quality.md:21` requires
authors to "err toward being pushy". Both are enforced. The one rule that would delete the waste —
`skill-author/SKILL.md:102`, "not the skill's internal mechanics" — is judgment-only and is absent
from the FAIL conditions at `description-quality.md:45-50`. The enforced rules inflate; the deflating
rule does not bite. The result is measurable: `gitea-files` spends 147 chars listing six verbs, then
301 chars re-quoting the same six as user phrasings, in the same order. `apm-workflow` does the same
with six capability clusters. Across the twelve longest descriptions, 30.7% is capability
enumeration and 11.6% is composition or implementation detail that cannot affect a routing decision.
**Capability enumeration in a description is a correctness hazard, not only a token cost.**
`docs/research/examples/skill-write/writing-skills/SKILL.md:154-158` reports a measured failure: "when
a description summarizes the skill's workflow, an agent may follow the description instead of reading
the full skill content. A description saying 'code review between tasks' caused an agent to do ONE
review, even though the skill's flowchart clearly showed TWO reviews." `git-commits` is exactly that
shape — 74% of its description is capability enumeration, including a rules table (`header max 100
chars, lowercase subject, no trailing periods, 11 standard types`) an agent can act on without ever
loading the body.
**The upstream sources cannot settle this.** The four skill-writing references under
`docs/research/examples/skill-write/` disagree on what a description contains — when-only
(`writing-skills/SKILL.md:99`), what-and-when (`skill-creator/SKILL.md:67`,
`anthropic-best-practices.md:187`), triggers-only (`writing-great-skills/SKILL.md:28`), and
what-plus-when-plus-negative (`write-skill/SKILL-TEMPLATE.md:5-6`). `writing-skills` and the Anthropic
document it bundles contradict each other inside one skill directory. They also disagree on whether
500 lines is binding, on the inline-versus-bundle threshold, and on the TOC threshold (>100 lines vs
>300 lines). "Grounded in the research" is therefore not available as a tiebreaker; a house choice is
required and this is it.
A fourth observation shaped the body half. The best progressive-disclosure ratio in the repo belongs
to `apm-workflow` — a 554-word body dispatching to 3,006 words of references — and the worst two
belong to the skills that define the house standard: `skill-author` (2,760 body / 1,247 references)
and `agent-author` (2,758 / 1,664). Both sit within twelve words of the 2,770 gate their own plugin
enforces. A ceiling that nothing approaches is not a constraint; a ceiling that two files have grown
into is a target.
## Decision
### Descriptions
A description carries three things and nothing else: a **trigger clause**, at most one **capability
clause**, and a **boundary clause**. Capability enumeration, output-format detail, composition notes
("composes X rather than duplicating Y"), and implementation detail move to the body or to
`README.md`.
- **250 characters SUGGESTION, 400 FAIL.** The agentskills.io 1,024-character limit remains as an
unchanged spec backstop. The SUGGESTION tier is what moves the average; the FAIL tier only stops
outliers.
- **Boundary clauses compress** to `Not <thing> → <skill-name>.` and must name a target that
resolves to a real skill under `plugins/*/.apm/skills/`. This is checked deterministically.
- **The blanket pushiness rules are deleted.** `skill-author/SKILL.md:104` and
`description-quality.md:21` are replaced by a conditional: add an indirect trigger only where the
user's natural phrasing genuinely omits the domain word — true for the `gitea-*` family, false for
`git-commits`. Stating the same trigger twice in two registers is a FAIL.
### Bodies
The body carries the **decision procedure only**: ordered steps, decision branches, gates, and which
reference to load when. Lookup tables, spec restatements, output schemas, templates, and rationale
prose move to `references/` behind an explicit "read X when Y" trigger.
- **600 words SUGGESTION, 900 FAIL.** The 2,770-word / 500-line spec backstop is unchanged and keeps
its existing meaning: conformance, not quality. These are two different gates measuring two
different things, and conflating them is what produced the current state.
- **Dispatch is mandatory at two or more mutually exclusive flows.** The body carries the dispatch
table and the gates that apply to every branch; each flow lives in its own self-contained
`references/` file. This is `apm-workflow/SKILL.md:33-41` promoted from accident to rule.
- **Gotchas are constrained.** A Gotcha must state a fact that contradicts a reasonable default —
something the agent gets wrong by acting sensibly. Maximum five entries. A Gotcha that paraphrases
a step in the body below it is a FAIL. A Gotchas section exceeding 25% of the body is a
SUGGESTION.
### Agents
Agents take the same description gates — they are preloaded identically — and **no body word gate**.
A skill body is loaded into the caller's context, competing with the live conversation; an agent body
becomes the system prompt of a fresh context. The rationale for the 900-word FAIL does not transfer.
A plugin-scope agent is a single file with no sibling `references/` directory, so it cannot disclose
to itself — it can only delegate to skills. `agent-audit` therefore gains a **delegation check**: an
agent body that restates a procedure owned by a skill it can invoke is a FAIL, with the fix being
"invoke `<skill>` instead". Length falls out of delegation rather than being gated directly.
### Invocation as a design axis
`skill-author` asks whether a skill is model-invoked or hand-invoked before writing a description. A
hand-invoked skill sets `disable-model-invocation: true` and carries one plain human-facing sentence
with no trigger list.
Verified end-to-end rather than assumed: `plugins/bin/.apm/skills/zoom-out/SKILL.md:4` carries the
flag, apm passes it through verbatim to both `.claude/skills/zoom-out/SKILL.md:4` and the flat mirror
at `plugins/bin/skills/zoom-out/SKILL.md:4`, and `zoom-out` is the one installed skill absent from
the model-visible skill listing in a live session. It remains invocable as `/zoom-out`.
### Merging siblings
Two skills that share substantial content, name each other as near-misses, and differ only in the
type of input they take should be **one skill with a dispatch table**. This catches `skill-audit` +
`agent-audit` and is scoped to them; the author pair is explicitly excluded, because
`skill-author` and `agent-author` emit genuinely different artifacts (a skill directory versus a
one-or-two-file agent pair, per ADR-0005 and ADR-0016) and their overlap is in the improve flow
rather than the core job.
### Enforcement and rollout
Gates land where the existing gates already live — no new layer:
| Check | Home |
|---|---|
| description and body counts, resolvable boundary targets | `skill-audit/scripts/validate.sh`, `scripts/skill-size-check.sh` |
| prose patterns (composition-note openers, restatement) | `plugins/kyberforge/.apm/skills/*/assets/vale/styles/Kyberforge/` |
| judgment calls | `references/description-quality.md`, `references/body-discipline.md` |
**Blocking immediately, with no baseline file.**
Three pre-existing contradictions are fixed in the same change, because they are the contract:
- `skill-audit/SKILL.md:58` asks whether the description opens with an action verb ("Audits…",
"Reviews…") while `:101` and `DescriptionOpener.yml` require an imperative "Use when…" opener. The
criterion is unsatisfiable against the house's own skills, both of which open with "Use when".
- `DescriptionOpener.yml`'s regex is anchored to `^This (skill|agent)\b`, so `gitea-workflow` ("This
is the human-facing entry point…") and `gitea-labels-milestones` ("This is a cross-cutting shared
skill…") both violate the rule and pass the linter.
- `description-quality.md:45-50` has no FAIL condition for internal-mechanics content, which is why
`skill-author/SKILL.md:102` never bit.
## Consequences
**Editing any non-compliant skill now requires retrofitting it first.** 30 of 39 descriptions exceed
400 characters and 14 of 39 bodies exceed 900 words. With the gate hot and no baseline, a one-line
fix to `gitea-prs` cannot be committed until that skill meets the contract. This is deliberate — it
guarantees convergence and avoids a half-state — but it means the retrofit is lazy and *mandatory*
rather than deferred. The follow-up retrofit issue should be prioritised accordingly, and the risk it
carries is the ordinary one for hot gates: a gate expensive enough to be inconvenient gets bypassed
with `SKIP=` and loses its authority.
**A ceiling does not produce an average.** If every author writes to the 400-character FAIL, the
preload lands at 15,600 chars — a 34% cut, not the ~50% intended. The halving depends entirely on the
250-character SUGGESTION tier being visible and respected. That tier works here in a way it does not
elsewhere in this repo: `skill-audit` already reports `PASS (N suggestions)` as a first-class
outcome. This is explicitly **not** the failure ADR-0013 records — Vale warnings are invisible
because vale's exit code keys on `error` alone, but these gates live in `validate.sh` and
`skill-audit`, where a SUGGESTION reaches the report. Realistic landing is 34-55% down, not a
guaranteed 50%.
**A word gate cannot detect the defect it is standing in for.** `git-commits` carries thirteen
Gotchas of which four restate steps in its own Workflow (`:32` ≡ step 9, `:33` ≡ step 9, `:36` ≡ step
2, `:31` ≡ the description) — 1,217 words that pass any plausible gate. The counts are a backstop to
the dispatch rule and the Gotchas constraint, not a substitute for them, and should not be read as
the mechanism.
**Some skills legitimately need more description budget than others.** A tiered limit keyed to
sibling density was considered and rejected as too clever; the flat 250/400 pair means the `gitea-*`
and `git-*` families — where every sibling shares a keyword and boundary clauses do real routing work
— are the ones most likely to sit at the FAIL tier permanently. If the retrofit shows that family
routing degrades, the tier is the first thing to revisit.
**Four broken routing targets are live and are not fixed here.** `skill-audit` routes to
`/skill-improve` twice in its description plus `README.md:10`, and no such skill exists — the real
target is `skill-author`. `research` routes to `neuledge-context`, which exists only inside that
string. `agent-author` says "Do not use for read-only review — examine agent files manually",
routing away from `agent-audit`, the correct sibling. `gitea-issues` contains the literal string
`gitea-labels- milestones`, a stray space introduced by YAML folding, breaking the skill name in
preloaded text. The resolvable-target check added here will fail on all four the moment those files
are touched; fixing them is split into its own issue.
**Duplication between `skill-author` and `agent-author` survives un-gated.** The merge rule
deliberately excludes the author pair, so the commit-verification argument in four near-copies, the
root-cause grouping rule in four copies, and the wholesale clone of the "Improving an existing X"
flow all remain. Cache isolation makes them structurally unavoidable
(`skill-audit/SKILL.md:95` forbids cross-skill references; `LESSONS.md:107` records why), so the
options are a sync gate or continued drift. This is an input to the kyberforge-bodies follow-up
issue, not a solved problem.
**Provenance frontmatter is explicitly out of scope.** `LESSONS.md:63` asserts that non-routing
frontmatter (`source_keys`, `category`, `version`) is loaded at agent startup, which would make the
7,352 characters of it across the corpus a third again on top of the description tax. Measured
against a live session on this Claude Code version, it is not: the model-visible skill listing
contains only `name` and `description`. That is host-observed rather than spec-guaranteed and says
nothing about Copilot CLI, but it is sufficient to establish that cutting `source_keys` would break
the ADR-0009 provenance machinery for no runtime gain. The metadata was added deliberately and stays.
## Alternatives considered
- **Keep pushiness, raise the budget to ~500 chars.** Undertriggering is the worse failure mode — a
skill that never fires is worth nothing regardless of cost — and `skill-creator/SKILL.md:67`
explicitly recommends being "pushy" against an observed undertriggering tendency. Rejected because
that claim is an unmeasured assertion about an older model, and because the correctness hazard in
`writing-skills:154-158` cuts the other way: a fat description is not merely expensive, it is a
shortcut agents take instead of reading the body. Would have landed a 35% cut.
- **A trigger-eval loop to set lengths empirically.** `skill-creator/SKILL.md:337-404` specifies 20
queries per skill, 8-10 positive and 8-10 near-miss, with a 60/40 train/test split selecting on
test score. This is the rigorous answer and the repo has deliberately never built it. Rejected
because it blocks the context cut behind a substantial new subsystem.
- **A repo-level aggregate preload budget** (≤12,000 chars across all skills, checked at pre-push).
The only option that measures the actual goal rather than a proxy. Rejected because it makes one
skill's edit fail on account of another skill's growth, and because it is meaningless for an
external consumer installing a subset of the plugins.
- **500-word body FAIL, matching `writing-skills/SKILL.md:217-221`.** Best-grounded in upstream and
would align this repo with the tightest source. Rejected because it fails 30 of 39 skills, and a
blunt gate gets satisfied by deleting content rather than relocating it.
- **A shrinking baseline file** recording each non-compliant skill's current numbers, failing only on
growth. Would have made the retrofit a visible burn-down instead of a wall. Rejected in favour of
hot gates.
- **A sync gate over the duplicated spans** instead of a merge rule — generalising
`scripts/check-vale-style-sync.sh` to cover shared prose so duplication persists but drift cannot.
Rejected for the audit pair in favour of merging, which removes the duplication rather than
policing it, and removes a mutually-excluding near-miss pair from the router at the same time. It
remains the only available answer for the author pair.
- **Merging `skill-author` + `agent-author` as well**, taking kyberforge from seven skills to five.
Largest cut available. Rejected because it reopens ADR-0005, ADR-0008 and ADR-0016 together, and a
merged author skill would carry both the skill-directory scaffold and the dual-provider agent
scaffold behind one dispatch.
- **Demoting Gotchas** to the end of the body or into `references/gotchas.md`, removing its
position-based exemption from the dispatch rule. Maximum saving on the largest body construct
(6,830 words, 21% of all body text). Rejected because a gotcha read after the mistake is worthless.