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

@@ -4,7 +4,7 @@ In this tutorial you will build a tiny, fully declarative GSD capability from sc
No code is required. Declarative capabilities — those that own only prompt fragments and hook declarations, with no executable hook scripts or MCP servers — require no trust prompt at install time.
We will build a capability called `hello-note`. It registers a `step` at the `plan:pre` extension point that injects a short greeting fragment into the planner's context and declares that it produces a file called `HELLO.md`.
We will build a capability called `hello-note`. It registers a `contribution` at the `plan:pre` extension point that injects a short greeting fragment into the planner's prompt and declares that it produces a file called `HELLO.md`.
---
@@ -46,7 +46,7 @@ Your project tree now looks like this:
## Step 2 — Write the prompt fragment
The fragment is a short Markdown file that will be injected into the planner's context when the `plan:pre` hook fires. Create it:
The fragment is a short Markdown file that will be injected into the planner's prompt when the `plan:pre` hook fires. Create it:
```bash
cat > capabilities/hello-note/fragments/plan-pre.md << 'EOF'
@@ -57,7 +57,7 @@ Record a brief note in HELLO.md summarising the plan goal in one sentence.
EOF
```
Notice that the fragment is plain prose. The capability system inlines it into the agent prompt at dispatch time.
Notice that the fragment is plain prose. The capability system reads this file and inlines its text when the capability is loaded, then renders it into the planner's prompt when the loop reaches `plan:pre`.
---
@@ -71,7 +71,7 @@ Create the manifest at `capabilities/hello-note/capability.json`:
"role": "feature",
"version": "0.1.0",
"title": "Hello Note",
"description": "Injects a greeting note step at plan:pre and produces HELLO.md.",
"description": "Injects a greeting note at plan:pre and produces HELLO.md.",
"tier": "standard",
"requires": [],
"engines": { "gsd": ">=1.6.0" },
@@ -79,16 +79,17 @@ Create the manifest at `capabilities/hello-note/capability.json`:
"skills": [],
"agents": [],
"config": {},
"steps": [
"steps": [],
"contributions": [
{
"point": "plan:pre",
"into": "planner",
"fragment": { "path": "fragments/plan-pre.md" },
"produces": ["HELLO.md"],
"consumes": [],
"onError": "skip"
}
],
"contributions": [],
"gates": []
}
```
@@ -97,11 +98,13 @@ A few things to notice:
- `version` is required in 1.6.0. Use semver.
- `engines.gsd` is a hard gate: GSD will refuse to install or load this capability on any version older than 1.6.0.
- `role: "feature"` means this capability adds optional behaviour to the loop — it is not a runtime descriptor.
- The single entry in `steps` attaches at `plan:pre`. `produces` tells the registry that this step writes `HELLO.md`, which lets the registry order hooks and detect unsatisfied dependencies in more complex setups.
- `onError: "skip"` means the loop continues even if this step fails. For a first capability that is the safe choice.
- `role: "feature"` means this capability adds optional behaviour to the loop — it is not a runtime descriptor. A `feature` capability must declare `runtimeCompat`; `{ "supported": ["*"] }` means "every runtime".
- This is a **contribution**, not a **step**. A contribution injects a prompt fragment into a named agent role (`into`) and needs no dispatch target. A step, by contrast, *must* carry a `ref` with exactly one of `skill`, `agent`, or `command` — so a fragment-only injection is always a contribution. That is why `steps` is left empty here.
- `into: "planner"` names the agent role that receives the fragment. `planner` is one of the roles published by the `plan:pre` extension point (alongside `researcher` and `checker`); the value must be a role that point publishes or the manifest fails validation.
- `produces` tells the registry that this contribution writes `HELLO.md`, which lets the registry order hooks and detect unsatisfied dependencies in more complex setups.
- `onError: "skip"` means the loop continues even if this contribution fails. For a first capability that is the safe choice.
No `ref.agent` or `ref.skill` is declared here because this is a fragment-only step: the planner receives the fragment text inline and acts on it. This keeps the capability completely declarative.
The fragment is referenced by `path`. At load time GSD reads the file and inlines its text into the registry, so the contribution carries the materialised content wherever the loop renders it. This keeps the capability completely declarative — no executable code is involved.
---
@@ -113,18 +116,21 @@ Install from the local path with `--scope project` so it is scoped only to this
gsd capability install ./capabilities/hello-note --scope project
```
You will see output similar to:
The command emits a JSON result:
```
Installing hello-note 0.1.0 …
Role : feature
Scope : project
Hooks : 1 (plan:pre step)
Executable surfaces : none
✔ hello-note installed.
```json
{
"status": "installed",
"id": "hello-note",
"version": "0.1.0",
"scope": "project",
"disclosure": [
"This capability ships no executable surfaces (declarative only)."
]
}
```
Because `hello-note` declares no executable surfaces (no hook scripts, no MCP servers, no command modules) GSD copies the files to the project capability ledger without displaying a consent prompt. That is intentional — declarative capabilities are safe to install without reviewing runnable code.
GSD copies the bundle into `.gsd/capabilities/hello-note/` and records it in the project ledger at `.gsd-capabilities.json`. Because `hello-note` declares no executable surfaces (no hook scripts, no MCP servers, no command modules) it installs without a consent prompt — the `disclosure` line confirms there was no runnable code to review. That is intentional: declarative capabilities are safe to install without reviewing executable code.
---
@@ -134,78 +140,102 @@ Because `hello-note` declares no executable surfaces (no hook scripts, no MCP se
gsd capability list
```
You will see at least one row for `hello-note`:
```
id version role scope status
hello-note 0.1.0 feature project enabled
```
You can also query the active hook set for the `plan:pre` point:
```bash
gsd capability hooks plan:pre
```
Expected output (abbreviated):
`list` emits a JSON array of every capability GSD can see — the first-party ones that ship with GSD, plus any you have installed. Your `hello-note` entry appears at the end:
```json
[
{
"capability": "hello-note",
"point": "plan:pre",
"kind": "step",
"produces": ["HELLO.md"],
"fragment": { "inline": "## Hello from hello-note\n…" }
}
]
{
"id": "hello-note",
"role": "feature",
"version": "0.1.0",
"tier": "standard",
"source": "./capabilities/hello-note",
"scope": "project",
"status": "active",
"reason": null,
"title": "Hello Note"
}
```
Notice that `fragment.inline` now contains the materialised text from `fragments/plan-pre.md`. The capability system inlined it at install time.
`status` is `active` — the capability is installed, compatible with your GSD version, and will fire. (The other status values are `incompatible`, when the host GSD version is outside the capability's `engines.gsd` range, and `inactive`, when a project-scoped capability has not been consented on this machine.)
---
## Step 6 — Trigger the loop step
Start a planning session. The planner will receive the `hello-note` fragment as part of its context:
```bash
gsd plan
```
Watch the planner output. You will see a line noting that `hello-note` contributed a `plan:pre` step. The planner will produce `HELLO.md` in your project's planning directory as directed by the fragment.
If you are running in an environment where the planner agent is not configured, you can inspect what the resolver would dispatch without running the full agent:
You can also query the active hook set for the `plan:pre` point:
```bash
gsd loop render-hooks plan:pre --raw
```
The JSON output will include your `hello-note` step with its inlined fragment, confirming that the capability is wired into the loop.
The envelope is `{ point, activeHooks, rendered }`. Your contribution appears in `activeHooks` (alongside any first-party hooks active at this point):
```json
{
"capId": "hello-note",
"kind": "contribution",
"into": "planner",
"fragment": {
"inline": "## Hello from hello-note\n\nThis planning session was started with the hello-note capability active.\nRecord a brief note in HELLO.md summarising the plan goal in one sentence.\n",
"path": "fragments/plan-pre.md"
},
"produces": ["HELLO.md"],
"onError": "skip"
}
```
Notice that `fragment.inline` now holds the materialised text from `fragments/plan-pre.md` — GSD inlined it at load time, while keeping the original `path` for reference. The top-level `rendered` field of the envelope contains the same fragment formatted as a `<contribution from="hello-note" into="planner">…</contribution>` block, which is what the planner actually receives.
---
## Step 7 — Disable the capability
## Step 6 — See the contribution reach the planner
When you want to stop the step from firing, disable the capability:
Planning is driven by a slash command, not a `gsd` subcommand. In your AI assistant, start a planning session for a phase with:
```bash
gsd capability disable hello-note
```text
/gsd:plan-phase
```
Run `gsd capability list` again. The `status` column will now show `disabled`. Run `gsd loop render-hooks plan:pre --raw` and you will see that `hello-note` is absent from the active hook set. Disabled capabilities are removed from the resolver output by construction — there is nothing feature-specific for the loop to run.
When the planner runs, the `plan:pre` hook set is rendered into its prompt, so it receives the `hello-note` contribution and, following the fragment's instruction, records a one-line note in `HELLO.md`.
To re-enable it:
You do not need to run a full planning session to confirm the wiring, though. The `loop render-hooks` command shows exactly what the loop would hand the planner — the same output you saw in Step 5:
```bash
gsd capability enable hello-note
gsd loop render-hooks plan:pre --raw
```
Find `hello-note` in `activeHooks` and read the `rendered` field: the `<contribution from="hello-note" into="planner">` block is the literal text the planner receives. That confirms the capability is wired into the loop, without dispatching a single agent.
---
## Step 7 — Remove the capability
When you want to stop the contribution from firing, remove the capability from the project:
```bash
gsd capability remove hello-note --scope project
```
This emits a JSON result describing what was removed:
```json
{
"status": "removed",
"id": "hello-note",
"scope": "project",
"removedFiles": [
".gsd/capabilities/hello-note"
],
"strippedEdits": 0,
"dataPreserved": true
}
```
Run `gsd capability list` again and `hello-note` is gone from the array. Run `gsd loop render-hooks plan:pre --raw` and you will see it is absent from `activeHooks`: a removed capability contributes nothing to the loop.
Removing the installed bundle does not touch the source folder you authored under `capabilities/hello-note/` — that is your copy. To reinstall, just run the Step 4 command again.
---
## You have built your first capability
You scaffolded a capability folder, wrote a manifest with a single `plan:pre` step, installed it into a project-scoped ledger without a trust prompt, confirmed it in the active hook set, watched it contribute to the planning loop, and disabled it cleanly.
You scaffolded a capability folder, wrote a manifest with a single `plan:pre` contribution, installed it into a project-scoped ledger without a trust prompt, confirmed it in the active hook set, saw it reach the planning loop, and removed it cleanly.
The capability you built is fully declarative: it owns a prompt fragment and a hook declaration, and no executable code was involved at any point.

View File

@@ -0,0 +1,291 @@
# Install Your First Capability
In this tutorial you will install a third-party GSD capability into a project, grant it consent, confirm it is active, check whether a newer version is available, and remove it again. By the end you will have driven the whole consumer-side lifecycle once, from the command line, with every step working.
This is the *install* side of capabilities. If you want to *author* one, see [Build your first capability](build-your-first-capability.md) — that tutorial builds a capability; this one consumes one.
So that the lesson is self-contained and reproducible offline, you will first create a tiny capability bundle on disk, then install it from a local path exactly as you would install any third-party capability. The capability is called `acme-greet`. It declares a single lifecycle **hook** — an executable surface — so that you see the consent gate fire for real.
---
## Before you begin
You need:
- **GSD 1.6.0 or later** (`gsd --version`). Capability install and management is a 1.6.0 feature, and the capability you build below declares `engines.gsd: ">=1.6.0"`. On an older host the install **hard-blocks** with an `engines` error before anything is staged — it does not partially install. If `gsd --version` reports an earlier version, upgrade GSD before continuing.
- A throwaway working directory. Create one now:
```bash
mkdir ~/cap-consumer-demo && cd ~/cap-consumer-demo
```
You will work inside `~/cap-consumer-demo` for the rest of this tutorial. You do **not** need to run `gsd init` or have an existing settings file — the install in Step 2 creates the host settings file (and its parent directory) for you when you pass `--shared-file`.
---
## Step 1 — Create the capability bundle you will install
A third-party capability is a folder containing a `capability.json` manifest and its declared files. Create one now:
```bash
mkdir -p ./acme-greet/hooks
```
Write the manifest at `./acme-greet/capability.json`:
```json
{
"id": "acme-greet",
"role": "feature",
"version": "1.0.0",
"title": "Acme Greeter",
"description": "Prints a greeting on a lifecycle event.",
"tier": "standard",
"requires": [],
"engines": { "gsd": ">=1.6.0" },
"runtimeCompat": { "supported": ["*"], "unsupported": [] },
"skills": [],
"agents": [],
"config": {},
"hooks": [
{ "event": "Stop", "script": "hooks/greet.sh" }
],
"steps": [],
"contributions": [],
"gates": []
}
```
The `"engines": { "gsd": ">=1.6.0" }` line is the host-compatibility gate: GSD checks it against your running version at install time, and an older host is hard-blocked with an `engines` error (see [Before you begin](#before-you-begin)). Leave it as-is.
Write the hook script it declares at `./acme-greet/hooks/greet.sh`:
```bash
cat > ./acme-greet/hooks/greet.sh << 'EOF'
#!/usr/bin/env bash
echo "Hello from acme-greet"
EOF
chmod +x ./acme-greet/hooks/greet.sh
```
You now have a complete, installable bundle:
```text
~/cap-consumer-demo/
acme-greet/
capability.json
hooks/
greet.sh
```
Because `acme-greet` declares a `hooks` entry, it has an **executable surface**: installing it would register a script that runs on a lifecycle event. GSD will not activate that without your explicit consent. That is the gate you will see next.
---
## Step 2 — Try to install it, and meet the consent gate
Install from the local path with `--scope project`, so the capability is scoped to this project only. Because the capability declares a runtime hook, also pass `--shared-file .claude/settings.json` — that tells GSD **which** host settings file to splice the hook registration into. (`--shared-file` is relative to the scope root, which for `--scope project` is your project directory. The file does not need to exist yet: when the install actually writes the hook, GSD creates `.claude/settings.json` and its parent directory if absent, and merges the hook into whatever is already there otherwise.) Without it, the bundle would still be staged, but its hook would never be wired into any runtime config — see Step 5:
```bash
gsd capability install ./acme-greet --scope project --shared-file .claude/settings.json
```
The install does **not** complete. You will see a disclosure of the executable surface and a prompt to grant consent, similar to:
```
Error: This capability declares executable surfaces and needs your consent before install:
This capability ships executable surfaces that will run in your agent runtime:
hooks (1): run as runtime hook commands
- Stop -> hooks/greet.sh
Re-run with --yes to grant consent and install.
```
This is intentional and is the heart of the capability trust model: **install never runs capability code**. The bundle is first copied into an isolated staging directory and its manifest is validated — still without executing anything — and then, before the capability is activated, any executable surface it declares is disclosed and must be consented to. Consent gates *activation*: nothing is promoted into place, no ledger entry or consent record is committed, and no host settings file is touched until you grant it. To understand why GSD draws the line here, read [The capability trust model](../explanation/capability-trust-model.md).
---
## Step 3 — Grant consent and install
Re-run the same command with `--yes` to grant consent for the disclosed surface:
```bash
gsd capability install ./acme-greet --scope project --shared-file .claude/settings.json --yes
```
This time the install completes. You will see a confirmation naming the capability, its version, the scope, and the executable surface you consented to:
```json
{
"status": "installed",
"id": "acme-greet",
"version": "1.0.0",
"scope": "project",
"disclosure": [
"This capability ships executable surfaces that will run in your agent runtime:",
" hooks (1): run as runtime hook commands",
" - Stop -> hooks/greet.sh"
]
}
```
Three things happened. The bundle was copied into the project's capability root at `.gsd/capabilities/acme-greet/`; the declared `Stop` hook was spliced into the `--shared-file` you named (`.claude/settings.json`); and — because this is a project-scope install — a **consent record** was written to your user-owned consent store at `${GSD_HOME:-~}/.gsd/consent.json`, bound to this project and this exact bundle. That record, not the in-repo ledger, is what lets the capability activate on this machine. The reasoning behind that split is explained in [The capability trust model](../explanation/capability-trust-model.md#the-project-scope-trust-boundary).
---
## Step 4 — Confirm it loaded
List the capabilities visible to this project:
```bash
gsd capability list
```
`list` emits a JSON array. The first-party capabilities are listed first; your installed overlay `acme-greet` appears as the last entry, with `source: "./acme-greet"`, the `project` scope, and `status: "active"`:
```json
{
"id": "acme-greet",
"role": "feature",
"version": "1.0.0",
"tier": "standard",
"source": "./acme-greet",
"scope": "project",
"status": "active",
"reason": null,
"title": "Acme Greeter"
}
```
`status: "active"` is the signal that the capability is both compatible with your GSD version *and* backed by a consent record on this machine. Had you copied a bundle into `.gsd/capabilities/` by hand — with no consent record — the same row would read `status: "inactive"` with a `reason`, and the capability would contribute nothing.
You can also inspect what you consented to. List your project consent records:
```bash
gsd capability trust list
```
You will see one record for `acme-greet`, keyed by the project root, recording the bundle integrity and disclosure signature you approved:
```json
{
"id": "acme-greet",
"scope": "project",
"projectRoot": "/Users/you/cap-consumer-demo",
"integrity": "",
"disclosureSignature": "…",
"contentHash": "…",
"consentedAt": "2026-06-20T12:00:00.000Z"
}
```
(The `integrity` field is empty for a local install — a directory has no single hashable artifact — but the `contentHash` still binds the record to the exact bundle content you installed.)
---
## Step 5 — Confirm the hook was registered
Because you installed with `--shared-file .claude/settings.json`, GSD spliced the capability's `Stop` hook into that file at install time. That is the step that actually wires the hook into the runtime — installing the bundle alone does **not** register a hook; only the `--shared-file` splice does. Look at the file:
```bash
cat .claude/settings.json
```
You will see a `hooks.Stop` entry stamped with a `_gsdCapability` marker naming the owning capability, whose `command` is the realpath-confined absolute path to the bundle's own `greet.sh`:
```json
{
"hooks": {
"Stop": [
{
"_gsdCapability": "acme-greet",
"hooks": [
{ "type": "command", "command": "'/Users/you/cap-consumer-demo/.gsd/capabilities/acme-greet/hooks/greet.sh'" }
]
}
]
}
}
```
That entry is what makes the `Stop` hook run — printing `Hello from acme-greet` — the next time the runtime fires its `Stop` lifecycle event. The `_gsdCapability` marker is also what lets `remove` strip *exactly* this entry later without touching anything else in `settings.json` (you will see that in Step 7).
Had you installed **without** `--shared-file`, the bundle would still be on disk and `list` would still show it `active`, but `.claude/settings.json` would carry no `Stop` entry — the hook would be declared but never wired in. The `--shared-file` flag is what turns a declared hook into a registered one.
> **`disable`/`enable` do not apply to an installed overlay.** Those verbs validate the id against GSD's **built-in** capability registry, which does not contain capabilities you installed yourself. Running `gsd capability disable acme-greet` fails:
>
> ```text
> capability set: error: unknown capability: "acme-greet"
> Error: capability set: 1 error(s) — see above
> ```
>
> `disable`/`enable`/`set` are for first-party capabilities. The off-switch for an installed overlay like `acme-greet` is `gsd capability remove` — which you will use in Step 7. (For the difference between the two paths, see [Turn a capability off](../how-to/turn-a-capability-off.md).) For now, leave `acme-greet` installed.
---
## Step 6 — Check whether an update is available
Ask GSD whether any installed overlay capability has a newer version available:
```bash
gsd capability outdated
```
This prints a table with one row per installed overlay capability. For a **local** source, `outdated` re-reads the `capability.json` at the recorded path and compares its version with the installed one. The bundle you installed from is still on disk at version `1.0.0`, so the row reports `current` — there is nothing newer to fetch:
```
ID Source Current Latest Status
---------- ------ ------- ------ -------
acme-greet local 1.0.0 1.0.0 current
```
For a capability installed from a git URL or npm, `outdated` performs a metadata-only remote peek instead and reports `outdated`, `pinned`, or — when the source cannot be auto-checked — `manual` or `unknown`. It never re-clones or re-extracts a bundle, and a single failing peek never crashes the command. See the [`outdated` reference](../reference/gsd-capability-command.md#outdated) for the full per-source matrix.
---
## Step 7 — Remove it
Remove the capability completely. Because you installed it with `--scope project`, you must remove it from the same scope — `remove` defaults to `global`, so pass `--scope project` here too:
```bash
gsd capability remove acme-greet --scope project
```
(Omitting `--scope` would look in the `global` scope and report `capability "acme-greet" is not installed in global scope`.)
You will see a confirmation listing exactly what was removed:
```json
{
"status": "removed",
"id": "acme-greet",
"scope": "project",
"removedFiles": [
".gsd/capabilities/acme-greet"
],
"strippedEdits": 1,
"dataPreserved": true
}
```
`strippedEdits` is the **count** of marker-isolated fragments stripped from shared files — `1` here, because removal excised the `Stop` hook entry you saw in `.claude/settings.json` in Step 5. Removal strips **only** entries carrying this capability's `_gsdCapability` marker, so anything else in that file (your own hooks, other settings) is left untouched. `dataPreserved` is `true` because you did not pass `--purge-data` — any runtime data the capability created would be left in place; add `--purge-data` to delete it too.
Removal does three things, leaving no orphaned state: it deletes the bundle from `.gsd/capabilities/` and strips its hook entry from `.claude/settings.json`, removes the ledger entry, and — because this was a project-scope capability — **revokes the consent record** in your consent store. Run `cat .claude/settings.json` and the `acme-greet` `Stop` entry is gone; run `gsd capability trust list` again and the `acme-greet` record is gone; run `gsd capability list` and the `acme-greet` row is gone.
---
## You have installed your first capability
You created a third-party capability bundle, hit the consent gate on an executable surface, granted consent and installed it project-scoped, confirmed it activated by both `list` and `trust list`, checked for updates, and removed it cleanly — consent and all.
The lifecycle you just drove — *disclose, consent, activate, audit, revoke* — is the same one you would use for any capability fetched from a git URL, an npm package, or a tarball. The only difference is where the bundle comes from.
---
## Where next
- [Import a capability from a URL](../how-to/import-a-capability-from-a-url.md) — install a third-party capability from a git URL, tarball, or npm package.
- [Turn a capability off](../how-to/turn-a-capability-off.md) — disable a capability or gate a single one of its hooks.
- [Remove a capability](../how-to/remove-a-capability.md) — the full removal task, including `--purge-data`.
- [The capability trust model](../explanation/capability-trust-model.md) — *why* install never runs code, and how consent and integrity work.
- [How overlay capabilities compose](../explanation/capability-overlay-model.md) — *why* first-party always wins and how precedence is resolved.
- [`gsd capability` command reference](../reference/gsd-capability-command.md) — every subcommand, flag, and output shape.