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:
@@ -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).
|
||||
|
||||
59
docs/explanation/interface-versioning-policy.md
Normal file
59
docs/explanation/interface-versioning-policy.md
Normal 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)
|
||||
79
docs/how-to/author-a-host-plugin.md
Normal file
79
docs/how-to/author-a-host-plugin.md
Normal 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.
|
||||
79
docs/reference/host-integration-interface.md
Normal file
79
docs/reference/host-integration-interface.md
Normal 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)
|
||||
77
docs/tutorials/embed-gsd-in-a-new-host.md
Normal file
77
docs/tutorials/embed-gsd-in-a-new-host.md
Normal 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.
|
||||
Reference in New Issue
Block a user