docs(#1464): fix ADR-1244 capability doc set — followable tutorials, overlay-model + install tutorial, set/fragment/runtimeCompat reference, accuracy fixes

This commit is contained in:
Tom Boucher
2026-06-20 11:59:09 -04:00
parent 1c1c31f677
commit 02c6491e61
11 changed files with 860 additions and 142 deletions

View File

@@ -17,11 +17,12 @@ These fields are present for both `role: "feature"` and `role: "runtime"` capabi
| `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"` | Yes | Discriminator that selects the body schema. |
| `version` | semver string | Yes (1.6.0+) | Semantic version of this capability. The registry rejects a manifest without one. |
| `title` | string | No | Short human-readable label. |
| `description` | string | No | Longer summary sentence. |
| `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[] | No | Capability `id` values this capability depends on. 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). |
| `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. |
@@ -68,38 +69,41 @@ The `config` field is an object whose keys are federated configuration keys cont
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 | Description |
|---|---|---|
| `point` | string | One of the 12 valid loop extension point identifiers (see table below). |
| `ref` | object | Either `{ "skill": "<stem>" }` or `{ "agent": "<stem>" }`. |
| `produces` | string[] | Artefact names this step produces. No two capability steps may produce the same artefact at the same point. |
| `consumes` | string[] | Artefact names this step consumes. |
| `when` | string | 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. |
| `onError` | `"skip"` \| `"halt"` | Behaviour on failure. `"skip"` is the default. Steps are purely additive — they never halt or redirect the host workflow on their own; a blocking precondition is expressed as a `gate`. |
| 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). |
### `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 | Description |
|---|---|---|
| `point` | string | One of the 12 valid loop extension point identifiers. |
| `into` | string | Agent role name. Must be a role published by that loop extension point in the host contract. |
| `fragment` | object | Either `{ "path": "<relative path>" }` (file content) or `{ "inline": "<string>" }` (literal text). |
| `when` | string | Dotted config key; activates the contribution conditionally. |
| `onError` | `"skip"` \| `"halt"` | Behaviour on failure. |
| 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 | Description |
|---|---|---|
| `point` | string | One of the 12 valid loop extension point identifiers. |
| `check` | object | One of three forms (see table below). |
| `when` | string | Dotted config key; activates the gate conditionally. |
| `blocking` | boolean | When `true`, a failed check halts the loop at this point. |
| `onError` | `"skip"` \| `"halt"` | Behaviour when the check itself errors. |
| 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:**
@@ -187,6 +191,7 @@ The following is the canonical UI design-contract capability from ADR-894. It il
"tier": "standard",
"requires": [],
"engines": { "gsd": ">=1.6.0" },
"runtimeCompat": { "supported": ["*"], "unsupported": [] },
"skills": ["ui-phase", "ui-review"],
"agents": ["gsd-ui-checker", "gsd-ui-auditor"],
"hooks": [],
@@ -241,4 +246,4 @@ The following is the canonical UI design-contract capability from ADR-894. It il
Notes on this example:
- `when` on each hook references its own config key; whether the phase is actually a frontend phase is decided inside `ui-phase` (self-gate).
- The `plan:pre` step self-skips on non-frontend phases, producing no `UI-SPEC.md`; the `execute:wave:post` gate's `ui.safety-gate` query passes gracefully when no `UI-SPEC.md` exists.
- A `contribution` follows this shape: `{ "point": "plan:pre", "into": "planner", "fragment": { "path": "loop/threat-model.md" }, "when": "workflow.security_enforcement" }`.
- A `contribution` follows this shape: `{ "point": "plan:pre", "into": "planner", "produces": [], "consumes": [], "fragment": { "path": "loop/threat-model.md" }, "when": "workflow.security_enforcement" }` (`produces` and `consumes` are required arrays — use `[]` when empty).

View File

