* test(#4740): pin the per-step role-family partition Failing-first coverage for the Loop Host Contract role partition. At this commit crossCheckRoleFamilies does not exist, so the rows throw "crossCheckRoleFamilies is not a function" -- the RED proof they bind to behavior rather than restating it. ADR-894 section 3 assigns roles per step but parenthesises the assignment as "(illustrative roles)", and nothing enforced it. The only thing standing in the way was a single deepEqual in this same file, which is editable prose. Rows cover: each step's own family accepted; a strict subset accepted; a foreign role rejected at every step; an unknown role rejected; an unknown step failing CLOSED; capitalization not silently matched; every offending role reported rather than only the first; and purity, because buildContract puts the same array into the generated contract. Two rows exist because an earlier cut of this suite was vacuous. The purity fixture is deliberately UNSORTED -- an alphabetically-sorted fixture cannot fail an in-place sort(), and the mutant was being killed by three unrelated rows instead. A parity row asserts ROLE_FAMILY and ROLE_TO_AGENT cover the exact same role-name domain, both directions: they are parallel constants over one domain, so divergence is the generative-fix class CLAUDE.md names. Every negative row asserts the offending ROLE NAME and the STEP NAME appear in the message. A count-only assertion survives a mutant that reports the wrong role, which the 80% Stryker gate would surface only after a full CI round-trip. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(#4740): reject a cross-family agent-role declaration Orchestration and execution are distinct functions of the loop and must not drift into one another. That partition was real but unenforced: ADR-894 section 3 calls its own role assignment "illustrative", and the generator accepted anything. Adding orchestrator to execute-phase.md's agent-roles line compiled, --check passed once regenerated, and capability-validator.cjs then began accepting into:"orchestrator" at every execute point. ROLE_FAMILY maps every role to one of orchestration, planning or execution. EXPECTED_FAMILY_BY_STEP gives each of the five steps exactly one family. crossCheckRoleFamilies rejects a cross-family role, a role outside the vocabulary, and an unknown step. It reports every offender, not the first. It fails CLOSED on an unknown step, deliberately diverging from assertPointsCoverage's "unknown step -- caught elsewhere". For points that is true: the canonical-set and duplicate checks catch it. For roles there is no second net, so failing open would leave an unknown step as the one input that bypasses the gate. crossCheckRoles' orchestrator exemption is untouched. ROLE_TO_AGENT maps roles to agent FILES and the orchestrator is the host, owning none -- admissibility and agent-file presence are separate concerns with separate checks. Additive to section 3's existing rule that contribution.into must be a member of the step's agentRoles, which is unchanged. That governs what a CAPABILITY may target; this governs what a WORKFLOW may declare. No capability is affected, and all five workflows already declare single-family sets, so the gate is green on the commit that introduces it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(#4740): make the ADR-894 role assignment normative Section 3 parenthesises its per-step role assignment as "(illustrative roles)". That word was accurate about the list's PURPOSE -- it illustrated the shape of a generated contract entry -- and wrong about its STATUS, because the assignment was load-bearing from the moment the generator consumed it. Read literally it makes the partition an example rather than a rule. Appended as a dated in-place section per docs/contributor-standards.md, which records that an accepted ADR is never rewritten and names this the default pattern. Section 3's original body is untouched. The amendment states the three disjoint families, the one family each step admits, that a step may declare a strict subset but never outside it, and why this is a clarification rather than a new decision: the contract is generated from the workflow markers "so it cannot drift into a lie", and all five workflows have always declared single-family sets. What was absent was any statement that it is required, and any check that it holds. It also pins the distinction that is easy to re-merge: contribution.into being a member of agentRoles governs what a CAPABILITY may target and is unchanged; the family rule governs what a WORKFLOW may declare. The CONTEXT.md glossary entry for the Loop Host Contract records the same, beside the agent-reference drift guard it already documented. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(#4740): add changeset fragment pr:0 placeholder is backfilled with the real number once the PR exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(#4740): backfill changeset pr number Replaces the pr:0 placeholder with 4742 now that the PR exists. Verified with GITHUB_BASE_REF=next, the way CI runs them: changeset lint and lint:docs both go from invalid_pr(0) to ok. Without that env both report success without evaluating the branch at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#4740): stop injecting the orchestrator procedure into executors claude-orchestration declared a contribution at execute:wave:pre with into:"executor". loop-hook-dispatch.md defines a contribution as "inject fragment.inline verbatim into the context for the role named in into", so its 267 lines were injected into EXECUTOR prompts whenever the capability was enabled. Those lines are orchestration end to end -- construct a wave manifest, resolve the dispatch backend, invoke the Workflow tool to spawn executors, bridge per-agent results into the merge chain. An executor can act on none of it. Retargeting to into:"orchestrator" would not have been a fix. ROLE_TO_AGENT carries no orchestrator entry by design: the orchestrator IS the host, and the host's procedure lives in execute-phase.md. A step's agentRoles enumerates agents a capability may inject context INTO, so adding orchestrator there would model the host as an injectable agent -- the same category error pointed the other way, and it would need an exception carved into the partition the same issue just made normative. So the defect is the mechanism, not the label. A contribution injects into an agent's context; "replace step 3's inline dispatch loop" is a change to what the HOST does. The contribution channel was serving as a host-behaviour directive because it was the only channel available at an execute point. The entry is removed. plan:post into:"planner" is correct and untouched. The procedure is preserved verbatim at docs/workflow-backend-dispatch.md inside the capability -- it is the only copy in the repo -- and is no longer injected anywhere. Consequence, not softened: the Workflow backend now has no loop wiring. Detection, emission and config remain and the design is intact, but nothing dispatches it. Under the separation ADR-1143 itself asserts it never had a legitimate channel; ADR-1143's own audit already records the end-to-end path has never been exercised. Wiring it properly needs a host-level mechanism that does not exist today. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#4740): invert the stale execute:wave:pre registry assertions Removing the contribution left four surfaces asserting or describing the old state. Caught by an isolated review before a verification run was spent, which is the point of reviewing first: the first of these was a guaranteed CI red. execute-wave-post-gate-pipeline-e2e asserted against the REAL generated registry that byLoopPoint['execute:wave:pre'] held exactly one contribution with capId claude-orchestration. It now holds zero. Inverted to assert exactly 0 -- not a vague >= 0 -- and the #2285 comment above it now explains the current state rather than the one it was written for. CONTEXT.md's Claude Orchestration entry claimed two contributions at wired points. It is now one, and the entry's execute:wave:post label was already wrong before this change: the manifest said execute:wave:pre. Rewritten to one plan:post contribution, why the execute-point one was removed, and where the procedure now lives. One assertion in claude-orchestration.test.cjs could not fail. It tested for the prose "(into the executor)" while the doc says "(`into: executor`)", so no plausible wording matched it and the paired plan:post assertion was carrying the row. Replaced with a check on the structural claim, and proved RED by restoring the two-contribution wording before reverting. The moved procedure keeps section headings that speak as a live contribution -- "When this contribution is active", "Why execute:wave:pre". Preserving the body verbatim was deliberate, so the headings stay and an editor's note under the header explains why they read that way. A sweep of all 17 files referencing byLoopPoint found no further siblings: the remaining hits are a synthetic capability fixture and an empty-points test that already expected no active hooks, both correct before and after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
34 KiB
Capability Manifest Reference (capability.json)
Canonical ADRs: ADR-1244 · ADR-894 · ADR-1016 See also: How to develop a capability · Capability Command Reference
Each capability is a folder capabilities/<id>/ (or an overlay root ~/.gsd/capabilities/<id>/ / .gsd/capabilities/<id>/) containing one capability.json declaration.
The file is schema-validated JSON with a common envelope plus a role-typed body (role: "feature", role: "runtime", or role: "reviewer").
Envelope fields
These fields are present for role: "feature", role: "runtime", and role: "reviewer" capabilities.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (kebab-case) | Yes | Unique identifier; must equal the folder name. The prefix gsd-, gsd-core-, and anthropic- are reserved for first-party use. |
role |
"feature" | "runtime" | "reviewer" |
Yes | Discriminator that selects the body schema. "reviewer" is for lane-only capabilities that ship a reviewer body and nothing else — see Reviewer body below. |
version |
semver string | Yes (1.6.0+) | Semantic version of this capability. The registry rejects a manifest without one. |
title |
string | Yes | Short human-readable label. Must be a non-empty string. |
description |
string | Yes | Longer summary sentence. Must be a non-empty string. |
tier |
"core" | "standard" | "full" |
Yes | Source of truth for install-profile membership and surface cluster assignment. tier propagates via the requires-closure; install profiles are generated from it. |
requires |
string[] | Yes | Capability id values this capability depends on. Must be present as an array (use [] when there are no dependencies). Each entry must exist in the registry, be acyclic, and be tier-monotone (a core capability may not require a standard or full capability; a standard capability may not require a full capability). |
engines |
object | No | Host-compatibility constraint. Sub-field: gsd — semver range string (e.g. ">=1.6.0 <3.0.0"). Acts as a hard gate at install and at load; a mismatch blocks installation and causes the overlay to be skipped with a warning at load time. |
runtimeCompat |
object | Yes (role: "feature") |
Declares which host runtimes this capability can surface through. Validated for every role: "feature" capability (a feature manifest without it fails validation). Sub-fields: supported — a non-empty array of kebab-case runtime ids, or the single wildcard ["*"] for a runtime-agnostic capability; unsupported — an array of kebab-case runtime ids (the wildcard is not permitted here); notes — optional object mapping a runtime id (or "*") to a non-empty explanatory string. The wildcard "*" may not be mixed with concrete ids in the same array, and the reserved names __proto__/constructor/prototype are rejected. |
compatVersions |
object | No | Graceful-downgrade table mapping "<capVersion>" to "<min gsd version>". Only meaningful for sources that enumerate versions (git tags, registry, npm); a bare tarball URL carries one version and simply blocks on incompatibility. |
integrity |
string | No | sha512-<base64> hash of the capability bundle. Verified before extraction when present; mismatch aborts install. |
provenance |
object | No | { sourceRepo: string, commit: string }. Emitted in CI for first-party and curated capabilities. |
author |
object | No | { name: string, email?: string, url?: string }. |
homepage |
string | No | URL. |
repository |
string | No | URL. |
license |
string | No | SPDX licence identifier (e.g. "MIT"). |
keywords |
string[] | No | Arbitrary search tags. |
Feature body (role: "feature")
Feature capabilities declare owned artefacts, lifecycle hooks, a federated configuration slice, and loop extension registrations.
skills and agents
| Sub-field | Type | Description |
|---|---|---|
skills |
string[] | Owned skill stems. Exactly one capability may own each stem across the entire merged registry (first-party ∪ overlay). |
agents |
string[] | Owned agent stems. Same uniqueness constraint as skills. |
The skills stems declared here are disclosed by name as instruction surfaces in the pre-install consent summary (ADR-2363). Bodies are installed verbatim and are not content-scanned — see the capability trust model.
agents are classified as an instruction surface too (ADR-2363 D3), but the stems declared here are not disclosed at the prompt: a third-party capability's agents[] are never staged into the agent's instruction context — the staging path that unions third-party skills into a runtime's skills directory has no equivalent for agents — so naming them would claim a surface that does not exist. This does not make agents safe or inert; it means the mechanism does not yet reach them.
hooks
Non-loop lifecycle hooks.
| Sub-field | Type | Description |
|---|---|---|
event |
string | Hook event name (host-runtime specific). |
script |
string | Path to the hook script, relative to the capability root. The hook command written into the host settings is the realpath-confined absolute path to this script (so it always runs the bundle's own file regardless of the working directory) and is POSIX single-quoted (so an install prefix containing spaces cannot break it). For shell safety the path must contain only [A-Za-z0-9._/-] — no whitespace, no shell metacharacters (; | & $ ` ( ) < > * ? [ ] { } ! ~ # ' " \ newline), no leading -, no absolute path, and no .. segment. A script outside this allowlist fails validation and the capability is rejected. |
config — federated config-key schema slice
The config field is an object whose keys are federated configuration keys contributed by this capability. Each key must be absent from the central config-schema and absent from every other capability's config object (collision fails the build gate). Each entry has the following shape:
| Property | Type | Description |
|---|---|---|
type |
"boolean" | "string" | "number" | "enum" |
Value type. |
default |
(type-consistent) | Default value; must be consistent with type. |
description |
string | Human-readable explanation of the key's effect. |
values |
string[] | enum only. Exhaustive list of permitted string values. |
steps
Steps run at a loop extension point as independent units. Ordering within a point is derived from produces/consumes (topological sort; capability-id is the tiebreak).
| Sub-field | Type | Required | Description |
|---|---|---|---|
point |
string | Yes | One of the 12 valid loop extension point identifiers (see table below). |
ref |
object | Yes | The dispatch target. Exactly one of { "skill": "<stem>" }, { "agent": "<stem>" }, or { "command": "<name>" } (the three are mutually exclusive). A skill/agent stem must be declared in this capability's skills/agents array. |
produces |
string[] | Yes | Artefact names this step produces. Must be present as an array (use [] when it produces none); an omitted produces fails validation. No two capability steps may produce the same artefact at the same point. |
consumes |
string[] | Yes | Artefact names this step consumes. Must be present as an array (use [] when it consumes none); an omitted consumes fails validation. |
onError |
"skip" | "halt" |
Yes | Behaviour on failure; must be present and one of "skip" or "halt" (an omitted onError fails validation). Steps are purely additive — they never halt or redirect the host workflow on their own; a blocking precondition is expressed as a gate. |
when |
string | No | Dotted config key; the step is active only when the key is truthy. Evaluated deterministically at render time; phase-context applicability is the skill's own responsibility. |
fragment |
object | No | Optional inline-or-file prompt fragment attached to the step, with the same { "path": "<relative path>" } or { "inline": "<string>" } semantics as a contribution's fragment. A path is materialised (read and inlined) at load time, resolved against the capability directory and confined to it (.. traversal is rejected). |
supportsReviewerLanes |
boolean | No | Strict opt-in trait (#4209): declares that this step's dispatch target accepts external reviewer-lane evidence. Only a literal true opts in — every other type fails validation, and false/omitted are inert (no reviewer-lane behaviour, no key on the projected active hook). Step-scoped, not capability-wide. |
contributions
Contributions inject a fragment into a named agent role's prompt at a loop extension point. Multiple contributions into the same agent role render as ordered labelled blocks (<contribution from="<id>">…</contribution>).
| Sub-field | Type | Required | Description |
|---|---|---|---|
point |
string | Yes | One of the 12 valid loop extension point identifiers. |
into |
string | Yes | Agent role name. Must be a role published by that loop extension point in the host contract. |
produces |
string[] | Yes | Artefact names this contribution produces. Use [] when it produces none. |
consumes |
string[] | Yes | Artefact names this contribution reads. Use [] when it reads none. |
fragment |
object | Yes | Either { "path": "<relative path>" } (file content) or { "inline": "<string>" } (literal text). |
when |
string | No | Dotted config key; activates the contribution conditionally. |
onError |
"skip" | "halt" |
No | Behaviour on failure. |
gates
Gates check a condition at a loop extension point and optionally block progression.
| Sub-field | Type | Required | Description |
|---|---|---|---|
point |
string | Yes | One of the 12 valid loop extension point identifiers. |
check |
object | Yes | One of three forms (see table below). Must be present as an object; an omitted check fails validation. |
blocking |
boolean | Yes | Must be present and a boolean; an omitted blocking fails validation. When true, a failed check halts the loop at this point. |
onError |
"skip" | "halt" |
Yes | Behaviour when the check itself errors; must be present and one of "skip" or "halt" (an omitted onError fails validation). |
when |
string | No | Dotted config key; activates the gate conditionally. |
check forms:
| Form | Shape | Blocking permitted | Notes |
|---|---|---|---|
| Query | { "query": "<gsd_run query>" } |
Yes | Deterministic first-party code. |
| Predicate | { "predicate": { "kind": "artifact-exists" | "config-equals" | …, … } } |
Yes | Declarative; no code path. |
| Agent verdict | { "agentVerdict": { "ref": …, "prompt": … } } |
No (forced advisory) | LLM evaluation; non-deterministic checks may not halt the loop. |
taskContentResolver
Declares that this capability resolves per-task content (<action>/<verify>/
<acceptance_criteria>/<read_first>/<done>) from an external issue tracker instead of
execute-plan.md's per-task loop reading it inline from a task's PLAN.md body. This is not
one of steps / contributions / gates, and it does not use a point value from the closed
12-point vocabulary above — it is dispatched directly, once per task, by execute-plan.md before
that task's read_first gate, documented separately in
loop-hook-dispatch.md.
See ADR-3646 for the full design and
Develop a task-content resolver capability
for the authoring walkthrough.
| Sub-field | Type | Required | Description |
|---|---|---|---|
trackerPrefix |
string (kebab-case) | Yes | Matches the prefix of a task's <task tracker-id="beads:GSD-42"> attribute — everything before the first :. Text after the first colon, including further colons, is passed through verbatim as the id. Must be unique across the merged first-party ∪ overlay capability set. |
invoke.binary |
string | Yes | Executable name or path for the resolver subprocess. |
invoke.args |
string[] | Yes | Argv passed to invoke.binary. Must contain the {{id}} placeholder at least once — GSD substitutes it with the task's tracker id (everything after the first :); an args array that never carries the placeholder fails validation, since the id could never reach the resolver. |
invoke.timeoutMs |
number | Yes | Bound on the subprocess invocation. Required — an unbounded resolver subprocess is this repo's named Unbounded Subprocesses defect class. A resolver exceeding this bound is killed and task resolve-content exits non-zero. |
taskContentResolver is feature-role only (role: "feature"); it is not admissible on role: "runtime" or role: "reviewer" bodies.
Valid point values
The 12 loop extension points are a closed, additive-only vocabulary. Every steps, contributions, and gates entry must use one of these identifiers exactly.
A valid point is necessary but not sufficient: each host workflow decides, per point, which hook kinds its dispatch text handles, and a point may dispatch a subset. The registry build derives the real answer from the host workflows (getWiredKinds() in scripts/gen-loop-host-contract.cjs) and rejects a manifest declaring a kind the point does not dispatch — so an unsupported combination is a build-time error naming the point, the kind, and the kinds that point does cover. It is never a hook that renders and is then silently dropped.
verify:pre dispatches all three kinds. A step there is advisory: it runs before UAT begins and never blocks it — a precondition that must halt verification is a gate. Its produces artefact names are consumed additively by the verify workflow's extract_tests step, which can deepen what UAT covers but cannot suppress a checkpoint. See Develop a capability for the authoring workflow.
| Point | Phase | Position |
|---|---|---|
discuss:pre |
Discuss | Before the discuss step executes |
discuss:post |
Discuss | After the discuss step completes |
plan:pre |
Plan | Before the plan step executes |
plan:post |
Plan | After the plan step completes |
execute:pre |
Execute | Before the execute phase begins |
execute:wave:pre |
Execute | Before each execution wave |
execute:wave:post |
Execute | After each execution wave |
execute:post |
Execute | After the execute phase completes |
verify:pre |
Verify | Before the verify step executes |
verify:post |
Verify | After the verify step completes |
ship:pre |
Ship | Before the ship step executes |
ship:post |
Ship | After the ship step completes |
Runtime body (role: "runtime")
Runtime capabilities describe how GSD projects its artefacts onto one host CLI. The body is a closed 8-axis (plus 4 install-surface) vocabulary; no feature-only fields (skills, agents, steps, contributions, gates, hooks) are permitted. Full semantic specifications, the closed enum values for each axis, and the 16-runtime worked examples are in ADR-1016.
| Axis | Field | Type summary |
|---|---|---|
| Config home | runtime.configHome |
Structured object with kind (dot-home | dot-home-nested | xdg | generic-agents-root), name, optional parent, env[], probe[], probeExists, skillsHome. probeExists is an optional sub-path applied to probe candidates: for generic-agents-root it is a hard filter (a candidate qualifies only if <candidate>/<probeExists> exists); for dot-home-nested it is a preference that makes probing pick the candidate GSD owns (e.g. gsd-core/VERSION) over a bare-existing sibling before falling back — see ADR-1016 and #213/#217. |
| Local config dir | runtime.localConfigDir |
Required dot-prefixed string. The runtime's local content-rewrite directory — the ./ target GSD stamps into rewritten artefact bodies (e.g. ./.claude/ → ./<localConfigDir>/) and the local install dir basename. Backs getDirName() (registry-derived, #1679). Usually .<runtime> (the runtime's home dot-dir), but three runtimes diverge because they read GSD's content from a non-home directory: copilot → .github (GitHub Copilot reads custom instructions from .github/copilot-instructions.md / .github/instructions/; see convertClaudeToCopilotContent rewrites in src/runtime-artifact-conversion.cts), antigravity → .agents (local agent/workflow dir; see the antigravity rewrites in src/runtime-artifact-conversion.cts), kimi → .kimi-code. Distinct from configHome.name (the global install home, which for these three is .copilot / antigravity / agents). Byte-parity-proven against the prior hand-maintained mapping by the golden-install-parity harness. |
| Config format | runtime.configFormat |
Closed enum: settings-json | toml | markdown | markdown-dir | none. |
| Artefact layout | runtime.artifactLayout |
Object with global and local arrays of ArtifactKind (kind, destSubpath, prefix, nesting, recursive, stage). |
| Command style | runtime.commandStyle |
Closed enum: slash-hyphen | shell-var. |
| Hooks surface | runtime.hooksSurface |
Closed enum: settings-json | codex-hooks-json | cursor-hooks-json | copilot-inline | cline-rules | kimi-hooks-toml | none. |
| Sandbox tier | runtime.sandboxTier |
Closed enum: none | codex-agent-sandbox. |
| Support tier | runtime.supportTier |
Integer: 1 (fully tested first-party) | 2 (shipped, lower coverage). |
| Install surface | runtime.installSurface |
Closed enum: settings-json | codex-toml | copilot-instructions | cline-rules | cursor-hooks-json | profile-marker-only. |
| Shared settings | runtime.writesSharedSettings |
boolean. Whether the runtime writes a shared settings.json. |
| Permission writer | runtime.permissionWriter |
null | "opencode" | "kilo" | "antigravity". The finish-time permissions-sidecar writer. |
| Extended hook events | runtime.extendedHookEvents |
string[] over a closed vocabulary: SubagentStop, Stop, PreCompact, FileChanged, BeforeAgent, AfterAgent, BeforeModel, SubagentStart. |
hostBehaviors
runtime.hostBehaviors is a closed vocabulary of per-host behavior switches consumed directly by installer and runtime-adaptation code. A key outside the vocabulary is ignored, with a non-fatal warning naming the capability and the key; it is never a validation error, so a manifest authored against a newer GSD degrades visibly instead of failing the build of a repo that merely reads it.
Adding a key is a reviewed first-party change, which is ADR-1016's intended friction rather than an obstacle: the runtime descriptor expresses every per-host difference as a value over a closed vocabulary, and a host needing a new shape gets a named primitive rather than an open escape hatch.
History.
hostBehaviorswent unvalidated until #2801, and this page previously described it as a deliberate open seam sanctioned by ADR-1016. That attribution was wrong — ADR-1016 does not mentionhostBehaviorsat all. See the ADR-1016 amendment.
The vocabulary holds 59 keys; 39 of them are set by exactly one capability. This table is not exhaustive — it lists the keys with the widest reuse so a reader can pattern-match new ones against the same shape:
| Key | Capabilities declaring it |
|---|---|
reapplyCommand |
9 |
skipSharedHooksInstall |
8 |
frontmatterDialect |
5 |
hyphenNameAgentBody |
3 |
legacyCommandsGsdInstallMigration |
3 |
legacyCommandsGsdUninstall |
3 |
nativePlugin |
3 |
skipUpdateBannerCommand |
3 |
verificationStyle |
3 |
reviewerCli has been removed. It was a boolean that marked a runtime capability as also being a reviewer lane. ADR-2782 replaced it with the reviewer body; it survived one release (1.9.0 → 1.10.0) as a derived legacy alias and was deleted in Phase 7 (#2801). No shipped capability declares it.
If your out-of-tree manifest still sets it: nothing crashes and nothing else about your capability changes — it simply contributes no reviewer lane, and the registry reports a non-fatal warning naming the capability. The warning reaches you at build time on stderr, and at install time through the overlay loader's diagnostics. To restore the lane, declare a reviewer body; Ship a reviewer lane in your capability is the migration path, and the field reference is below.
See ADR-1016 (the runtime descriptor is a closed vocabulary; its 2026-08-09 amendment closes hostBehaviors too) and ADR-2782 (introduces the reviewer body, and D9 retires the reviewerCli alias).
For a minimal role: "runtime" example, see ADR-1016 §Decision 8.
Reviewer body (role: "reviewer", or on any role)
ADR-2782 introduces the reviewer lane: one external CLI or model endpoint that /gsd-review hands a plan to for independent review.
To declare one, follow Ship a reviewer lane in your capability. This section is the field reference behind that guide.
The reviewer body is optional and absent-safe at every layer. A capability with no reviewer body is simply not a lane — that is never a validation error. This is a normative forward/backward-compatibility invariant, not a nicety: a plugin, a runtime, or a future GSD version may omit reviewer entirely with no consequence.
The shape is hybrid:
- A
reviewerbody is admissible onrole: "runtime", so an existing runtime capability —codex,antigravity— keeps one manifest that is both an installable runtime and a reviewer lane. - A third role,
role: "reviewer", exists for lane-only CLIs that GSD never installs into. There are currently 5:coderabbit,gemini,llama-cpp,lm-studio,ollama.
Current role counts across capabilities/: feature 20, runtime 19, reviewer 5.
All 12 shipped lane declarations carry all 14 fields below.
| Field | Type | Notes |
|---|---|---|
slug |
string | Lane identity; grammar ^[a-z0-9][a-z0-9_-]*$. May use _ (lm_studio, llama_cpp) even where the capability folder id is kebab-case (lm-studio). |
flags |
string[] | User-facing CLI flags that select this lane. A lane may declare more than one — antigravity declares --antigravity and --agy. 12 lanes declare 13 flags in total. |
transport |
closed enum | spawn | openai-http. |
probe |
object | Availability check. probe.kind is a closed enum: command-exists | command-capability | http-reachable. command-capability additionally takes binary, needle, and a required timeoutMs — it exists because a bare binary name can be ambiguous (kimi is claimed by both the Kimi Code CLI and the legacy Python kimi-cli), and the timeout bound is mandatory because an unbounded --help | grep probe is this repo's named Unbounded Subprocesses defect. |
invoke |
object | Shape is selected by transport. For spawn: binary, args[], promptChannel (stdin | argv | argv-file-ref | none), outputChannel (stdout | file-arg), outputArg (required when outputChannel is file-arg), modelArg (string or null), effortChannel (none | argv | env), env (optional; an object of environment name/value pairs, string values only, merged over the inherited environment for that one spawn — keys must match the portable environment-name grammar [A-Za-z_][A-Za-z0-9_]*, which is a portability policy rather than an OS limit, and __proto__ is refused because it would be dropped before reaching the child). For openai-http: hostConfigKey, defaultHost, path, modelDiscovery (none | first-from-models-endpoint), fallbackModel, effortChannel. args supports the {{model}}, {{prompt}}, {{effort}}, and {{output}} placeholders. Every field in this object is disclosed at install and bound to the consent signature — env and defaultHost by name in the consent prompt, the rest through a residual, so any change to a declared invoke field forces re-consent. env additionally refuses execution-primitive names — PATH, NODE_OPTIONS, LD_PRELOAD, DYLD_INSERT_LIBRARIES, BASH_ENV, PYTHONPATH, PERL5OPT, RUBYOPT, GIT_SSH_COMMAND, JAVA_TOOL_OPTIONS and siblings, matched case-insensitively (Windows environment lookup is). A lane needing a specific executable declares an absolute binary rather than reshaping the child's PATH. That denylist is defence in depth and not the boundary: it cannot be complete against an arbitrary child, and disclosure runs before validation, so install-time consent — which shows every declared pair and warns on execution-primitive names — is what actually gates them. |
timeoutFloorMs |
number | Measured per-lane floor. Lane divergence here is real and correct — the descriptor's job is to declare divergence in one place, not to promise uniformity. |
timeoutConfigKey |
string or null |
Federated config key holding this lane's outer timeout override, in SECONDS, e.g. review.timeouts.antigravity. Falls back to timeoutFloorMs when unset or invalid (#3274). |
emptyOutput |
closed enum | stub-with-stderr | handler-owned. |
reviewsSection |
string | The REVIEWS.md heading this lane renders under. Must be unique across the merged roster. |
evidenceClass |
closed enum | source-grounded | diff-only (diff-only findings are down-weighted in consensus). |
requiresBinaries |
string[] | Extra binaries the lane needs beyond invoke.binary. |
promptBudgetKey |
string or null |
Federated config key bounding prompt size. |
modelConfigKey |
string or null |
Federated config key naming the model, e.g. review.models.kimi-code. |
handler |
closed enum or null |
antigravity | openai-compatible | opencode | null. |
handler is a closed enum of first-party handler names, not an open escape hatch. ADR-1016 explicitly rejected "arbitrary code in the descriptor"; hard shapes are absorbed by adding a named primitive that is reviewed first-party. The consequence, stated plainly: a third-party reviewer lane is strictly data-only. A plugin can ship a lane, but not a quirky lane that needs imperative code — a lane requiring behavior beyond the closed handler set is not expressible and must be proposed and merged first-party.
Uniqueness is enforced across the merged first-party ∪ overlay set: duplicate slug, duplicate flags entry, and duplicate reviewsSection are each build-time violations. Two lanes sharing a reviewsSection heading would silently merge their output in REVIEWS.md, producing apparent consensus that does not exist.
An unknown field inside a reviewer body is a non-fatal warning on stderr, never a build failure (ADR-2782 D4), so a manifest built against a newer GSD degrades visibly rather than crashing.
Example — lane-only role: "reviewer" capability
{
"id": "coderabbit",
"role": "reviewer",
"version": "1.8.0",
"title": "CodeRabbit",
"description": "CodeRabbit CLI — cross-AI /gsd-review reviewer lane only; not a GSD install target (no runtime body, no artifacts).",
"tier": "full",
"requires": [],
"engines": { "gsd": ">=1.8.0" },
"reviewer": {
"slug": "coderabbit",
"flags": ["--coderabbit"],
"transport": "spawn",
"probe": { "kind": "command-exists", "binary": "coderabbit" },
"invoke": {
"binary": "coderabbit",
"args": ["review", "--prompt-only"],
"promptChannel": "none",
"outputChannel": "stdout",
"modelArg": null,
"effortChannel": "none"
},
"timeoutFloorMs": 360000,
"timeoutConfigKey": null,
"emptyOutput": "stub-with-stderr",
"reviewsSection": "CodeRabbit",
"evidenceClass": "diff-only",
"requiresBinaries": [],
"promptBudgetKey": null,
"modelConfigKey": null,
"handler": null
}
}
Conformance invariants
The following invariants are enforced at build time by scripts/gen-capability-registry.cjs and at install time by the runtime-callable validateCapability() / validateCrossCapability() over the merged first-party ∪ overlay set.
versionis required. The registry rejects any manifest without a semverversionfield.iduniqueness. No two capabilities may share anid. An overlay whoseidcollides with a first-partyidis rejected; first-party always wins.- Skill and agent stem uniqueness. Exactly one capability may own each skill or agent stem across the entire merged registry.
requiresexist and are acyclic. Everyidlisted inrequiresmust exist in the registry; the dependency graph must be acyclic.requiresis tier-monotone. Acorecapability may not require astandardorfullcapability. Astandardcapability may not require afullcapability.pointvalues are from the closed set. Everypointinsteps,contributions, andgatesmust be one of the 12 identifiers above.contribution.intois a published agent role. Theintovalue must be an agent role declared by the host contract for that loop extension point. The published roles are family-partitioned (ADR-894 §3, Amendment 2026-09-14): every role belongs to exactly one of orchestration (orchestrator), planning (researcher,planner,checker) or execution (executor,verifier), and each loop step publishes exactly one family. Soexecute:*publishesexecutor/verifierand neverorchestrator, anddiscuss:*/verify:*/ship:*publishorchestratorand neverexecutor/verifier. The partition is enforced when the host contract is generated, so the role set a point publishes is stable rather than incidental.- Config key exclusivity. A federated config key must be owned by exactly one capability and absent from the central
config-schema. Presence in both is a collision; a half-migrated key fails the build gate. - Artefact production uniqueness per point. No two capability steps may
producesthe same artefact name at the same loop extension point. engines.gsdis a hard gate. A capability whoseengines.gsdrange does not satisfy the installed GSD version is blocked at install and skipped (with a warning) at load time.- Path confinement. Declared module paths may not use parent-directory traversal (
../); modules arerequire()'d only from the capability's own install root. - Reserved namespace. Capability
idvalues beginning withgsd-,gsd-core-, oranthropic-are reserved; third-party capabilities using these prefixes are rejected. taskContentResolver.trackerPrefixuniqueness, feature-only.trackerPrefixmust be unique across the merged first-party ∪ overlay capability set (mirrorsreviewsSectionuniqueness on the reviewer body above); a collision is a build-time violation.taskContentResolveris admissible only onrole: "feature"bodies.
Example — complete role: "feature" capability
The following is the canonical UI design-contract capability from ADR-894. It illustrates all major body sections.
{
"id": "ui",
"role": "feature",
"version": "1.0.0",
"title": "UI design contracts",
"description": "UI-SPEC design contract and retrospective UI audit for frontend phases.",
"tier": "standard",
"requires": [],
"engines": { "gsd": ">=1.6.0" },
"runtimeCompat": { "supported": ["*"], "unsupported": [] },
"skills": ["ui-phase", "ui-review"],
"agents": ["gsd-ui-checker", "gsd-ui-auditor"],
"hooks": [],
"config": {
"workflow.ui_phase": {
"type": "boolean",
"default": true,
"description": "Enable the UI design-contract gate during planning."
},
"workflow.ui_review": {
"type": "boolean",
"default": true,
"description": "Enable the retrospective UI audit."
},
"workflow.ui_safety_gate": {
"type": "boolean",
"default": true,
"description": "Block execution on unmet UI-SPEC contracts."
}
},
"steps": [
{
"point": "plan:pre",
"ref": { "skill": "ui-phase" },
"produces": ["UI-SPEC.md"],
"consumes": ["CONTEXT.md"],
"when": "workflow.ui_phase",
"onError": "skip"
},
{
"point": "verify:post",
"ref": { "skill": "ui-review" },
"produces": ["UI-REVIEW.md"],
"consumes": ["UI-SPEC.md"],
"when": "workflow.ui_review",
"onError": "skip"
}
],
"contributions": [],
"gates": [
{
"point": "execute:wave:post",
"check": { "query": "ui.safety-gate" },
"when": "workflow.ui_safety_gate",
"blocking": true,
"onError": "halt"
}
]
}
Notes on this example:
whenon each hook references its own config key; whether the phase is actually a frontend phase is decided insideui-phase(self-gate).- The
plan:prestep self-skips on non-frontend phases, producing noUI-SPEC.md; theexecute:wave:postgate'sui.safety-gatequery passes gracefully when noUI-SPEC.mdexists. - A
contributionfollows this shape:{ "point": "plan:pre", "into": "planner", "produces": [], "consumes": [], "fragment": { "path": "loop/threat-model.md" }, "when": "workflow.security_enforcement" }(producesandconsumesare required arrays — use[]when empty).