* feat(#3778): dispatch plan:pre planner contributions in quick.md - Add plan:pre capability gate to quick.md Step 5, mirroring plan-phase.md's existing render + generic contribution dispatch pattern - Inject planner-targeted contribution fragments into the planner prompt, after AGENT_SKILLS_PLANNER, matching D-08 ordering - Add tests/quick-plan-pre-capabilities.test.cjs proving the dispatch is generic (D-01) via real scanWiredKinds/coveredKindsInRegion functions - Record quick.md's byte-growth rationale in this commit trailer for every gsd-core-verbatim runtime Emitted-Drift-Ack-Growth: quick.md — #3778: Step 5 (Spawn planner, quick mode) gains a `plan:pre` capability gate, mirroring `plan-phase.md:420-424` and `:797`. This is shipped shell and prose read by an agent at runtime, not compiled, so the reasoning has to travel with the feature rather than being deferred to a reference doc: (1) the dispatch paragraph phrases role routing possessively ("the role each entry's `into` names") rather than as an `into ==` equality, because `coveredKindsInRegion` (scripts/gen-loop-host-contract.cjs) voids a segment's `kind == "contribution"` coverage credit when a role or capability equality shares that same segment — an equality phrasing here would silently fail the generic-dispatch proof required by D-01; (2) `activeHooks` is read directly in-context from `PLAN_PRE_HOOKS_JSON`/`HOOKS_JSON` and the unfiltered `rendered` digest is explicitly forbidden from being pasted, because `rendered` carries every kind and role — including non-planner-targeted contributions such as a `into: "checker"` twin — and pasting it would leak checker-scoped guidance into the planner's prompt (T-01-02 in the threat model); (3) the injection block sits inside `<planning_context>` AFTER `${AGENT_SKILLS_PLANNER}` and after the Project skills line, matching plan-phase's `:741` -> `:797` ordering (D-08), so agent-skills content is never shadowed by capability-contributed prose. No prose was moved into an eagerly `@`-imported reference to shrink the measured file — @gsd-core/references/loop-hook-dispatch.md already existed before this change and is deferred to for the generic contract only, exactly as plan-phase.md already does. * test(#3778): expand quick.md plan:pre dispatch coverage to all nine locked conditions Extend tests/quick-plan-pre-capabilities.test.cjs with D-02 (silent omit-when-empty), D-03 (single shared planner spawn), D-06 (array-order dispatch phrasing), D-07 (planner-only into filter), and D-08 (render call < agent-skills placeholder < injection block < spawn ordering) assertions, all extracted via a brace-bounded slice anchored on the literal injection instruction rather than a naive first-brace scan (${AGENT_SKILLS_PLANNER} and the surrounding prompt's ${VALIDATE_MODE ? ...} ternaries also contain brace pairs). Add a capability-registry.test.cjs describe block proving the registry-wide D-07 exclusion is meaningful: at least one plan:pre contribution exists, every plan:pre contribution has a non-empty into/fragment.inline, and the registry as a whole carries at least one non-planner-into contribution. Add a loop-host-contract.test.cjs regression pin for D-09: quick.md stays absent from STEP_WORKFLOWS, parseLoopHostBlock still throws on quick.md's real content, and buildContract() still yields exactly 5 entries. Verified red-without-Task-1 by temporarily reverting quick.md to its pre-f30de9cc content and re-running these three suites (D-08 failed as expected), then restored via git checkout and re-confirmed green. * docs(#3778): note quick planning also renders plan:pre in the tutorial The tutorial's Step 6 named only /gsd-plan-phase as the trigger for the plan:pre hook set. Since quick.md now dispatches the same hook set (f30de9cc), the sentence understated the capability's real reach. * feat(#3778): add changeset fragment * chore(#3778): reference the upstream issue in the changeset fragment The fragment was the only one of 81 in .changeset/ without a trailing (#NNNN) reference or a bold lead-in. serializeChangelog auto-appends only the pr: field, so the rendered CHANGELOG entry carried no link back to issue #3778. * test(#3778): scope the D-07 registry assertion to what it actually proves The registry-wide non-planner check was named "D-07 exclusion is meaningful", which overclaims: it proves only that `into` takes non-planner values somewhere in the registry, not that anything is excluded at plan:pre. Every plan:pre contribution is currently into: "planner", so the filter is a forward-looking safeguard there. Narrowing the assertion to plan:pre (as review suggested) would fail today. Asserting plan:pre is all-planner would be brittle — it would break the day a legitimate non-planner plan:pre contribution lands, which is exactly when the safeguard starts doing work. So the assertion is unchanged and only the name and comment are corrected. * chore(#3778): point the changeset fragment at the upstream PR The fragment carried pr: 3, the fork staging PR. changeset lint derives the real PR number from GITHUB_EVENT_PATH, so on the upstream PR that would read as pr-field drift. Point it at open-gsd/gsd-core#3934. * test(#3778): require contributions in Quick revision prompts * test(loop-host): require Quick auxiliary registration * fix(#3778): preserve contributions in Quick plan revisions * fix(#3778): validate Quick as a planner contribution host * fix(#3778): tighten Quick contribution contract * test(#3778): drop unnecessary source-contract exemption * fix(#3778): require Quick planner target coverage * docs(#3778): describe targeted auxiliary coverage --------- Co-authored-by: davdittrich <davdittrich@gmail.com> Co-authored-by: CI Rebase Check <ci@gsd-redux> Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
252 lines
10 KiB
Markdown
252 lines
10 KiB
Markdown
# Build Your First Capability
|
|
|
|
In this tutorial you will build a tiny, fully declarative GSD capability from scratch and watch it act inside your project's loop. By the end you will have a working capability installed, visible in `gsd capability list`, and firing at the `plan:pre` extension point.
|
|
|
|
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 `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`.
|
|
|
|
---
|
|
|
|
## Before you begin
|
|
|
|
You need:
|
|
|
|
- GSD 1.6.0 or later (`gsd --version`).
|
|
- A throwaway project directory. Create one now:
|
|
|
|
```bash
|
|
mkdir ~/hello-demo && cd ~/hello-demo
|
|
gsd init
|
|
```
|
|
|
|
You will work inside `~/hello-demo` for the rest of this tutorial.
|
|
|
|
---
|
|
|
|
## Step 1 — Scaffold the capability folder
|
|
|
|
Capabilities live in a `capabilities/<id>/` folder. Create the folder structure:
|
|
|
|
```bash
|
|
mkdir -p capabilities/hello-note/fragments
|
|
```
|
|
|
|
Your project tree now looks like this:
|
|
|
|
```text
|
|
~/hello-demo/
|
|
.gsd/
|
|
capabilities/
|
|
hello-note/
|
|
fragments/ ← prompt fragments live here
|
|
```
|
|
|
|
---
|
|
|
|
## Step 2 — Write the prompt fragment
|
|
|
|
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'
|
|
## Hello from hello-note
|
|
|
|
This planning session was started with the hello-note capability active.
|
|
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 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`.
|
|
|
|
---
|
|
|
|
## Step 3 — Write `capability.json`
|
|
|
|
Create the manifest at `capabilities/hello-note/capability.json`:
|
|
|
|
```json
|
|
{
|
|
"id": "hello-note",
|
|
"role": "feature",
|
|
"version": "0.1.0",
|
|
"title": "Hello Note",
|
|
"description": "Injects a greeting note at plan:pre and produces HELLO.md.",
|
|
"tier": "standard",
|
|
"requires": [],
|
|
"engines": { "gsd": ">=1.6.0" },
|
|
"runtimeCompat": { "supported": ["*"], "unsupported": [] },
|
|
"skills": [],
|
|
"agents": [],
|
|
"config": {},
|
|
"steps": [],
|
|
"contributions": [
|
|
{
|
|
"point": "plan:pre",
|
|
"into": "planner",
|
|
"fragment": { "path": "fragments/plan-pre.md" },
|
|
"produces": ["HELLO.md"],
|
|
"consumes": [],
|
|
"onError": "skip"
|
|
}
|
|
],
|
|
"gates": []
|
|
}
|
|
```
|
|
|
|
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. 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.
|
|
|
|
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.
|
|
|
|
---
|
|
|
|
## Step 4 — Install the capability into your project
|
|
|
|
Install from the local path with `--scope project` so it is scoped only to this demo project:
|
|
|
|
```bash
|
|
gsd capability install ./capabilities/hello-note --scope project
|
|
```
|
|
|
|
The command emits a JSON result:
|
|
|
|
```json
|
|
{
|
|
"status": "installed",
|
|
"id": "hello-note",
|
|
"version": "0.1.0",
|
|
"scope": "project",
|
|
"disclosure": [
|
|
"This capability ships no executable surfaces (declarative only)."
|
|
]
|
|
}
|
|
```
|
|
|
|
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.
|
|
|
|
---
|
|
|
|
## Step 5 — Confirm the installation
|
|
|
|
```bash
|
|
gsd capability list
|
|
```
|
|
|
|
`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
|
|
{
|
|
"id": "hello-note",
|
|
"role": "feature",
|
|
"version": "0.1.0",
|
|
"tier": "standard",
|
|
"source": "./capabilities/hello-note",
|
|
"scope": "project",
|
|
"status": "active",
|
|
"reason": null,
|
|
"title": "Hello Note"
|
|
}
|
|
```
|
|
|
|
`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.)
|
|
|
|
You can also query the active hook set for the `plan:pre` point:
|
|
|
|
```bash
|
|
gsd loop render-hooks plan:pre --raw
|
|
```
|
|
|
|
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 is an unfiltered diagnostic digest across kinds and roles; planner hosts do not paste it. They read `activeHooks` in registry order and inject only active `kind: "contribution"` entries whose `into` is `planner`, using each entry's `fragment.inline` and resolved `configValues`.
|
|
|
|
---
|
|
|
|
## Step 6 — See the contribution reach the planner
|
|
|
|
Planning is driven by a slash command, not a `gsd` subcommand. In your AI assistant, start a planning session for a phase with:
|
|
|
|
```text
|
|
/gsd-plan-phase
|
|
```
|
|
|
|
When the planner runs — whether from `/gsd-plan-phase` or `/gsd-quick` — the active planner-targeted `plan:pre` contributions are injected into its prompt, so it receives `hello-note` and, following the fragment's instruction, records a one-line note in `HELLO.md`. In `/gsd-quick --full` and `/gsd-quick --validate`, the same rendered hook snapshot is reused if the plan checker sends the plan back to the planner for revision.
|
|
|
|
The first-party security capability is enabled by default and also contributes at `plan:pre` into the planner role. Its guidance requires generated plans to include `<threat_model>` blocks and carries the configured ASVS level and blocking threshold. Disable `workflow.security_enforcement` if that behavior is not wanted. Contributions targeting any non-planner role are not inserted into Quick's planner prompts.
|
|
|
|
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 loop render-hooks plan:pre --raw
|
|
```
|
|
|
|
Find `hello-note` in `activeHooks` and inspect its `fragment.inline`, `into`, and optional `configValues`. Those are the fields the planner host filters and injects. The unfiltered top-level `rendered` digest is useful for inspection, but it is not the planner prompt. This confirms the capability is wired into the loop without dispatching an 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` 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.
|
|
|
|
---
|
|
|
|
## Where next
|
|
|
|
- [Publish a capability](../how-to/publish-a-capability.md) — package and share your capability via a URL or registry.
|
|
- [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.
|
|
- [Capability manifest reference](../reference/capability-manifest.md) — all fields, types, and validation rules for `capability.json`.
|
|
- [Capability trust model](../explanation/capability-trust-model.md) — why declarative capabilities need no consent prompt and how executable surfaces are disclosed.
|