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
This commit is contained in:
2026-08-14 19:39:14 +00:00
parent 2e395a4efa
commit 1c6eababb0
2 changed files with 254 additions and 0 deletions

View File

@@ -0,0 +1,239 @@
# 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.