@@ -80,11 +80,11 @@ For third-party capabilities, a version whose executable set (hooks, command mod
| Source kind | Re-resolution behaviour |
|---|---|
| `<name>@<registry>` | Registry catalogue query |
| git (`https://…/repo.git#<tag>`) | Remote tag fetch |
| npm (`npm:@org/pkg@<range>`) | `npm dist-tags` / range resolution |
| tarball (`https://…/cap-x.y.z.tgz`) | Re-fetch of the recorded URL |
| local (`./local/path`) | Re-read of the recorded filesystem path |
| registry (`<name>@<registry>`) | **Not yet implemented** — the registry source kind is reserved; re-resolution throws, so an overlay recorded from a registry spec cannot currently be updated. |
---
@@ -127,7 +127,11 @@ gsd capability disable <id> [--config-dir <path>] [--runtime <r>] [--scope <s>]
**Behaviour**
Marks the capability **inactive** in the runtime activation state — identical to `gsd capability set <id> --off`. A disabled capability stays on disk; it is excluded from the active surface and contributes no hooks, config keys, or loop extension registrations until re-enabled. This toggles the capability-state layer (the runtime config), not the install ledger. The id must be a capability known to the registry; activation toggling of an installed **third-party overlay** by id is not yet wired through this path — remove an overlay with `gsd capability remove`. `enable` reverses a disable without re-fetching.
Marks the capability **inactive** in the runtime activation state — identical to `gsd capability set <id> --off`. A disabled capability stays on disk; it is excluded from the active surface and contributes no hooks, config keys, or loop extension registrations until re-enabled. This toggles the capability-state layer (the runtime config), not the install ledger.
> **Scope: first-party capabilities only.** `<id>` is validated against the **build-time first-party registry** (the generated `capability-registry.cjs`). An installed **third-party overlay** — one added with `gsd capability install …` — is **not** in that registry, so `disable` rejects it with `unknown capability: "<id>"`. Deactivate an installed overlay with [`gsd capability remove <id> --scope <scope>`](#remove) instead.
`enable` reverses a disable without re-fetching.
---
@@ -143,6 +147,44 @@ gsd capability enable <id> [--config-dir <path>] [--runtime <r>] [--scope <s>]
Clears the inactive flag for `<id>` in the runtime activation state — identical to `gsd capability set <id> --on`. On the next GSD invocation the capability is included in the active surface again, subject to its `engines.gsd` range (an incompatible capability is still skipped with a warning at load time).
> **Scope: first-party capabilities only.** Like `disable`, `enable` validates `<id>` against the build-time first-party registry and rejects an installed overlay with `unknown capability: "<id>"`. There is no `enable` for an installed overlay — re-install it with [`gsd capability install …`](#install) if it was removed.
---
### `set`
**Synopsis**
```
gsd capability set <id> [--on | --enable | --off | --disable] [--gate <key>=<bool>]… [--config-dir <path>] [--runtime <r>] [--scope <s>]
```
**Flags**
| Flag | Description |
|---|---|
| `--on` / `--enable` | Surface the capability (activate its skills). Mutually exclusive with `--off`/`--disable`. |
| `--off` / `--disable` | Unsurface the capability (deactivate its skills). Mutually exclusive with `--on`/`--enable`. |
| `--gate <key>=<bool>` | Set one capability **gate** to `true` or `false`. Repeatable to set several gates in one call. `<bool>` must be the literal `true` or `false`; any other value is rejected. |
| `--config-dir <path>` | Override the runtime config directory the surface state is read from and written to. |
| `--runtime <r>` | When given, re-materialise the surface (rewrite skill files) for runtime `<r>` after the state change. |
| `--scope <s>` | The materialise scope (`global` or `project`); only meaningful together with `--runtime`. Defaults to `global`. |
**Behaviour**
`set` is the single write verb behind the capability **activation** axes. It mutates two independent layers and then re-resolves and reports the capability's state:
- The **enabled** axis (`--on`/`--off`) toggles whether the capability's skills are on the runtime surface (the same mechanism `disable`/`enable` use; `disable`/`enable` are thin aliases for `set … --off`/`--on`).
- The **gate** axis (`--gate`) writes capability-owned config keys into `.planning/config.json`.
> **Scope: first-party capabilities only.** `set` validates `<id>` against the **build-time first-party registry** (the generated `capability-registry.cjs`); an unrecognized id — including any installed **third-party overlay** — is rejected with `unknown capability: "<id>"` and no writes are performed. `set` is for the activation/gate axes of first-party capabilities; to turn off an installed overlay use [`gsd capability remove`](#remove).
A **gate** is a dotted config key declared in the capability's `config` slice whose boolean value controls whether one of the capability's loop hooks fires. Setting a gate to `false` stops that hook running while leaving the capability surfaced; setting it to `true` re-arms it. A `--gate <key>=…` whose `<key>` is not a declared config key of `<id>`, or whose value is not boolean, is rejected and **no** writes are performed (the whole operation is validated before any state is written).
The command is **fail-closed on intent**: if you ask to enable a capability whose skills are not in the install profile, or whose surface/profile does not actually carry it, the operation reports an error rather than silently no-op'ing. Enabling a capability that owns no skills is an advisory warning (use gates to toggle its hooks instead). Surfacing a capability whose every hook is gated off is reported as a warning ("surfaced but every hook is gated off — did you mean `--off`?").
In `--raw` mode the full `{ capabilities, warnings, errors }` envelope is emitted as JSON and the process exits non-zero when `errors` is non-empty; in human mode warnings and errors are written to stderr and a one-line summary of the target capability (`enabled`, `surfaced`, `installed`, active-hook count) is printed.
---
### `list`
@@ -150,7 +192,7 @@ Clears the inactive flag for `<id>` in the runtime activation state — identica
**Synopsis**
```
gsd capability list [--json]
gsd capability list [--json] [--scope global|project]
```
**Flags**
@@ -158,10 +200,11 @@ gsd capability list [--json]
| Flag | Description |
|---|---|
| `--json` | Currently a **no-op**: `list` always emits the JSON array regardless of this flag. The flag is accepted for forward compatibility — a formatted human-readable table is planned, at which point `--json` will select the JSON form. Do not rely on omitting `--json` to get non-JSON output today. |
| `--scope` | Read only the given scope's overlay ledger (`global` or `project`). When omitted, both overlay scopes are swept. First-party capabilities are always listed regardless of `--scope`. |
**Behaviour**
Lists capabilities visible to the current session: first-party capabilities (from the registry) plus installed overlay capabilities in both the `global` and `project` scopes. Emits a JSON array of descriptors.
Lists capabilities visible to the current session: first-party capabilities (from the registry) plus installed overlay capabilities. With no `--scope`, both the `global` and `project` overlay scopes are swept; with `--scope`, only that scope's overlay ledger is read. Emits a JSON array of descriptors.
**Output shape**
@@ -189,7 +232,7 @@ Lists capabilities visible to the current session: first-party capabilities (fro
| `incompatible` | An overlay whose `engines.gsd` range does not satisfy the current GSD version; skipped with a warning at load time. |
| `inactive` | A **project-scope** overlay that is present on disk (and may have a committed-looking project ledger) but has **no user consent record on this machine** (#1459). It is *discovered but not activated*: it contributes no surfaces and runs nothing. The accompanying `reason` field explains why. Consent it by re-installing through the lifecycle (`gsd capability install … --scope project`). |
The `reason` field is `null` for active/incompatible rows and carries a short explanation for `inactive` rows.
The `reason` field is present on **overlay** rows: `null` for active/incompatible overlays and a short explanation for `inactive` ones. First-party rows omit `reason` (and `scope`/`source`/`status` are always `first-party`/`first-party`/`active`).
> Whether a capability has been turned off via `disable` is reported by `gsd capability state` (the activation-state view), not by `list`.
@@ -281,6 +324,8 @@ gsd capability trust revoke <id> [--project <path>]
"scope": "project",
"projectRoot": "/abs/realpath/of/project",
"integrity": "sha512-… | (empty)",
"disclosureSignature": "string",
"contentHash": "sha512-…",
"consentedAt": "ISO-8601 timestamp"
}
]
@@ -298,11 +343,11 @@ The `install` subcommand accepts the following source specification forms.
| Form | Example | Adapter | `--integrity` |
|---|---|---|---|
| Registry name | `my-cap@gsd-registry` | Registry — fetches the capability bundle from the named registry; `integrity` is populated from the registry catalogue. | Verified over the fetched bundle. |
| Git URL with tag | `https://github.com/org/repo.git#v1.2.0` | Git — clones/fetches at the specified tag; `#sha:<40-hex>` pins a specific commit. | **Rejected** — a clone is a directory tree, not a single hashable artifact. Pin the commit with `#sha:<commit>` instead. |
| npm package | `npm:@org/gsd-capability-foo@^1.0.0` | npm — resolves via `npm dist-tags` / semver range; installs with `--ignore-scripts`. | Verified over the `npm pack` `.tgz` bytes (same SRI sha512 domain as a tarball). |
| Tarball URL | `https://host/path/cap-x.y.z.tgz` | Tarball — fetches over HTTPS. | Verified over the downloaded `.tgz` bytes. |
| Local path | `./local/path` (or an absolute path) | Local — copies from the filesystem path. Auto-update detection is not available for this form. | **Rejected** — a local directory has no single hashable artifact; integrity pinning is not supported for local sources. |
| Registry name | `my-cap@gsd-registry` | **Reserved — not yet implemented.** The spec form parses, but there is no first-party registry endpoint, so resolution throws and the install fails. Use a git, npm, tarball, or local source today. | n/a |
Which source forms are *permitted* is governed by the `capabilities.strict_known_registries` policy (see [Configuration](../CONFIGURATION.md) and [the capability trust model](../explanation/capability-trust-model.md)): `null`/absent is permissive, `[]` is lockdown (no third-party sources), and a host allowlist permits only matching registries. This policy is **project-scoped** — it is read from the current project's `.planning/config.json` and applied to installs run in that project regardless of `--scope`; there is no machine-wide source allowlist. (A present-but-unparseable config fails **closed** — external installs are blocked until it is fixed.)