docs(#1683): Diátaxis host-integration docs + versioning policy + ADR-1239 Accepted — Slice 3 (#1940)

- how-to: author a host-plugin (external-author guide against the SDK surface)
- tutorial: embed GSD in a new host (end-to-end programmatic-cli example)
- reference: the Host-Integration Interface (axes, adapters, handshake, profiles)
- explanation: interface versioning + deprecation policy (additive vs breaking,
  PROTOCOL_VERSION bumps, deprecation window)
- ADR-1239 Status: Proposed → Accepted (Phases B–E shipped)
This commit is contained in:
Tom Boucher
2026-07-02 18:38:11 -04:00
committed by GitHub
parent 1388e7a362
commit 69fef7c00e
5 changed files with 296 additions and 2 deletions

View File

@@ -1,6 +1,6 @@
# ADR-1239: GSD as an Embeddable Orchestration Engine
- **Status:** Proposed
- **Status:** Accepted
- **Date:** 2026-06-14
- **Issue:** [#1239](https://github.com/open-gsd/gsd-core/issues/1239)
- **Epic:** [#857](https://github.com/open-gsd/gsd-core/issues/857) (Capability system)
@@ -87,7 +87,7 @@ Each phase is its own `approved-*` issue + PR with equivalence/parity proof.
### Amendment — Phase A implemented (#1684, v1.7.0)
Phase A is **implemented** (the ADR itself remains `Proposed` overall until Phases B–E land). The negotiated capability schema is materialized as a pure, additive, no-I/O module — the **Host-Integration Interface** (`src/host-integration.cts` → `gsd-core/bin/lib/host-integration.cjs`):
Phase A is **implemented** and Phases B–E have landed, so this ADR is **Accepted** (Status flipped from `Proposed` once Phase E shipped the published SDK + serialized handshake + versioning policy + Diátaxis docs). The negotiated capability schema is materialized as a pure, additive, no-I/O module — the **Host-Integration Interface** (`src/host-integration.cts` → `gsd-core/bin/lib/host-integration.cjs`):
- **The eight negotiated axes** are carried under `capability.json` `runtime.hostIntegration` (extending, not replacing, the ADR-1016 axes), validated by `validateRuntimeBody` (`capability-validator.cjs`) across all 16 runtime descriptors, with the closed vocabulary kept in lock-step by a parity guard.
- **`PROTOCOL_VERSION`** is an integer starting at `1`, **distinct** from the package `version` / `engines.gsd` semver (the `version`/`protocolVersion` overlap, resolved).

View File

@@ -0,0 +1,59 @@
# Interface versioning and deprecation policy
This explains how the GSD Host-Integration Interface (ADR-1239) evolves over
time — what is an additive change, what is breaking, how `PROTOCOL_VERSION`
bumps, and what deprecation window host-plugin authors can rely on. The policy
exists because a published SDK is a long-term compatibility commitment (Hyrum's
Law: once external hosts depend on it, the observable behavior is the contract).
---
## The version knob: `PROTOCOL_VERSION`
`PROTOCOL_VERSION` is a positive integer carried by the engine and exchanged in
every negotiation (in-process and serialized). It governs the negotiated
capability **set** — which axes, values, and adapter shapes exist. A host
declares the version it targets; the engine negotiates down to
`min(host, engine)` and warns when a host declares a newer version than the
engine (capabilities beyond the engine's version are not trusted).
## Additive vs. breaking
- **Additive (no version bump required):** adding a new *optional* axis value, a
new adapter option, or a new reference profile. Existing host-plugins keep
working — the engine negotiates to their declared version and ignores
unknowns. Additive changes are the default; the interface prefers growing the
vocabulary over changing it.
- **Breaking (requires a `PROTOCOL_VERSION` bump):** removing or renaming an
axis, changing an adapter's call/return shape, changing the negotiation result
shape, or narrowing a value set. A breaking change is never silent — it lands
behind a version bump so a host that declares the old version negotiates down
and gets a warning, not a crash.
## Deprecation window
A value or adapter shape is **deprecated**, not removed, for at least **one
minor release cycle**. During the window:
1. The old form still works at its declared version.
2. The reference docs mark it deprecated with the successor + the removal version.
3. The negotiation emits a `warnings` entry when a host uses a deprecated value.
Removal is a breaking change and follows the version-bump rule above. The
`undocumented` sentinel is never deprecated — it is the permanent fail-closed
fallback for a host that omits an axis.
## What this means for host-plugin authors
- Pin to a `PROTOCOL_VERSION` and assert on it in your host-plugin's test.
- Treat unknown axis values / adapter options as additive (ignore, don't crash).
- Watch negotiation `warnings` — they surface both "host declared a newer
version" and "host used a deprecated value."
- The serialized handshake's consistency with the in-process negotiation is
itself part of the contract: a change that breaks that consistency is breaking.
## See also
- [Reference: the Host-Integration Interface](../reference/host-integration-interface.md)
- ADR-1239 (Status: Accepted)

View File

@@ -0,0 +1,79 @@
# How to author a host-plugin for GSD
This guide is for **external tool authors** who want to embed GSD's orchestration
loop (dispatch + hooks + model + state) into a new host — a CLI, IDE, or agent
runtime GSD does not special-case — by writing a **host-plugin** against the
published Host-Integration SDK, **without modifying gsd-core source**.
The contract is the SDK entry (`src/host-integration-sdk.cts` →
`gsd-core/bin/lib/host-integration-sdk.cjs`): import only from it. Everything it
re-exports is public and versioned (`PROTOCOL_VERSION`); everything else in
gsd-core is internal and may change. A smoke test
(`tests/sdk-smoke.test.cjs`) proves a third-party host can be built from this
surface alone.
---
## 1. Decide your host's profile
Classify your host into one of three profiles (the SDK's `profileOf` does this
from your axes):
| Profile | Example hosts | Binding |
|---|---|---|
| `programmatic-cli` | OpenCode, pi | imperative adapter (`createImperativeAdapter`) |
| `declarative-cli` | Antigravity, Codex | declarative adapter (`createDeclarativeAdapter`) |
| `ide` | VS Code | imperative adapter + engine-owned hook bus + active model + sandboxed state |
## 2. Declare your host's integration axes
Declare the eight negotiated axes (`embeddingMode`, `commandSurface`, `dispatch`,
`modelMode`, `hookBus`, `stateIO`, `transport`, `runtime`) from your host's
**authoritative documentation** — never infer. Where the docs are silent, use the
`undocumented` sentinel (the SDK degrades it fail-closed). See
`docs/reference/host-integration-interface.md` for the closed vocabulary.
## 3. Compose the engine adapters
```js
const SDK = require('@opengsd/gsd-core/sdk'); // the published entry
// Engine-owned hook bus (for hosts with no event bus, e.g. VS Code):
const hookBus = SDK.createHookBus({ bus: 'engine' });
// Active model via a host provider (e.g. vscode.lm); or 'passive' for CLIs:
const model = SDK.createModelAdapter({ modelMode: 'active' }, { sendRequest: hostSendRequest });
// State IO — filesystem for CLIs, sandboxed-storage for IDEs/web:
const stateIO = SDK.createStateIO({ io: 'filesystem' });
// The engine-as-library adapter bound to your runtime id:
const adapter = SDK.createImperativeAdapter({ runtime: 'my-host' });
```
## 4. Negotiate capabilities (serialized, for out-of-process hosts)
If your host runs out-of-process (can't share object refs with the engine), use
the serialized handshake instead of the in-process `negotiateHostCapabilities`:
```js
const req = SDK.buildHandshakeRequest({ ...myAxes });
const { effective, points, warnings } = SDK.handleHandshakeRequest(req);
```
The result is identical to the in-process negotiation for the same axes — that
consistency is the contract.
## 5. Bind your host primitives + ship
Map the six interface points (command, dispatch, model, hooks, state, artifact)
to your host's primitives (palette/chat, plugin API, provider calls, etc.). The
reference host-plugins (OpenCode, Antigravity, pi, VS Code — see Phase 5) are
canonical examples to mirror. Your host-plugin is now a self-contained artifact;
no gsd-core source change is required.
## See also
- **Tutorial:** `docs/tutorials/embed-gsd-in-a-new-host.md` — a complete end-to-end embedding.
- **Reference:** `docs/reference/host-integration-interface.md` — every axis, adapter, and the handshake.
- **Versioning:** `docs/explanation/interface-versioning-policy.md` — what evolves additively vs. breaking.

View File

@@ -0,0 +1,79 @@
# Reference: the Host-Integration Interface
This is the normative reference for the GSD Host-Integration Interface (ADR-1239)
— the versioned, negotiated contract over which any host embeds GSD's
orchestration loop. The published surface is the SDK entry
(`src/host-integration-sdk.cts`); this document specifies every symbol on it.
The governing principle: **every axis value a host declares must come from that
host's authoritative documentation.** Where docs are silent, the host declares
the `undocumented` sentinel and the engine degrades fail-closed — it never
assumes a capability.
---
## Protocol version
`PROTOCOL_VERSION` (a positive integer) is the interface version. It governs the
negotiated capability set; see the [versioning policy](../explanation/interface-versioning-policy.md)
for what a bump means.
## The eight negotiated axes
`HOST_INTEGRATION_AXES` is the closed vocabulary. Each axis takes a documented
value (or the `undocumented` sentinel):
| Axis | Values |
|---|---|
| `embeddingMode` | `imperative` \| `declarative` |
| `commandSurface` | `slash-file` \| `slash-programmatic` \| `slash-toml` \| `palette` \| `prose-only` |
| `dispatch` | struct: `{ namedDispatch, nested, maxDepth, background, subagentToolkit, backgroundDispatch }` |
| `modelMode` | `active` \| `passive` |
| `hookBus` | `host` \| `engine` \| `none` |
| `stateIO` | `filesystem` \| `sandboxed-storage` \| `session-log-append` |
| `transport` | `mcp` \| `native-extension` |
| `runtime` | `node` \| `bun` \| `sandboxed-web` \| `python` \| `go` \| `rust` \| `electron` \| `other` |
## Classification + negotiation
- `profileOf(axes)` → `'programmatic-cli'` \| `'declarative-cli'` \| `'ide'` \| `null`.
- `negotiateHostCapabilities(host, engine)` → `{ protocolVersion, effective, points, warnings }` — the in-process negotiation. Pure; never throws.
- `handleHandshakeRequest(request)` / `buildHandshakeRequest(descriptor)` — the **serialized** (out-of-process) form of the same negotiation, JSON-safe across a wire boundary. The two are consistent: a serialized request yields the same `effective` axes as the in-process call.
- `degradationFor(point, axes)` → `{ level, fallback, unknown? }` for one of the six interface points (`command` \| `dispatch` \| `model` \| `hooks` \| `state` \| `artifact`).
- `hookEventSurfaceFor(hookEvents)` → the host-fireable hook events for a dialect (`'claude'` \| `'gemini'` \| `'opencode-subset'`), or `null` if unknown.
- `shouldFlattenDispatch(dispatch)` → `true` when the orchestrator must run inline (fail-closed).
## The engine adapters
All five satisfy a common `{ kind, runtime, install, uninstall }` shape and are
constructed fail-closed (they throw if a required host primitive is absent):
| Adapter | Factory | Selected by |
|---|---|---|
| Embedding (declarative) | `createDeclarativeAdapter({runtime})` | `embeddingMode: 'declarative'` |
| Embedding (imperative) | `createImperativeAdapter({runtime})` | `embeddingMode: 'imperative'` (also exposes the composed `registry`) |
| Model | `createModelAdapter({modelMode}, {sendRequest?})` | `modelMode` (`'active'` needs a host `sendRequest`) |
| Hook bus | `createHookBus({bus}, {hostEmit?})` | `hookBus` (`'host'` needs a host `hostEmit`) |
| State IO | `createStateIO({io}, {backend?})` | `stateIO` (`'sandboxed-storage'`/`'session-log-append'` need a host `backend`) |
The declarative + imperative adapters delegate install/uninstall in-process to
the **same** `installRuntimeArtifacts` engine function `bin/install.js` uses, so
adapter output is byte-identical to a first-party install (gated by
`tests/golden-install-parity.test.cjs`).
## Profiles
`PROFILE_BASELINES` fixes the three reference profiles:
| Profile | Baseline |
|---|---|
| `programmatic-cli` | imperative, slash-file, host bus, filesystem, mcp, node |
| `declarative-cli` | declarative, slash-file, host bus, filesystem, mcp, node |
| `ide` | imperative, palette, **active** model, **engine** bus, **sandboxed-storage**, sandboxed-web |
## See also
- [How-to: author a host-plugin](../how-to/author-a-host-plugin.md)
- [Tutorial: embed GSD in a new host](../tutorials/embed-gsd-in-a-new-host.md)
- [Interface versioning policy](../explanation/interface-versioning-policy.md)
- ADR-1239 (the design) · `docs/reference/host-integration-capability-matrix.md` (per-host values + citations)

View File

@@ -0,0 +1,77 @@
# Tutorial: embed GSD in a new host
This tutorial walks through embedding GSD's orchestration loop into a brand-new
host end-to-end — from classifying the host, to composing the engine adapters,
to running a GSD command through the embedded engine. It is the learning path
that pairs with the [how-to](../how-to/author-a-host-plugin.md) (steps) and the
[reference](../reference/host-integration-interface.md) (spec).
You will build a minimal **programmatic-cli** host-plugin that invokes a GSD
command via the embedded engine. No gsd-core source changes.
---
## Prerequisites
- The GSD Host-Integration SDK entry importable in your host's runtime.
- Your host's authoritative docs open (you'll declare axes from them, never infer).
## Step 1 — Classify the host
Your host is a programmatic CLI with an event bus and a passive model (it can
only receive prompts, not call a model for you). That is the `programmatic-cli`
profile.
```js
const SDK = require('@opengsd/gsd-core/sdk');
// Confirm the profile from your declared axes:
const profile = SDK.profileOf({ embeddingMode: 'imperative', runtime: 'node' });
// → 'programmatic-cli'
```
## Step 2 — Compose the engine adapters
```js
const hookBus = SDK.createHookBus({ bus: 'host' }); // your host fires events
const model = SDK.createModelAdapter({ modelMode: 'passive' });
const stateIO = SDK.createStateIO({ io: 'filesystem' });
const adapter = SDK.createImperativeAdapter({ runtime: 'my-cli' });
```
## Step 3 — Negotiate capabilities
```js
const req = SDK.buildHandshakeRequest({
embeddingMode: 'imperative', commandSurface: 'slash-file', modelMode: 'passive',
hookBus: 'host', stateIO: 'filesystem', transport: 'mcp', runtime: 'node',
});
const { effective, points, warnings } = SDK.handleHandshakeRequest(req);
// `effective` is the negotiated capability set; `points` is per-interface-point
// degradation; `warnings` flags any axis the host omitted.
```
## Step 4 — Run a GSD command through the embedded engine
The imperative adapter exposes the engine surface; your host binds its command
surface (slash commands, palette, chat) to it. A user invoking `/gsd:phase` in
your host dispatches through the embedded engine exactly as it would in a
first-party host — that is the parity the interface guarantees.
## Step 5 — Verify parity
Assert that your host-plugin, built from the SDK entry alone, produces the same
negotiated result as the in-process path. The SDK smoke test
(`tests/sdk-smoke.test.cjs`) is the template — copy its structure for your
host's own parity test.
## Recap
You classified a host, declared its axes from authoritative docs, composed the
adapters, negotiated capabilities over the (serialized) handshake, and ran a GSD
command through the embedded engine — all without touching gsd-core source. A
new host is now a plugin you wrote.
## Next
- [How-to: author a host-plugin](../how-to/author-a-host-plugin.md) — the step-by-step reference.
- [Interface versioning policy](../explanation/interface-versioning-policy.md) — how the surface evolves.