* docs(#1239): ADR for GSD as an embeddable orchestration engine Records the design to invert GSD from a standalone installer that projects onto a host into an embeddable orchestration engine a host loads as a plugin, driven through a negotiated host-integration interface. Unifies ADR-1016 projection (the declarative adapter) with imperative embedding behind one contract; grounded in a 9-host capability survey. Closes #1239 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(#1239): de-slash illustrative command placeholders (docs-parity gate) Replace illustrative /gsd:x /gsd-x /gsd.x placeholders with namespace-prefix wording so the docs-parity live-registry check does not parse them as non-live commands. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
14 KiB
ADR-1239: GSD as an Embeddable Orchestration Engine
- Status: Proposed
- Date: 2026-06-14
- Issue: #1239
- Epic: #857 (Capability system)
- Realizes / inverts: ADR-857 Decision 8 — flips projection to embedding, and unifies them
- Subsumes as adapters: ADR-1016 (Runtime Capability Descriptor → the declarative adapter), ADR-58 (
InstallPlan), ADR-3660, ADR-894 - Distinct from: #956 (third-party feature plugins / Connected Capabilities)
Context
GSD is a standalone installer that projects onto a host — npx @opengsd/gsd-core --codex writes artifacts into ~/.codex via a per-runtime descriptor (ADR-1016). That answers only "how do we write our files onto a CLI we already know." It does not let GSD be embedded as an orchestration engine a host loads as a plugin: a new host (a "pi console") has no path, and the dependency points the wrong way (GSD reaches into the host instead of the host embedding GSD).
We want the inversion: GSD is the engine; the host loads it through a stable, negotiated interface; a third party writes the thin host-plugin. This is "turn the CLIs into Capabilities like we did for the loop."
The six interface points (the integration surface, already implicit in the code)
- Command / workflow invocation —
gsd-tools.cjsCommand Routing Hub (ADR-0012) + the workflow/slash surface. - Agent dispatch — GSD spawns sub-agents through the host's Agent/Task primitive.
- Model invocation — GSD tiers → host model ids.
- Lifecycle hooks —
hookEvents/hooksSurface/extendedHookEvents. - State + config IO —
.planning/+ config under a declaredconfigHome. - Artifact surface — how the host renders GSD's commands/agents/skills.
Research: how 8 supported/target hosts actually expose these (source of truth)
Surveyed Claude Code, Codex, OpenCode, pi (pi.dev), VS Code, Gemini CLI, Cursor, Cline, Hermes (official docs + local capabilities/*/capability.json). Two structural facts dominate:
(a) Hosts split into two embedding modes.
- Imperative (a programmatic plugin API): Claude Code (subagents + 30 hook events + MCP +
Agent()tool), pi (TS extensions:registerCommand/registerTool/registerProvider+ ~30 fine-grained hooks +before_provider_requestpayload mutation), OpenCode (JS plugins + ~25 events), VS Code (extension host:vscode.lm, chat participants, LM tools), Cline (SDKAgentPluginwithbeforeTool). - Declarative (files only, "no in-process extension API"): Gemini CLI (TOML commands +
.mdagents + 10 hook scripts), Cursor (.mdc/.md+ 19 hook events viahooks.json), Codex (AGENTS.md prose +/skillsmenu, no custom slash commands), Cline-via-rules (.clinerulestext — the surface GSD uses today, 0 programmatic events).
→ ADR-1016's projection model is the declarative-embedding adapter. Imperative embedding is the new adapter. Both sit behind one negotiated interface.
(b) Every interface point varies from rich → degraded → absent, per host. Agent nesting alone: Claude foreground-unlimited/background-depth-5; Codex depth-1; Gemini strictly-flat; Cline depth-2 (leaf = read-only, no MCP); Hermes spawn-depth-2 orchestrator/leaf + kanban-async; OpenCode subtask synchronous-only; pi no named-dispatch primitive; VS Code DIY tool-loop. This is why the contract must be negotiated, not assumed.
Precedent: MCP's negotiated lifecycle
MCP's initialize handshake has each side declare capabilities + a protocol version, and "a requestor SHOULD only augment a request with a capability the receiver declared." That is exactly the shape: the host-plugin declares which primitives it provides; GSD declares requirements; GSD degrades gracefully when a primitive is absent (generalizing #853 into a first-class contract).
Decision
Define a Host-Integration Interface: a versioned, negotiated contract over the six interface points, with GSD as an embeddable orchestration engine consumed through it. A host integration is a host-plugin = a negotiated capability set + an embedding-mode adapter (declarative = ADR-1016 projection; imperative = code that drives host primitives) + a thin binding. First-party hosts are authored through the same interface a third party would use (dogfooding). Third-party loading is purely additive (opt-in loader + trust gate over the descriptor: schema validation + configHome write-confinement). This unifies projection and embedding rather than replacing one with the other.
The negotiated capability schema (extends the ADR-1016 axes)
At load, host-plugin and engine exchange protocolVersion + a capability object. New axes the research requires:
embeddingMode:imperative|declarative— does the host run GSD as code or interpret GSD's artifacts?commandSurface:slash-file(Claude/OpenCode,gsd:-namespaced) |slash-programmatic(pi/VS-Code-chat) |slash-toml(Gemini,gsd.-namespaced) |palette(VS Code) |prose-only(Codex). Drives how interface point 1 binds;prose-onlyis a real degradation.dispatch:{ namedDispatch: bool, nested: bool, maxDepth: int, background: bool, subagentToolkit: 'full'|'read-only' }. GSD's orchestration flattens whenmaxDepth/nestedare insufficient (run plan/execute inline) — the #853 rule, generalized and tested.modelMode:active(host exposessendRequest/provider registration → GSD calls the model: VS Code, pi) |passive(GSD can only inject prompts/instructions: Gemini/Cursor/Cline/Codex/OpenCode). Two model-layer adapters;passivemeans GSD expresses orchestration declaratively.hookBus:host(host fires events GSD subscribes to: Gemini/Cursor/Hermes/Codex/pi/OpenCode) |engine(host has no bus → GSD owns it internally and fires its own: VS Code) |none(no bus → degrade lifecycle gating to rule-text instructions: Cline-rules). Plus the portable event floor (SessionStart/PreToolUse/PostToolUse/Stop/SessionEnd— the "claude dialect" all hook-capable hosts share) and negotiated extended events.stateIO:filesystem(most) |sandboxed-storage(VS Code web: no arbitrary FS) |session-log-append(pi JSONL).configHomewrite-confinement applies to the filesystem case.transport:mcp(near-universal — Claude/Codex/OpenCode/VS-Code/Gemini/Cursor/Cline/Hermes all consume MCP) |native-extension(pi: MCP needs a community extension) — GSD may ship a companion MCP server binding interface points 1+5 (the MemPalace pattern, already shipping).runtime:node|bun(pi) |sandboxed-web(VS Code web: nochild_process); + flags likesystemMessages: bool(VS Code rejects system-role messages).
The primitive vocabulary stays closed and first-party (ADR-857 Decision 8): a host needing a novel primitive needs a first-party primitive; the negotiation surfaces "unsupported" rather than letting a descriptor inject code. (Third-party code contributions are #956.)
Per-interface-point capability + degradation ladder (grounded)
| Interface point | Full | Degraded | Absent → fallback |
|---|---|---|---|
| 1 Command | slash-file/slash-programmatic (Claude, OpenCode, pi, Gemini, Cursor) |
slash-toml namespacing (Gemini gsd.-prefixed) |
prose-only (Codex): commands become AGENTS.md prose + skills menu |
| 2 Dispatch | nested + background + full toolkit (Claude fg) | shallow/flat/read-only (Codex d1, Gemini flat, Cline d2 read-only) | no named dispatch (pi): single-agent inline; build via SDK sub-session |
| 3 Model | active (VS Code lm, pi providers, before_provider_request) |
per-agent model field only (OpenCode, Gemini sub-agent) | passive: instruction-injection only; no tier routing |
| 4 Hooks | host bus, rich events (Claude 30, pi 30, Cursor 19) | host bus, thin events (Hermes 6, Codex 10 command-only) | engine-owns-bus (VS Code) / none → rule-text (Cline) |
| 5 State | filesystem .planning/ (all CLIs) |
sandboxed storage (VS Code) | session-log append (pi); Memento index |
| 6 Artifact | / typeahead + @agent + mgmt UI (Claude) |
menu/@-only (Codex /skills, Gemini passive skills) |
palette + chat participant only (VS Code: skills become LM tools) |
Consequences
Positive: GSD embeds into any host with a plugin mechanism (Codex/OpenCode/pi today; a new "pi console" tomorrow) with no GSD source change; projection and imperative embedding unify under one contract; the agent-nesting bug class becomes a declared, tested capability; the engine gains a clean boundary; per-host degradation is explicit and testable rather than scattered runtime === '…' checks.
Negative / cost: a multi-phase refactor drawing a boundary through bin/install.js (the residue: inline agent loop, missing destSubpath write-confinement, getDirName/_applyRuntimeRewrites/post-layout hooks); two model adapters + two embedding adapters to build and test; per-interface-point degradation must be specified and parity-tested per host; trust-gate (write-confinement) is security-load-bearing; IDE hosts (VS Code) break terminal/shell/file-slash assumptions and need a distinct profile.
Phased migration (the epic, #1239)
- Phase A — Define the interface (this ADR): six points, the negotiated capability schema above, protocol version, and the degradation ladder.
- Phase B — Engine ↔ host boundary: separate orchestration core (loop,
gsd-tools, state) from install/projection; fold per-runtime residue into descriptors (absorbs #1173/ADR-1235 + theinstall.jsresidue list); adddestSubpathwrite-confinement. - Phase C — Two embedding adapters + trust gate: formalize the declarative adapter (today's projection) and the imperative adapter; opt-in external-descriptor loader + schema validation +
configHomeconfinement; the MCP-companion-server binding. - Phase D — Dogfood one reference host per profile: a programmatic-CLI (Claude or pi), a declarative-CLI (Gemini or Codex), and an IDE (VS Code) — re-authored through the public interface, with golden parity for the CLIs.
- Phase E — Third-party SDK + docs: publish the interface + reference host-plugins; a new host is a plugin someone writes.
Each phase is its own approved-* issue + PR with equivalence/parity proof.
Host-capability profiles (negotiation baselines)
- Programmatic-CLI (Claude Code, pi, OpenCode): imperative; full dispatch; host hook bus; MCP;
slashsurface. The richest target — minimal degradation. - Declarative-CLI (Gemini, Cursor, Codex, Cline-rules, Hermes): declarative (projection); host hook bus or none; passive model; shallow/flat dispatch; MCP (except via rules). The ADR-1016 path.
- IDE (VS Code): imperative but not a terminal — palette/chat surface, engine-owned hook bus,
activemodel (no system messages), sandboxed state, possible no-child_process. A distinct profile that most stresses the interface.
Alternatives considered
- Projection-only (ADR-1016 as-is) — rejected: never embeds; reverses the dependency.
- Per-host bespoke integrations — rejected: the add-a-host tax ADR-857 exists to end.
- Expose only
gsd-tools.cjsas "the API" — rejected: the engine is the loop (dispatch + hooks + model + state), not just deterministic CLI ops; the six points are irreducible. - One embedding mode — rejected: the research shows hosts are split imperative/declarative; forcing one strands half of them. Two adapters behind one interface is the minimum.
- Fold into #956 (Connected Capabilities) — rejected: that's the heavier code-loading door; the host door is data + thin adapter (the "wrong altitude" finding).
Open questions (narrowed by the research)
- Exact wire-shape of the
initializehandshake — in-process descriptor merge (declarative) vs a serialized capability exchange (imperative/SDK hosts like pi/VS Code). - Where precisely to cut the engine↔host boundary (which modules are "engine" vs "host adapter").
- Whether the companion MCP server becomes the primary imperative transport (it covers points 1+5 on nearly every host) — and the pi fallback.
- The degradation ladder's fatal-vs-degradable line per point (e.g. is
prose-onlycommand surface acceptable, or does Codex stay projection-only?). - Interface versioning/deprecation policy across capability-set evolution.
Appendix — per-host capability matrix (research evidence)
| Host | Mode | Cmd surface | Dispatch | Model | Hook bus (events) | MCP | Runtime |
|---|---|---|---|---|---|---|---|
| Claude Code | imperative | slash-file (gsd:-ns) |
nested fg ∞ / bg depth-5; Agent() |
passive (per-subagent model) |
host (30) | yes (bundle) | node |
| pi (pi.dev) | imperative | slash-programmatic | no named dispatch; SDK sub-session | active (providers + before_provider_request) |
host (~30 fine) | community ext | bun |
| OpenCode | imperative | slash-file | mode:subagent/@; subtask sync |
per-agent model | host (~25) | yes | node |
| VS Code | imperative/IDE | palette + chat / |
DIY lm tool-loop |
active (vscode.lm, no system msg) |
engine-owned (none) | yes (provider) | node / sandboxed-web |
| Codex | declarative | prose-only + /skills |
max_depth=1 |
passive (session-only) | host (10, command-only) | yes | node |
| Gemini CLI | declarative | slash-toml (gsd.-ns) |
flat (no nesting) | passive (sub-agent model:) |
host (10: BeforeAgent/Model/Tool) | yes | node |
| Cursor | declarative | slash-file (gsd--ns) |
host sub-agents; 19 hook events | passive | host (19) | yes | node |
| Cline | declarative (rules) | slash-file | use_subagents depth-2 read-only |
passive | none (rules) / SDK beforeTool |
yes | node |
| Hermes | declarative | slash-file | delegate_task depth-2 + kanban |
passive (pre/post_llm_call unbound) |
host (6) writes shared config | yes | node |