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:
@@ -1135,8 +1135,8 @@ gsd capability install ./my-cap --scope project # Install a local capabilit
|
||||
gsd capability install npm:@org/gsd-cap-x@^1 --yes # Install from npm, granting executable-surface consent
|
||||
gsd capability update my-cap # Upgrade from its recorded source
|
||||
gsd capability outdated --json # Which installed overlays have a newer version?
|
||||
gsd capability disable my-cap # Turn it off without removing it
|
||||
gsd capability remove my-cap # Remove the overlay capability
|
||||
gsd capability disable ui # Turn a FIRST-PARTY capability off (disable/enable/set are first-party only)
|
||||
gsd capability remove my-cap --scope project # Turn the installed overlay off — remove it from the scope it was installed in
|
||||
```
|
||||
|
||||
**Programmatic access:** `node gsd-tools.cjs capability <subcommand>` — see [CLI Tools Reference](CLI-TOOLS.md).
|
||||
@@ -1672,9 +1672,11 @@ The check is also run as part of `npm test` via `tests/enh-2789-description-budg
|
||||
|
||||
## Capability commands (third-party)
|
||||
|
||||
A capability can ship its own command family by declaring `commands: [{ family, module, router }]` in its `capability.json` (ADR-1244 D7). Once the capability is **installed and consented** (a committed entry exists in the per-runtime `.gsd-capabilities.json` ledger), running `gsd-tools <family> …` (equivalently the `gsd <family>` wrapper) dispatches to the capability's router. The first-party families `graphify`, `intel`, and `audit-uat`/`audit-open` use exactly this registry-driven seam.
|
||||
A capability can ship its own command family by declaring `commands: [{ family, module, router }]` in its `capability.json` (ADR-1244 D7). Once the capability is **active**, running `gsd-tools <family> …` (equivalently the `gsd <family>` wrapper) dispatches to the capability's router. The first-party families `graphify`, `intel`, and `audit-uat`/`audit-open` use exactly this registry-driven seam.
|
||||
|
||||
Dispatch is gated for safety: the router module is loaded **only from the capability's own install root** (a bare `.cjs` basename, traversal- and symlink-confined), and a capability that is merely present on disk **without** a committed ledger entry is **not** command-dispatchable (its declarative skills/agents/config still load). A project-scoped capability's commands are only as trustworthy as the repository they ship in — see [The capability trust model](explanation/capability-trust-model.md).
|
||||
For a **project-scoped** third-party capability, "active" is decided by the **user-owned consent store** (`${GSD_HOME:-~}/.gsd/consent.json`), not by the in-repo ledger. Since #1459, the authoritative project-scope activation gate is a consent record on **this machine**, bound to the project root and the exact bundle content; a forged or cloned in-repo `.gsd-capabilities.json` ledger that *looks* committed activates nothing on its own — see [The capability trust model](explanation/capability-trust-model.md#the-project-scope-trust-boundary). A **global** capability (under your own home) is trusted without a per-project record.
|
||||
|
||||
Command dispatch is then gated **twice**. Beyond that primary activation gate, the router module is loaded **only from the capability's own install root** (a bare `.cjs` basename, traversal- and symlink-confined), and dispatch additionally requires a **committed** (non-`_pending`) entry in the per-runtime `.gsd-capabilities.json` ledger — a *secondary* signal that the install actually completed. A capability that is merely present on disk without a committed ledger entry is not command-dispatchable; a project-scoped one is not even *active* without the consent record. (A project ledger lives in the repo tree and is only as trustworthy as the repository — which is precisely why the consent store, not the ledger, is the project-scope activation gate.)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ Language versions: [English](README.md) · [Português (pt-BR)](pt-BR/README.md)
|
||||
|
||||
- [Your first project](tutorials/your-first-project.md) — install to first shipped phase, one guaranteed path
|
||||
- [Onboarding an existing codebase](tutorials/onboarding-an-existing-codebase.md) — bring GSD Core to a brownfield repo
|
||||
- [Build your first capability](tutorials/build-your-first-capability.md) — author a tiny declarative capability and watch it act in the loop
|
||||
- [Install your first capability](tutorials/install-your-first-capability.md) — install a third-party capability end-to-end: consent, verify, check for updates, remove
|
||||
|
||||
---
|
||||
|
||||
@@ -69,6 +71,7 @@ Language versions: [English](README.md) · [Português (pt-BR)](pt-BR/README.md)
|
||||
- [Multi-agent orchestration](explanation/multi-agent-orchestration.md) — how subagents are spawned, scoped, and coordinated
|
||||
- [Security model](explanation/security-model.md) — trust boundaries, permissions, and safe automation
|
||||
- [The capability trust model](explanation/capability-trust-model.md) — why third-party capabilities are gated by consent + integrity + reversibility, not a sandbox
|
||||
- [How overlay capabilities compose](explanation/capability-overlay-model.md) — why first-party always wins and how the loader resolves precedence, conflicts, and fail-closed gates
|
||||
- [Architecture](ARCHITECTURE.md) — system architecture, agent model, and data flow
|
||||
- [Discuss modes](workflow-discuss-mode.md) — assumptions mode vs interview mode for `/gsd-discuss-phase`
|
||||
- [Context monitoring](context-monitor.md) — context window monitoring hook architecture
|
||||
|
||||
285
docs/explanation/capability-overlay-model.md
Normal file
285
docs/explanation/capability-overlay-model.md
Normal file
@@ -0,0 +1,285 @@
|
||||
# How overlay capabilities compose
|
||||
|
||||
> **Explanation** — This document describes *why* GSD composes first-party and
|
||||
> third-party capabilities the way it does, and *what the precedence and conflict
|
||||
> rules are*. It is not a step-by-step guide; for the consumer lifecycle see
|
||||
> [Install your first capability](../tutorials/install-your-first-capability.md),
|
||||
> and for the field-level rules see the
|
||||
> [capability manifest reference](../reference/capability-manifest.md). For the
|
||||
> security side of the same boundary, see
|
||||
> [the capability trust model](capability-trust-model.md). For the decision
|
||||
> record, see
|
||||
> [ADR-1244 D2](../adr/1244-capability-ecosystem.md#d2--runtime-capability-registry-overlay).
|
||||
|
||||
---
|
||||
|
||||
## The central idea: the registry is a module, not a data file
|
||||
|
||||
GSD's capabilities — first-party and third-party alike — are described by a single
|
||||
**capability registry**: a composed object that every consumer (the loop resolver,
|
||||
the config loader, the surface command, `gsd capability list`) reads to learn which
|
||||
skills, agents, config keys, and loop hooks exist.
|
||||
|
||||
The first-party registry is *frozen and generated*: it is built at release time from
|
||||
the shipped `capabilities/*/capability.json` manifests into a committed
|
||||
`capability-registry.cjs`, and it never changes at runtime. Third-party capabilities
|
||||
cannot be baked into that file — they are installed on the user's machine, after the
|
||||
release. So the registry is not consumed as a static data file. It is consumed through
|
||||
a function:
|
||||
|
||||
```text
|
||||
loadRegistry({ includeInstalled: true }) → composed registry
|
||||
```
|
||||
|
||||
`loadRegistry` reads the frozen first-party registry and, when asked, composes a
|
||||
**validated installed overlay** on top of it: the third-party capability manifests
|
||||
found at runtime under the per-scope install roots. The result is one registry that
|
||||
covers first-party and third-party capabilities identically — every derived view
|
||||
(`bySkill`, `byAgent`, `byLoopPoint`, `configKeys`, the cluster map) spans both. The
|
||||
whole point of the overlay model is that an installed capability is *not* a
|
||||
second-class citizen: once it composes cleanly, it participates in the loop exactly
|
||||
as a shipped one does.
|
||||
|
||||
The interesting question is everything that can go wrong while composing two sources
|
||||
that were authored independently — and what GSD does about each case. That is the rest
|
||||
of this document.
|
||||
|
||||
---
|
||||
|
||||
## The activation chain
|
||||
|
||||
Before a third-party capability contributes anything to your loop, it passes through
|
||||
four distinct stages. They are worth naming because they fail in different ways and at
|
||||
different times — and the order matters: **the consent gate runs during composition,
|
||||
before surface and config**, not after them.
|
||||
|
||||
1. **Install** writes the capability into a scope root and records it in the ledger.
|
||||
This is the lifecycle's job; it never runs capability code (see the trust model).
|
||||
The capability now exists *on disk*.
|
||||
2. **Load / compose (with the project-scope consent gate)** is what `loadRegistry`
|
||||
does. As it composes each overlay it applies the composition gates — id/skill/agent/
|
||||
config/family collisions, the `engines.gsd` re-check, and, for a *project-scoped*
|
||||
overlay, **the project-scope consent gate**. That gate runs *inside* `loadRegistry`,
|
||||
before any of the overlay's fragments are even materialised: a project overlay is
|
||||
inert (discovered-but-inactive) until a matching record exists in your user-owned
|
||||
consent store. This is the security gate described in
|
||||
[the trust model](capability-trust-model.md#the-project-scope-trust-boundary). A
|
||||
capability that fails any composition gate — consent included — never enters the
|
||||
registry the rest of GSD reads, so it cannot reach the later stages at all.
|
||||
3. **Surface** decides which of the *composed* registry's skills are projected into the
|
||||
host runtime. This is the install-profile and `/gsd:surface` layer — a capability's
|
||||
skills can be on the surface or held back without uninstalling it. It only ever sees
|
||||
capabilities that already cleared composition.
|
||||
4. **Config activation** decides, per loop hook, whether it fires. A hook's `when`
|
||||
key (a dotted config key) gates it: a `step` or `gate` whose key is falsy does not
|
||||
run. This is the `gsd capability set <id> --gate <key>=<bool>` and `/gsd:settings`
|
||||
layer — again, only for capabilities that survived composition.
|
||||
|
||||
This document is about what `loadRegistry` does at the moment of composition — stage 2,
|
||||
which sits between install and the later surface/config stages and contains the consent
|
||||
gate. A capability that is installed but skipped at composition (including for missing
|
||||
consent) never reaches the surface or config stages, because it is not in the registry
|
||||
the rest of GSD reads.
|
||||
|
||||
---
|
||||
|
||||
## Where overlays come from, and the order they are considered
|
||||
|
||||
`loadRegistry` scans two install roots, in this order:
|
||||
|
||||
- **Global** — `$GSD_HOME/.gsd/capabilities/<id>/` (where `GSD_HOME` defaults to your
|
||||
home directory). This is under your own control and is trusted without a per-project
|
||||
record.
|
||||
- **Project** — `<projectRoot>/.gsd/capabilities/<id>/`. This lives inside a repository
|
||||
and is therefore only as trustworthy as the repository; it is gated by the consent
|
||||
store.
|
||||
|
||||
The roots are deduplicated by their *canonical* (symlink-resolved) physical path, so a
|
||||
single directory is never scanned twice — and, crucially, so a symlinked `GSD_HOME`
|
||||
that physically *is* the project root cannot smuggle an in-repo bundle into the trusted
|
||||
global slot. When the global and project roots resolve to the same physical directory
|
||||
(or distinctness cannot be proven), the surviving scope escalates to the more
|
||||
restrictive `project` — consent-required. This is a deliberately conservative choice:
|
||||
when GSD cannot prove a global root is distinct from your project tree, it treats it as
|
||||
project-scoped rather than risk granting trusted-global activation to repo-plantable
|
||||
content.
|
||||
|
||||
Within this ordering, the composition rules below decide which overlays survive.
|
||||
|
||||
---
|
||||
|
||||
## First-party always wins
|
||||
|
||||
The single load-bearing precedence rule is: **first-party always wins.** When a
|
||||
third-party overlay collides with a first-party capability, the overlay is rejected —
|
||||
never the other way round.
|
||||
|
||||
Collision is defined broadly, because impersonation can happen along several axes. An
|
||||
overlay is rejected if it collides on any of:
|
||||
|
||||
- **`id`** — the capability identifier. Two capabilities cannot share an id; a
|
||||
first-party id always keeps it.
|
||||
- **A skill or agent stem** — exactly one capability may own each skill/agent stem
|
||||
across the entire merged registry. An overlay that claims a stem already owned
|
||||
(by first-party *or* by an already-accepted overlay) is rejected.
|
||||
- **A federated config key** — a key declared in the overlay's `config` slice that
|
||||
already exists in the central config schema or in another capability's slice.
|
||||
- **A command family** — the `family` of a declared command module, if another
|
||||
capability already owns it.
|
||||
|
||||
Two further rules protect the first-party namespace directly:
|
||||
|
||||
- **Reserved prefixes.** The `gsd-`, `gsd-core-`, and `anthropic-` id prefixes are
|
||||
reserved. An overlay whose id begins with one is rejected outright — a third party
|
||||
cannot publish `gsd-security` and borrow the implicit trust of the GSD namespace.
|
||||
- **Cross-capability invariants.** Each candidate overlay is added to the merged
|
||||
capability map and the *full* cross-capability validation suite (contract roles,
|
||||
`consumes`-satisfiability, owner uniqueness, config-key exclusivity, `requires`
|
||||
acyclicity and tier-monotonicity) is re-run. First-party alone is always clean, so
|
||||
any new error is provably the candidate's fault, and the candidate is dropped.
|
||||
|
||||
### Why this asymmetry
|
||||
|
||||
The asymmetry is intentional and follows directly from the trust model's central
|
||||
thesis — *artifact parity is not trust parity*. A third-party capability is allowed to
|
||||
ship the same kinds of artifacts as GSD Core, but first-party capabilities carry an
|
||||
authority third-party ones do not: their provenance is the GSD release process itself.
|
||||
If a collision could let an overlay shadow a first-party skill, agent, or command, then
|
||||
installing a capability could silently *replace* a shipped behaviour — the install would
|
||||
be the attack. By making first-party unconditionally win every collision, GSD
|
||||
guarantees that no installed capability can ever redefine what GSD Core does. An overlay
|
||||
can only *add*; it can never *override*.
|
||||
|
||||
---
|
||||
|
||||
## When a single overlay fails: skip, don't crash
|
||||
|
||||
Overlays are untrusted, independently authored, and read at runtime from a possibly
|
||||
repo-plantable directory. A malformed one must never bring down the loop. So the second
|
||||
rule of composition is: **a bad overlay is skipped with a warning; the loop always gets
|
||||
a usable registry.**
|
||||
|
||||
A capability is skipped (and a warning recorded in the registry's `_overlay.warnings`)
|
||||
for any of these reasons:
|
||||
|
||||
- its `capability.json` is missing, unreadable, non-regular (a planted FIFO/device), or
|
||||
oversized;
|
||||
- it fails structural or cross-capability validation;
|
||||
- it collides with first-party or an already-accepted overlay (the precedence rule
|
||||
above);
|
||||
- its `engines.gsd` range does not satisfy the running GSD version (the load-time
|
||||
re-gate, which mirrors the install-time gate so an upgrade of GSD itself can retire an
|
||||
incompatible overlay);
|
||||
- it carries an in-flight `_pending` install/upgrade marker (deferred until
|
||||
reconciliation completes);
|
||||
- (for a project overlay) it has no matching consent record on this machine — it is
|
||||
*discovered but inactive*.
|
||||
|
||||
The composition body is total: even an unexpected throw from a validator or a
|
||||
fragment-materialisation step is caught per-candidate, turned into a skip, and the next
|
||||
candidate is processed. A single broken overlay cannot poison the rest of the set.
|
||||
|
||||
---
|
||||
|
||||
## The one place where skipping is dangerous: gates
|
||||
|
||||
Skipping a broken overlay is the safe default for most surfaces — but not for *gates*.
|
||||
|
||||
A capability's loop hooks come in three kinds:
|
||||
|
||||
- a **step** adds an independent unit of work at an extension point;
|
||||
- a **contribution** injects a prompt fragment into an agent role;
|
||||
- a **gate** checks a condition and can *block* the loop from proceeding.
|
||||
|
||||
For steps and contributions, skipping a capability means the loop simply proceeds
|
||||
**without** that addition. That is *fail-open*, and it is correct: the loop is missing an
|
||||
optional step, not doing something unsafe.
|
||||
|
||||
A gate is the opposite. The whole purpose of a gate is to *stop* the loop when a
|
||||
condition is not met — a deploy gate, a house-style verification gate, a safety check. If
|
||||
GSD skipped a broken gate-declaring capability and proceeded, it would behave exactly as
|
||||
if the gate had *passed* — silently waving through the very thing the gate existed to
|
||||
block. That is a fail-open on a security-relevant control, and it is unacceptable.
|
||||
|
||||
So composition treats gates asymmetrically from steps and contributions. When a
|
||||
capability that declares a gate is skipped, GSD records its gate points in
|
||||
`_overlay.incompatibleGateCapIds` and `_overlay.blockedGates`, and the loop resolver
|
||||
**injects a synthetic blocking gate** at each of those extension points. The loop
|
||||
**fails closed**: rather than proceed as if the gate passed, it halts with a message
|
||||
naming the skipped capability and why its gate could not be evaluated.
|
||||
|
||||
The discriminator is therefore *not* "is this overlay broken?" but "what does failing
|
||||
to load it mean?" — and for a gate, failing to load it means you must not proceed.
|
||||
|
||||
---
|
||||
|
||||
## When the whole compose fails: fall back to first-party
|
||||
|
||||
There is one more failure layer above the per-candidate skip. A set of overlays can
|
||||
each pass every per-candidate check yet still trip a stricter whole-set check when the
|
||||
canonical builder (`buildRegistry`) materialises the merged registry — a topological
|
||||
cycle that only appears across the combined set, a config-slice shape problem, a format
|
||||
mismatch. An unguarded failure there would crash every consumer of the registry.
|
||||
|
||||
The fallback is uncompromising: if the whole-set build fails, GSD **discards every
|
||||
overlay** and returns the frozen first-party registry, plus a warning recording why. The
|
||||
loop keeps running with exactly the shipped capabilities and none of the overlays. Two
|
||||
details make this safe rather than merely convenient:
|
||||
|
||||
- Every accepted overlay's **command root is cleared**, so no dropped overlay can leave
|
||||
behind a path that a runtime dispatcher might `require()` a command module from.
|
||||
- Every dropped overlay's **gates are recorded as blocked** — using the same extraction
|
||||
as the per-candidate path — so a gate-declaring overlay that vanishes in the fallback
|
||||
still **fails closed**, never open.
|
||||
|
||||
The principle is the same at every layer: when GSD cannot compose an overlay, it removes
|
||||
the overlay's *additions* but never weakens a *control*.
|
||||
|
||||
---
|
||||
|
||||
## Why compose through one builder
|
||||
|
||||
A subtle but important design choice: the merged registry is materialised by the **same**
|
||||
`buildRegistry` function that produces the first-party registry, run over a map of
|
||||
first-party capabilities *plus* the accepted overlays. GSD does not have one code path
|
||||
that builds the first-party views and a separate path that bolts overlay views on.
|
||||
|
||||
The reason is drift. Every derived view — `bySkill`, `byLoopPoint`, the config schema,
|
||||
the cluster map, profile membership — is a projection of the capability set. If overlays
|
||||
were projected by a different builder, those projections could diverge from the
|
||||
first-party ones in subtle ways, and an overlay capability might behave *almost* like a
|
||||
first-party one but not quite. By forcing both through the single canonical builder, GSD
|
||||
guarantees that an accepted overlay is indistinguishable from a first-party capability in
|
||||
every derived view — which is exactly the artifact-parity promise the platform makes.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
The overlay model rests on a few rules applied consistently:
|
||||
|
||||
- The registry is composed at runtime by `loadRegistry`, not read as a static file.
|
||||
- **First-party always wins** every collision — id, skill/agent stem, config key,
|
||||
command family, reserved prefix. An overlay can only add, never override.
|
||||
- A bad overlay is **skipped, not crashed** — the loop always gets a usable registry.
|
||||
- Skipping **fails open** for steps and contributions (a missing optional addition) but
|
||||
**fails closed** for gates (a missing control must block, not pass).
|
||||
- A whole-set compose failure **falls back to first-party**, clearing command roots and
|
||||
still blocking dropped gates.
|
||||
- One canonical builder materialises both first-party and overlay views, so an accepted
|
||||
overlay has true parity with a shipped capability.
|
||||
|
||||
Every one of these choices answers the same question — *what does it mean if this
|
||||
composition step fails?* — and resolves it in favour of first-party authority and a
|
||||
fail-closed security posture.
|
||||
|
||||
---
|
||||
|
||||
## Related documents
|
||||
|
||||
- [ADR-1244 D2 — Runtime Capability Registry overlay](../adr/1244-capability-ecosystem.md#d2--runtime-capability-registry-overlay)
|
||||
- [The capability trust model](capability-trust-model.md) — the security side of the same boundary
|
||||
- [Capability Overlay (Configuration)](../CONFIGURATION.md#capability-overlay-installed-third-party-capabilities) — the operator-facing view of the same rules
|
||||
- [Capability manifest reference](../reference/capability-manifest.md) — the field-level conformance invariants
|
||||
- [`gsd capability` command reference](../reference/gsd-capability-command.md)
|
||||
- [Install your first capability](../tutorials/install-your-first-capability.md)
|
||||
@@ -53,6 +53,7 @@ At minimum, a feature Capability declares:
|
||||
{
|
||||
"id": "example",
|
||||
"role": "feature",
|
||||
"version": "0.1.0",
|
||||
"title": "Example",
|
||||
"description": "Adds an example planning step.",
|
||||
"tier": "standard",
|
||||
|
||||
@@ -2,17 +2,21 @@
|
||||
|
||||
This guide covers two distinct operations: **removing** a capability (deletes its files and cleans up all shared configuration it wrote) and **disabling** a capability (toggles it off without touching any files). Choose the one that fits your intent.
|
||||
|
||||
> **Which one applies depends on where the capability came from.** `disable`/`enable` work **only** on first-party capabilities shipped inside GSD. An **installed third-party overlay** (added with `gsd capability install …`) cannot be disabled — its only off-switch is `remove` (re-install to restore it).
|
||||
|
||||
---
|
||||
|
||||
## Disable a capability (reversible, files kept)
|
||||
## Disable a first-party capability (reversible, files kept)
|
||||
|
||||
If you want to stop a capability from participating in the loop but may want it back later, disable it:
|
||||
`disable`/`enable`/`set` are for **first-party** capabilities only — the ones that ship inside GSD (for example `ui`, `code-review`, `research`). They validate `<id>` against GSD's **build-time** capability registry, so an **installed third-party overlay** (anything you added with `gsd capability install …`) is **not** in that registry and is rejected with `unknown capability: "<id>"`. For an installed overlay there is no `disable`; the off-switch is `remove` (and you re-install to bring it back) — see [Remove a capability](#remove-a-capability) below.
|
||||
|
||||
If you want to stop a **first-party** capability from participating in the loop but may want it back later, disable it:
|
||||
|
||||
```bash
|
||||
gsd capability disable <id>
|
||||
```
|
||||
|
||||
Disabling is a toggle: no files are deleted, no shared configuration is modified. The capability's hooks stop firing, its skills leave the active surface, and its command modules stop responding. To re-activate it:
|
||||
Disabling is a toggle: no files are deleted, no shared configuration is modified. It acts on the **runtime surface and hook activation** of a skill-owning first-party capability: the capability's hooks stop firing and its skills leave the active surface. Disabling does **not** unregister first-party command families — those are dispatched from the generated capability registry, which `disable` does not consult, so any commands the capability owns continue to respond. To re-activate the surface and hooks:
|
||||
|
||||
```bash
|
||||
gsd capability enable <id>
|
||||
@@ -43,7 +47,7 @@ GSD uses the **ledger** — a per-runtime record written at install time (for ex
|
||||
### What is NOT removed
|
||||
|
||||
- **Shared files themselves.** Files such as `settings.json` and `hooks.json` are edited in place, not deleted. Only the capability's specific entries are excised.
|
||||
- **Persistent capability data.** Any data the capability wrote during use (databases, caches, runtime artefacts stored outside the install root) is **not** auto-deleted. You must pass `--purge-data` to remove it, and GSD will prompt for confirmation before doing so:
|
||||
- **Persistent capability data.** Any data the capability wrote during use (databases, caches, runtime artefacts stored outside the install root) is **not** auto-deleted by default. Pass `--purge-data` to delete it as part of the removal:
|
||||
|
||||
```bash
|
||||
gsd capability remove <id> --purge-data
|
||||
@@ -51,16 +55,14 @@ GSD uses the **ledger** — a per-runtime record written at install time (for ex
|
||||
|
||||
If you want to keep your data, omit `--purge-data`. The capability's runtime data will remain on disk even after the capability itself is removed.
|
||||
|
||||
### Prompts and confirmation
|
||||
### No prompt — `remove` is non-interactive
|
||||
|
||||
`gsd capability remove` will ask you to confirm before proceeding. Pass `--yes` to skip the prompt in scripts or non-interactive contexts:
|
||||
`gsd capability remove` is **non-interactive**: it does not prompt, and there is no `--yes` flag. It acts immediately on the scope you give it. `--purge-data` likewise deletes the capability's data directly, with no confirmation step — so be sure before you pass it. The full contract is:
|
||||
|
||||
```bash
|
||||
gsd capability remove <id> --yes
|
||||
gsd capability remove <id> [--purge-data] [--scope global|project]
|
||||
```
|
||||
|
||||
If the capability also ships persistent data and you pass `--purge-data`, GSD prompts once more specifically for the data deletion, regardless of `--yes`, because that action is irreversible.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
@@ -94,10 +96,11 @@ gsd capability remove <id> --scope project
|
||||
|
||||
| | `disable` | `remove` |
|
||||
|---|---|---|
|
||||
| Applies to | First-party only | Installed overlays (and reconcile of orphaned first-party state) |
|
||||
| Files deleted | No | Yes (ledger-recorded files only) |
|
||||
| Shared config entries removed | No | Yes (capability's entries only) |
|
||||
| Federated config keys dropped | No | Yes |
|
||||
| Persistent data deleted | No | Only with `--purge-data` + prompt |
|
||||
| Persistent data deleted | No | Only with `--purge-data` (deleted directly, no prompt) |
|
||||
| Reversible without reinstall | Yes (`enable`) | No |
|
||||
| Use when | You want it back later | You no longer need it |
|
||||
|
||||
|
||||
@@ -2,80 +2,131 @@
|
||||
|
||||
This guide shows you how to switch a GSD capability off so it stops taking part in the loop — and stays off — and how to switch off a single feature of a capability without disabling the whole thing.
|
||||
|
||||
GSD resolves one capability state from three places: whether the capability is installed, whether it is surfaced, and whether each of its hooks is gated in config. "Off" means off across all three. For why the model works this way, see [Develop a Capability for GSD 1.5+](develop-a-capability.md).
|
||||
GSD resolves one capability state from three places: whether the capability is installed, whether it is surfaced, and whether each of its hooks is gated in config. "Off" means off across all three. For why the model works this way, see [Develop a Capability for GSD 1.6.0+](develop-a-capability.md).
|
||||
|
||||
> **First-party vs. installed: pick the right off-switch.** The path depends on where the capability came from.
|
||||
>
|
||||
> - A **first-party** capability — one that ships with GSD (for example `ui`, `code-review`, `research`) — is turned off with `gsd capability disable <id>` or gated with `gsd capability set <id> --gate …`. These verbs validate `<id>` against the built-in capability registry.
|
||||
> - An **installed third-party overlay** — one you added with `gsd capability install …` — is **not** in that build-time registry, so `disable`/`enable`/`set` reject it with `unknown capability: "<id>"`. The off-switch for an installed overlay is `gsd capability remove <id> --scope <scope>`.
|
||||
>
|
||||
> The rest of this guide covers first-party capabilities. For installed overlays, jump to [Turn off an installed third-party capability](#turn-off-an-installed-third-party-capability).
|
||||
|
||||
The reliable, fully general way to change first-party capability state is the `capability` command. The `/gsd:surface` and `/gsd:settings` slash commands are convenient interactive front-ends, but they operate on **skill clusters**, not arbitrary capabilities — so reach for the CLI when you want a precise, scriptable, per-capability switch.
|
||||
|
||||
---
|
||||
|
||||
## Turn a whole capability off
|
||||
## Turn a whole first-party capability off
|
||||
|
||||
Use the runtime surface — the on/off switch. It is reversible and needs no reinstall:
|
||||
Disable the capability by id:
|
||||
|
||||
```
|
||||
/gsd:surface disable <capability>
|
||||
```bash
|
||||
gsd capability disable <id>
|
||||
```
|
||||
|
||||
For example, to stop the UI capability:
|
||||
|
||||
```
|
||||
/gsd:surface disable ui
|
||||
```bash
|
||||
gsd capability disable ui
|
||||
```
|
||||
|
||||
The capability's skills leave the surface and all of its hooks go inactive. Check the result with:
|
||||
This unsurfaces the capability's skills and makes all of its hooks inactive. It is reversible and needs no reinstall — the bundle stays on disk and your hook gates are preserved. `gsd capability disable <id>` is exactly `gsd capability set <id> --off`; re-enable with `gsd capability enable <id>` (i.e. `--on`).
|
||||
|
||||
`disable`/`enable`/`set` only accept ids the built-in registry knows about. Run them against an installed third-party overlay and you get `unknown capability: "<id>"` — see [Turn off an installed third-party capability](#turn-off-an-installed-third-party-capability) for that case.
|
||||
|
||||
Check the result:
|
||||
|
||||
```bash
|
||||
node gsd-tools.cjs capability state --raw
|
||||
gsd capability state --raw
|
||||
```
|
||||
|
||||
The capability now reports `enabled: false` and every hook `active: false`. To turn it back on, `/gsd:surface enable ui` — your earlier hook gates are preserved.
|
||||
The capability now reports `enabled: false` and every hook `active: false`.
|
||||
|
||||
---
|
||||
|
||||
## Turn off one feature of a capability
|
||||
|
||||
To keep a capability on but switch off a single hook, gate that hook instead of disabling the capability. Use `/gsd:settings`, or set the key directly:
|
||||
To keep a capability on but switch off a single hook, gate that hook instead of disabling the capability. A **gate** is a dotted config key declared in the capability's `config` slice whose boolean value controls whether one of its hooks fires. Set it to `false`:
|
||||
|
||||
```bash
|
||||
node gsd-tools.cjs capability set code-review --gate workflow.code_review=false
|
||||
gsd capability set code-review --gate workflow.code_review=false
|
||||
```
|
||||
|
||||
The capability stays enabled; only that hook stops firing.
|
||||
The capability stays enabled; only that hook stops firing. `--gate` is repeatable, so you can set several gates in one call. See the [`set` reference](../reference/gsd-capability-command.md#set) for the full contract.
|
||||
|
||||
---
|
||||
|
||||
## Capabilities that own no skills
|
||||
|
||||
Some capabilities (for example, research) contribute only hooks and agents — they have no skills to unsurface, so `/gsd:surface disable` does not affect them. Switch these off by gating their hooks:
|
||||
Some capabilities (for example, `research`) contribute only hooks and agents — they have no skills to unsurface, so disabling them via the surface has no effect. Switch these off by gating their hooks instead:
|
||||
|
||||
```bash
|
||||
node gsd-tools.cjs capability set research --gate workflow.research=false
|
||||
gsd capability set research --gate workflow.research=false
|
||||
```
|
||||
|
||||
If you gate every hook of a capability off while it is still surfaced, `gsd-tools capability state` flags it as surfaced-but-inactive — a sign you probably meant to disable the capability itself.
|
||||
If you gate every hook of a capability off while it is still surfaced, `gsd capability state` flags it as surfaced-but-inactive — a sign you probably meant to disable the capability itself.
|
||||
|
||||
---
|
||||
|
||||
## Turn off an installed third-party capability
|
||||
|
||||
A capability you added with `gsd capability install …` is an **installed overlay**, not a first-party capability. It is not present in the build-time registry that `disable`/`enable`/`set` validate against, so those verbs reject it:
|
||||
|
||||
```bash
|
||||
gsd capability disable my-overlay
|
||||
# error: unknown capability: "my-overlay"
|
||||
```
|
||||
|
||||
**Remove it.** This is the deactivation path for an installed overlay — it strips the overlay's files and edits for the chosen scope:
|
||||
|
||||
```bash
|
||||
gsd capability remove my-overlay --scope global # default scope is global
|
||||
gsd capability remove my-overlay --scope project # for a project-scoped install
|
||||
```
|
||||
|
||||
`--scope` defaults to `global`, so pass `--scope project` for a project install. Add `--purge-data` to also delete the overlay's persisted data. If the id is not installed in the chosen scope you get `capability "my-overlay" is not installed in <scope> scope`. (Trying to `remove` a first-party id instead reports that it cannot be removed here — use the product uninstaller, `gsd --uninstall`.)
|
||||
|
||||
> The `/gsd:surface` clusters described below are derived from the **built-in** capability registry, so they cover first-party skill-owning capabilities. For an installed overlay, `remove` is the off-switch.
|
||||
|
||||
See [Remove a capability](remove-a-capability.md) for the full removal flow and [`gsd capability remove`](../reference/gsd-capability-command.md#remove) for every flag and output field.
|
||||
|
||||
---
|
||||
|
||||
## The interactive paths (`/gsd:surface` and `/gsd:settings`)
|
||||
|
||||
The slash commands are the interactive equivalents, useful when you are working inside an agent session rather than scripting:
|
||||
|
||||
- **`/gsd:surface disable <cluster>`** toggles a whole skill **cluster** on or off and re-stages the surface. Its argument is validated against the fixed set of cluster names — one of `core_loop`, `audit_review`, `milestone`, `research_ideate`, `workspace_state`, `docs`, `ui`, `ai_eval`, `ns_meta`, `utility` (the command rejects anything else and lists these). A few of these names coincide with first-party skill-owning capability ids (for example `ui`), so `/gsd:surface disable ui` works — but the command does **not** accept an arbitrary capability id, including an installed overlay's id. To switch off a specific capability by id, use the CLI (`gsd capability disable <id>` for first-party, `gsd capability remove <id>` for an installed overlay). Reverse a cluster with `/gsd:surface enable <cluster>`.
|
||||
- **`/gsd:settings`** is the interactive prompt for GSD's workflow toggles (the `workflow.*` config keys that gate hooks). Use it to turn workflow features on or off conversationally; it writes the same config keys that `gsd capability set … --gate` writes.
|
||||
|
||||
For anything you want to be exact about — a specific capability id, a single named gate, or a step in a script or CI job — prefer the CLI.
|
||||
|
||||
---
|
||||
|
||||
## Scripting it
|
||||
|
||||
`/gsd:surface` and `/gsd:settings` are the interactive paths. To mutate capability state directly (in scripts or CI), call the underlying command:
|
||||
To mutate capability state directly (in scripts or CI), call the command non-interactively. The first three verbs work on **first-party** ids; the last works on **installed overlays**:
|
||||
|
||||
```bash
|
||||
# Disable via surface
|
||||
node gsd-tools.cjs capability set <id> --off
|
||||
# Disable a whole first-party capability
|
||||
gsd capability disable <id> # equivalently: gsd capability set <id> --off
|
||||
|
||||
# Re-enable
|
||||
node gsd-tools.cjs capability set <id> --on
|
||||
gsd capability enable <id> # equivalently: gsd capability set <id> --on
|
||||
|
||||
# Toggle one hook gate
|
||||
node gsd-tools.cjs capability set <id> --gate <key>=<true|false>
|
||||
gsd capability set <id> --gate <key>=<true|false>
|
||||
|
||||
# Deactivate an installed third-party overlay (disable/set would reject it)
|
||||
gsd capability remove <id> --scope <global|project>
|
||||
```
|
||||
|
||||
See [CLI tools — Capability Commands](../CLI-TOOLS.md#capability-commands) for the full reference.
|
||||
See the [`gsd capability` command reference](../reference/gsd-capability-command.md) for every subcommand, flag, and output shape.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [Develop a Capability for GSD 1.5+](develop-a-capability.md)
|
||||
- [`gsd capability` command reference](../reference/gsd-capability-command.md) — `disable`, `enable`, `set`, and the rest of the family
|
||||
- [Develop a Capability for GSD 1.6.0+](develop-a-capability.md)
|
||||
- [Install a minimal GSD and add skills later](install-minimal-and-add-skills.md)
|
||||
- [CLI tools reference — Capability Commands](../CLI-TOOLS.md#capability-commands)
|
||||
- [docs index](../README.md)
|
||||
|
||||
@@ -80,7 +80,7 @@ npm version 1.2.0
|
||||
npm publish
|
||||
```
|
||||
|
||||
**New tarball.** Upload the new archive at a URL and communicate the URL to consumers. GSD cannot auto-detect updates for tarball sources — consumers must run `gsd capability update <id> <new-url>` manually after you announce the new URL. If you anticipate frequent updates, consider switching to a git or npm source.
|
||||
**New tarball.** Upload the new archive at a URL and communicate the URL to consumers. GSD cannot auto-detect updates for tarball sources, and `gsd capability update` only ever re-resolves the URL **already recorded** in the ledger — it takes no new-URL argument. To move a tarball install to a new URL, the consumer **re-installs from the new URL** (`gsd capability install <new-url> …`), which overwrites the recorded source. If you anticipate frequent updates, consider switching to a git or npm source so `gsd capability update <id>` can pick up new versions automatically.
|
||||
|
||||
---
|
||||
|
||||
@@ -99,11 +99,11 @@ GSD contacts the source of each installed capability and reports which ones have
|
||||
| Source | Auto-detectable? |
|
||||
|---|---|
|
||||
| Git (tags / manifest) | Yes — GSD fetches available tags. |
|
||||
| Registry | Yes — GSD queries the catalogue. |
|
||||
| npm | Yes — GSD checks `dist-tags`. |
|
||||
| Tarball URL | **No** — a tarball exposes one version; updates must be applied manually when the author announces a new URL. |
|
||||
| Tarball URL | **No** — a tarball exposes one version; updates must be applied manually by re-installing from a new URL. |
|
||||
| Registry (`<name>@<registry>`) | **Not yet** — the registry source kind is reserved but unimplemented today; `outdated` reports `status: unknown` for it and `update` cannot re-resolve it. |
|
||||
|
||||
If a capability is installed from a tarball and the author publishes a new version at a different URL, you will need to run `gsd capability update <id> <new-url>` yourself once the author communicates the new address.
|
||||
If a capability is installed from a tarball and the author publishes a new version at a different URL, `gsd capability update <id>` will not help — it only re-resolves the URL already recorded at install time, and takes no new-URL argument. Once the author communicates the new address, **re-install from it** with `gsd capability install <new-url> …`; that overwrites the recorded source with the new version.
|
||||
|
||||
### Apply an update
|
||||
|
||||
@@ -123,11 +123,13 @@ Updates are **atomic**: GSD fully fetches and validates the new version before s
|
||||
|
||||
### Consent when the executable surface changes
|
||||
|
||||
If the new version adds or removes hooks, MCP server entries, or command modules compared to the version you have installed, GSD will pause and present a summary of the changes before proceeding. You must confirm explicitly; declining leaves the current version in place.
|
||||
The CLI is **non-interactive** — it never stops to ask a question. If the new version adds or removes hooks, MCP server entries, or command modules compared to the version you have installed, `gsd capability update <id>` **aborts** rather than swapping: it prints the disclosed surface change and instructs you to re-run with `--yes`, leaving the current version fully in place. Re-running with `--yes` grants consent for the new surface and completes the swap:
|
||||
|
||||
This re-prompt applies even if you previously consented to auto-update. The consent mechanism is scoped to the declared executable surface of a specific version, so a changed surface is always a fresh decision.
|
||||
```bash
|
||||
gsd capability update <id> --yes
|
||||
```
|
||||
|
||||
Auto-update is **off by default** for third-party capabilities. If you enable it, the re-prompt on executable-surface change still applies.
|
||||
This re-consent is required every time the surface changes, scoped to the declared executable surface of a specific version, so a changed surface is always a fresh `--yes`. (A version whose executable surface is unchanged updates without `--yes`.)
|
||||
|
||||
### When `engines.gsd` no longer matches
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
291
docs/tutorials/install-your-first-capability.md
Normal file
291
docs/tutorials/install-your-first-capability.md
Normal 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.
|
||||
Reference in New Issue
Block a user