@opengsd/gsd-core 1.6.1 → 1.7.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/.claude-plugin/marketplace.json +20 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +711 -0
  4. package/agents/gsd-advisor-researcher.md +2 -0
  5. package/agents/gsd-ai-researcher.md +1 -1
  6. package/agents/gsd-assumptions-analyzer.md +2 -0
  7. package/agents/gsd-code-fixer.md +2 -0
  8. package/agents/gsd-code-reviewer.md +2 -0
  9. package/agents/gsd-codebase-mapper.md +2 -0
  10. package/agents/gsd-debugger.md +2 -0
  11. package/agents/gsd-doc-writer.md +2 -0
  12. package/agents/gsd-eval-auditor.md +2 -0
  13. package/agents/gsd-executor.md +9 -6
  14. package/agents/gsd-integration-checker.md +2 -0
  15. package/agents/gsd-nyquist-auditor.md +2 -0
  16. package/agents/gsd-phase-researcher.md +2 -0
  17. package/agents/gsd-plan-checker.md +2 -0
  18. package/agents/gsd-planner.md +2 -0
  19. package/agents/gsd-project-researcher.md +2 -0
  20. package/agents/gsd-research-synthesizer.md +2 -0
  21. package/agents/gsd-roadmapper.md +2 -0
  22. package/agents/gsd-security-auditor.md +2 -0
  23. package/agents/gsd-ui-auditor.md +2 -0
  24. package/agents/gsd-ui-checker.md +2 -0
  25. package/agents/gsd-ui-researcher.md +2 -0
  26. package/agents/gsd-verifier.md +5 -2
  27. package/bin/gsd-mcp-server.js +31 -0
  28. package/bin/install.js +411 -1146
  29. package/commands/gsd/review.md +6 -0
  30. package/gemini-extension.json +1 -1
  31. package/gsd-core/bin/gsd-tools.cjs +134 -8
  32. package/gsd-core/bin/lib/adapter-declarative.cjs +35 -0
  33. package/gsd-core/bin/lib/adapter-imperative.cjs +52 -0
  34. package/gsd-core/bin/lib/assumption-delta.cjs +231 -0
  35. package/gsd-core/bin/lib/capability-lifecycle.cjs +7 -7
  36. package/gsd-core/bin/lib/capability-loader.cjs +45 -9
  37. package/gsd-core/bin/lib/capability-lock.cjs +2 -2
  38. package/gsd-core/bin/lib/capability-registry.cjs +891 -82
  39. package/gsd-core/bin/lib/capability-source.cjs +26 -11
  40. package/gsd-core/bin/lib/capability-validator.cjs +222 -2
  41. package/gsd-core/bin/lib/cli-skew-check.cjs +44 -0
  42. package/gsd-core/bin/lib/command-aliases.cjs +8 -0
  43. package/gsd-core/bin/lib/commands.cjs +2 -1
  44. package/gsd-core/bin/lib/config.cjs +27 -0
  45. package/gsd-core/bin/lib/embedding-adapter.cjs +27 -0
  46. package/gsd-core/bin/lib/external-descriptor-trust.cjs +70 -0
  47. package/gsd-core/bin/lib/frontmatter.cjs +53 -6
  48. package/gsd-core/bin/lib/handshake-serialized.cjs +70 -0
  49. package/gsd-core/bin/lib/hook-bus.cjs +81 -0
  50. package/gsd-core/bin/lib/host-integration-sdk.cjs +53 -0
  51. package/gsd-core/bin/lib/host-integration.cjs +469 -0
  52. package/gsd-core/bin/lib/init.cjs +35 -7
  53. package/gsd-core/bin/lib/install-engine.cjs +755 -0
  54. package/gsd-core/bin/lib/install-profiles.cjs +35 -4
  55. package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
  56. package/gsd-core/bin/lib/mcp-server.cjs +194 -0
  57. package/gsd-core/bin/lib/milestone.cjs +68 -40
  58. package/gsd-core/bin/lib/model-adapter.cjs +50 -0
  59. package/gsd-core/bin/lib/phase-id.cjs +18 -0
  60. package/gsd-core/bin/lib/phase.cjs +57 -90
  61. package/gsd-core/bin/lib/phases-command-router.cjs +4 -3
  62. package/gsd-core/bin/lib/planning-workspace.cjs +1 -1
  63. package/gsd-core/bin/lib/probe-core.cjs +132 -2
  64. package/gsd-core/bin/lib/review-reviewer-selection.cjs +129 -13
  65. package/gsd-core/bin/lib/roadmap-command-router.cjs +3 -2
  66. package/gsd-core/bin/lib/roadmap-parser.cjs +21 -11
  67. package/gsd-core/bin/lib/roadmap-upgrade.cjs +3 -2
  68. package/gsd-core/bin/lib/roadmap.cjs +33 -22
  69. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +65 -9
  70. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +54 -4
  71. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +5 -2
  72. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1 -1
  73. package/gsd-core/bin/lib/runtime-name-policy.cjs +160 -30
  74. package/gsd-core/bin/lib/shell-command-projection.cjs +16 -0
  75. package/gsd-core/bin/lib/stale-bake-guard.cjs +254 -0
  76. package/gsd-core/bin/lib/state-command-router.cjs +4 -0
  77. package/gsd-core/bin/lib/state-io.cjs +55 -0
  78. package/gsd-core/bin/lib/state-transition.cjs +1603 -0
  79. package/gsd-core/bin/lib/state.cjs +327 -683
  80. package/gsd-core/bin/lib/surface.cjs +4 -1
  81. package/gsd-core/bin/lib/validate.cjs +2 -1
  82. package/gsd-core/bin/lib/verify.cjs +6 -4
  83. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +12 -2
  84. package/gsd-core/bin/lib/workstream-inventory.cjs +28 -0
  85. package/gsd-core/bin/lib/workstream.cjs +4 -4
  86. package/gsd-core/bin/shared/config-schema.manifest.json +9 -0
  87. package/gsd-core/references/agent-skills-bootstrap.md +60 -0
  88. package/gsd-core/references/honest-verifier.md +105 -0
  89. package/gsd-core/references/model-profiles.md +27 -0
  90. package/gsd-core/references/reviewer-instances.md +99 -0
  91. package/gsd-core/workflows/autonomous.md +30 -32
  92. package/gsd-core/workflows/complete-milestone.md +6 -10
  93. package/gsd-core/workflows/execute-phase.md +1 -1
  94. package/gsd-core/workflows/forensics.md +3 -3
  95. package/gsd-core/workflows/help/modes/full.md +1 -1
  96. package/gsd-core/workflows/manager.md +15 -15
  97. package/gsd-core/workflows/milestone-summary.md +3 -3
  98. package/gsd-core/workflows/new-milestone.md +6 -0
  99. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +42 -0
  100. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +102 -0
  101. package/gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md +23 -0
  102. package/gsd-core/workflows/plan-phase.md +4 -159
  103. package/gsd-core/workflows/review.md +33 -2
  104. package/gsd-core/workflows/thread.md +4 -4
  105. package/gsd-core/workflows/verify-phase.md +11 -4
  106. package/gsd-core/workflows/verify-work.md +1 -2
  107. package/hooks/dist/gsd-graphify-update.sh +7 -1
  108. package/hooks/gsd-graphify-update.sh +7 -1
  109. package/package.json +6 -4
  110. package/scripts/ci-test-scope.cjs +38 -9
  111. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
  112. package/scripts/lint-regression-test-names.allowlist.json +3 -0
  113. package/scripts/lint-test-file-count.allowlist.json +19 -5
  114. package/scripts/mutation-matrix.cjs +45 -3
  115. package/scripts/prompt-injection-scan.sh +8 -0
  116. package/scripts/run-tests.cjs +51 -1
  117. package/scripts/sync-manifest-versions.cjs +66 -14
  118. package/skills/gsd-review/SKILL.md +6 -0
  119. package/scripts/lint-windows-test-portability.cjs +0 -178
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Serialized (out-of-process) capability-exchange handshake (ADR-1239 Phase E / #1683).
3
+ *
4
+ * Phase 1's `negotiateHostCapabilities` is IN-PROCESS (a host descriptor merged
5
+ * directly into the engine). Out-of-process SDK hosts (pi, VS Code) cannot share
6
+ * object references with the engine — they exchange a SERIALIZED capability set
7
+ * over a wire boundary (an MCP-style `initialize`). This module is the wire form
8
+ * of that handshake, kept CONSISTENT with the in-process negotiation: a request
9
+ * built + serialized here MUST yield the same NegotiationResult the in-process
10
+ * call produces for the same axes (asserted in tests/handshake-serialized.test.cjs).
11
+ *
12
+ * Wire shape (JSON — no object refs, safe across a process/IPC boundary):
13
+ *
14
+ * request = { protocolVersion: number, axes: Partial<HostIntegrationAxes> }
15
+ * response = NegotiationResult = { protocolVersion, effective, points, warnings }
16
+ *
17
+ * The engine side delegates to negotiateHostCapabilities; the host side builds
18
+ * the request from a descriptor. Both round-trip through JSON so the exchange is
19
+ * strictly serializable (a non-JSON-safe value would break the wire contract).
20
+ *
21
+ * Pure + additive: no I/O, no global state. The companion MCP server (Phase 4)
22
+ * or an SDK host binds this to a real transport.
23
+ */
24
+ 'use strict';
25
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
26
+ const hostIntegration = require("./host-integration.cjs");
27
+ /** The wire method name for the initialize-style exchange. */
28
+ const HANDSHAKE_METHOD = 'gsd/host-initialize';
29
+ /**
30
+ * Host side: build a JSON-serializable handshake request from a host descriptor.
31
+ * Accepts either `{ protocolVersion?, axes }` or a bare axes object.
32
+ * Defaults protocolVersion to the engine's current PROTOCOL_VERSION.
33
+ */
34
+ function buildHandshakeRequest(hostDescriptor) {
35
+ if (!hostDescriptor || typeof hostDescriptor !== 'object') {
36
+ throw new TypeError('buildHandshakeRequest: host descriptor (object) is required');
37
+ }
38
+ const desc = hostDescriptor;
39
+ const hasAxes = Object.prototype.hasOwnProperty.call(desc, 'axes');
40
+ const axes = (hasAxes ? desc.axes : desc);
41
+ if (!axes || typeof axes !== 'object') {
42
+ throw new TypeError('buildHandshakeRequest: descriptor.axes (object) is required');
43
+ }
44
+ const protocolVersion = typeof desc.protocolVersion === 'number' && Number.isFinite(desc.protocolVersion)
45
+ ? desc.protocolVersion
46
+ : hostIntegration.PROTOCOL_VERSION;
47
+ // Force a wire round-trip so a non-JSON-safe descriptor fails HERE, not later.
48
+ return JSON.parse(JSON.stringify({ protocolVersion, axes }));
49
+ }
50
+ /**
51
+ * Engine side: handle a serialized handshake request → a JSON-serializable
52
+ * NegotiationResult. Delegates to negotiateHostCapabilities, so the result is
53
+ * identical to the in-process negotiation for the same axes.
54
+ */
55
+ function handleHandshakeRequest(request, engine = hostIntegration.DEFAULT_ENGINE) {
56
+ if (!request || typeof request !== 'object') {
57
+ throw new TypeError('handleHandshakeRequest: request (object) is required');
58
+ }
59
+ const req = request;
60
+ const axes = (req.axes && typeof req.axes === 'object' ? req.axes : {});
61
+ const protocolVersion = req.protocolVersion;
62
+ const result = hostIntegration.negotiateHostCapabilities({ ...axes, ...(typeof protocolVersion === 'number' && Number.isFinite(protocolVersion) ? { protocolVersion } : {}) }, engine);
63
+ // Force a wire round-trip: the response must be strictly JSON-serializable.
64
+ return JSON.parse(JSON.stringify(result));
65
+ }
66
+ module.exports = {
67
+ HANDSHAKE_METHOD,
68
+ buildHandshakeRequest,
69
+ handleHandshakeRequest,
70
+ };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Hook-bus seam (ADR-1239 Phase C-1, AC4 / #1680).
3
+ *
4
+ * The lifecycle-hook ownership model, selected by the negotiated `hookBus`
5
+ * axis (host-integration.cts):
6
+ *
7
+ * - `engine` — GSD owns the bus internally (in-process pub/sub). Used by
8
+ * hosts that have no event bus (VS Code). Full subscribe + emit.
9
+ * - `host` — the host fires events; GSD subscribes. Handlers register
10
+ * locally for a Phase-5 host binding to dispatch to; `emit` delegates to a
11
+ * host-supplied emitter (fail-closed until bound — GSD does not drive a
12
+ * host-owned bus).
13
+ * - `none` — no bus (Cline-rules). Degrades to rule-text instructions;
14
+ * subscribe/emit are no-ops.
15
+ *
16
+ * Portable event floor — the "claude dialect" all hook-capable hosts share
17
+ * (sourced from src/runtime-hooks-surface.cts). Extended events are negotiated
18
+ * per-host (Phase 5).
19
+ *
20
+ * Minimal seam (per ADR-1239 open wire-shape question): the host-side dispatch
21
+ * wiring lands in Phase 5 (#1682). This slice ships the three ownership modes
22
+ * + the engine pub-sub + the fail-closed contract.
23
+ */
24
+ 'use strict';
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.PORTABLE_EVENT_FLOOR = void 0;
27
+ exports.createHookBus = createHookBus;
28
+ exports.PORTABLE_EVENT_FLOOR = Object.freeze(['SessionStart', 'PreToolUse', 'PostToolUse', 'Stop', 'SessionEnd']);
29
+ function createHookBus({ bus }, options = {}) {
30
+ if (bus !== 'host' && bus !== 'engine' && bus !== 'none') {
31
+ throw new TypeError(`createHookBus: bus must be 'host' | 'engine' | 'none' (got ${JSON.stringify(bus)})`);
32
+ }
33
+ if (bus === 'none') {
34
+ return Object.freeze({
35
+ bus,
36
+ subscribe() { },
37
+ emit() { },
38
+ });
39
+ }
40
+ if (bus === 'engine') {
41
+ const subs = new Map();
42
+ return Object.freeze({
43
+ bus: 'engine',
44
+ subscribe(event, handler) {
45
+ const list = subs.get(event);
46
+ if (list)
47
+ list.push(handler);
48
+ else
49
+ subs.set(event, [handler]);
50
+ },
51
+ emit(event, payload) {
52
+ const list = subs.get(event);
53
+ if (!list)
54
+ return;
55
+ for (const h of list) {
56
+ // Handler errors are isolated — one throwing handler must not break the bus.
57
+ try {
58
+ h(payload);
59
+ }
60
+ catch { /* swallow; bus stays up */ }
61
+ }
62
+ },
63
+ });
64
+ }
65
+ // host: GSD subscribes; emits go to the host-supplied emitter (fail-closed until bound).
66
+ const hostEmit = options.hostEmit;
67
+ return Object.freeze({
68
+ bus: 'host',
69
+ subscribe(_event, _handler) {
70
+ // Host owns the bus; GSD's subscriptions are dispatched by a Phase-5 host
71
+ // binding that calls the registered handlers when the host fires events.
72
+ // Stored host-side; locally this is a seam until that binding lands.
73
+ },
74
+ emit(event, payload) {
75
+ if (typeof hostEmit !== 'function') {
76
+ throw new Error("host hook-bus emit: no host emitter bound — the 'host' bus requires a hostEmit primitive (Phase 5 wires the concrete host).");
77
+ }
78
+ hostEmit(event, payload);
79
+ },
80
+ });
81
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Host-Integration SDK — the published public surface (ADR-1239 Phase E / #1683).
3
+ *
4
+ * External host-plugin authors import ONLY from this entry. It IS the contract:
5
+ * everything it re-exports is public + versioned (PROTOCOL_VERSION governs the
6
+ * set); everything else in gsd-core is internal. An SDK smoke test
7
+ * (tests/sdk-smoke.test.cjs) builds a new host-plugin against this surface only
8
+ * — proving an external author can wire a host without reading gsd-core internals.
9
+ *
10
+ * Surface: the negotiated schema + classification, the five adapters
11
+ * (declarative/imperative/model/hook/state), and the serialized handshake.
12
+ * Frozen so the public shape cannot be mutated by consumers.
13
+ */
14
+ 'use strict';
15
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
16
+ const hostIntegration = require("./host-integration.cjs");
17
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
18
+ const adapterDeclarative = require("./adapter-declarative.cjs");
19
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
20
+ const adapterImperative = require("./adapter-imperative.cjs");
21
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
22
+ const modelAdapter = require("./model-adapter.cjs");
23
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
24
+ const hookBus = require("./hook-bus.cjs");
25
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
26
+ const stateIo = require("./state-io.cjs");
27
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
28
+ const handshake = require("./handshake-serialized.cjs");
29
+ const SDK = Object.freeze({
30
+ // ── Schema + protocol version ────────────────────────────────────────────
31
+ PROTOCOL_VERSION: hostIntegration.PROTOCOL_VERSION,
32
+ HOST_INTEGRATION_AXES: hostIntegration.HOST_INTEGRATION_AXES,
33
+ INTERFACE_POINTS: hostIntegration.INTERFACE_POINTS,
34
+ PROFILE_BASELINES: hostIntegration.PROFILE_BASELINES,
35
+ // ── Negotiation + classification ─────────────────────────────────────────
36
+ negotiateHostCapabilities: hostIntegration.negotiateHostCapabilities,
37
+ profileOf: hostIntegration.profileOf,
38
+ degradationFor: hostIntegration.degradationFor,
39
+ hookEventSurfaceFor: hostIntegration.hookEventSurfaceFor,
40
+ extensionEventSurfaceFor: hostIntegration.extensionEventSurfaceFor,
41
+ shouldFlattenDispatch: hostIntegration.shouldFlattenDispatch,
42
+ // ── Embedding + engine adapters ──────────────────────────────────────────
43
+ createDeclarativeAdapter: adapterDeclarative.createDeclarativeAdapter,
44
+ createImperativeAdapter: adapterImperative.createImperativeAdapter,
45
+ createModelAdapter: modelAdapter.createModelAdapter,
46
+ createHookBus: hookBus.createHookBus,
47
+ createStateIO: stateIo.createStateIO,
48
+ // ── Serialized handshake (out-of-process SDK hosts: pi / VS Code) ─────────
49
+ HANDSHAKE_METHOD: handshake.HANDSHAKE_METHOD,
50
+ buildHandshakeRequest: handshake.buildHandshakeRequest,
51
+ handleHandshakeRequest: handshake.handleHandshakeRequest,
52
+ });
53
+ module.exports = SDK;
@@ -0,0 +1,469 @@
1
+ 'use strict';
2
+ /**
3
+ * Host Integration module — ADR-1239 Phase A.
4
+ *
5
+ * Pure, additive, no-I/O module providing a closed vocabulary for host
6
+ * integration axes, degradation ladder, profile classification, and
7
+ * capability negotiation.
8
+ *
9
+ * The SINGLE source of truth for integration axes and degradation levels.
10
+ * All functions are pure (no side effects, no I/O).
11
+ *
12
+ * Per-CLI sourced axis VALUES (with citations) live in docs/reference/host-integration-capability-matrix.md — every value is documented or explicitly 'undocumented'.
13
+ */
14
+ // ---------------------------------------------------------------------------
15
+ // Protocol version
16
+ // ---------------------------------------------------------------------------
17
+ const PROTOCOL_VERSION = 1;
18
+ // ---------------------------------------------------------------------------
19
+ // Undocumented sentinel — fail-closed when a host omits CLI docs for an axis
20
+ // ---------------------------------------------------------------------------
21
+ /**
22
+ * Sentinel value used when a host descriptor's CLI docs do not state a value
23
+ * for an axis. It VALIDATES (accepted by the validator) but NEVER propagates
24
+ * into effective axes — it fails closed exactly like an unknown/missing value.
25
+ *
26
+ * Do NOT add to HOST_INTEGRATION_AXES (which is the documented vocabulary).
27
+ */
28
+ const UNDOCUMENTED = 'undocumented';
29
+ // ---------------------------------------------------------------------------
30
+ // Closed vocabulary — axes and interface points
31
+ // ---------------------------------------------------------------------------
32
+ const HOST_INTEGRATION_AXES = Object.freeze({
33
+ embeddingMode: Object.freeze(['imperative', 'declarative']),
34
+ commandSurface: Object.freeze(['slash-file', 'slash-programmatic', 'slash-toml', 'palette', 'prose-only']),
35
+ modelMode: Object.freeze(['active', 'passive']),
36
+ hookBus: Object.freeze(['host', 'engine', 'none']),
37
+ stateIO: Object.freeze(['filesystem', 'sandboxed-storage', 'session-log-append']),
38
+ transport: Object.freeze(['mcp', 'native-extension']),
39
+ runtime: Object.freeze(['node', 'bun', 'sandboxed-web', 'python', 'go', 'rust', 'electron', 'other']),
40
+ subagentToolkit: Object.freeze(['full', 'read-only']),
41
+ });
42
+ const INTERFACE_POINTS = Object.freeze(['command', 'dispatch', 'model', 'hooks', 'state', 'artifact']);
43
+ // ---------------------------------------------------------------------------
44
+ // Profile baselines
45
+ // ---------------------------------------------------------------------------
46
+ // Fail-closed floor: the most restrictive known value per axis, injected when a host omits an axis (degrade-closed, never assume capability).
47
+ const SAFE_DEFAULTS = {
48
+ embeddingMode: 'declarative',
49
+ commandSurface: 'prose-only',
50
+ dispatch: { namedDispatch: false, nested: false, maxDepth: 0, background: false, subagentToolkit: 'read-only', backgroundDispatch: false },
51
+ modelMode: 'passive',
52
+ hookBus: 'none',
53
+ stateIO: 'session-log-append',
54
+ transport: 'mcp',
55
+ runtime: 'node',
56
+ };
57
+ const PROFILE_BASELINES = Object.freeze({
58
+ 'programmatic-cli': Object.freeze({
59
+ embeddingMode: 'imperative',
60
+ commandSurface: 'slash-file',
61
+ dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: true }),
62
+ modelMode: 'passive',
63
+ hookBus: 'host',
64
+ stateIO: 'filesystem',
65
+ transport: 'mcp',
66
+ runtime: 'node',
67
+ }),
68
+ 'declarative-cli': Object.freeze({
69
+ embeddingMode: 'declarative',
70
+ commandSurface: 'slash-file',
71
+ dispatch: Object.freeze({ namedDispatch: true, nested: false, maxDepth: 1, background: false, subagentToolkit: 'full', backgroundDispatch: false }),
72
+ modelMode: 'passive',
73
+ hookBus: 'host',
74
+ stateIO: 'filesystem',
75
+ transport: 'mcp',
76
+ runtime: 'node',
77
+ }),
78
+ 'ide': Object.freeze({
79
+ embeddingMode: 'imperative',
80
+ commandSurface: 'palette',
81
+ dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: 5, background: true, subagentToolkit: 'full', backgroundDispatch: true }),
82
+ modelMode: 'active',
83
+ hookBus: 'engine',
84
+ stateIO: 'sandboxed-storage',
85
+ transport: 'mcp',
86
+ runtime: 'sandboxed-web',
87
+ }),
88
+ });
89
+ // ---------------------------------------------------------------------------
90
+ // degradationFor — plain data-table lookup (NOT clever code)
91
+ // ---------------------------------------------------------------------------
92
+ /**
93
+ * Look up the degradation level for a given interface point and partial axes.
94
+ *
95
+ * NEVER throws. Returns { level:'absent', fallback:'...', unknown:true } for
96
+ * any missing or unrecognised axis value.
97
+ */
98
+ function degradationFor(point, axes) {
99
+ const UNKNOWN = {
100
+ level: 'absent',
101
+ fallback: 'unknown capability — degraded closed',
102
+ unknown: true,
103
+ };
104
+ switch (point) {
105
+ case 'command': {
106
+ const cs = axes.commandSurface;
107
+ if (cs === 'slash-file' || cs === 'slash-programmatic')
108
+ return { level: 'full', fallback: '' };
109
+ if (cs === 'slash-toml' || cs === 'palette')
110
+ return { level: 'degraded', fallback: 'toml/palette surface — limited command routing' };
111
+ if (cs === 'prose-only')
112
+ return { level: 'absent', fallback: 'AGENTS.md prose + skills menu' };
113
+ return UNKNOWN;
114
+ }
115
+ case 'dispatch': {
116
+ const d = axes.dispatch;
117
+ if (!d || typeof d !== 'object')
118
+ return UNKNOWN;
119
+ const disp = d;
120
+ if (disp.namedDispatch !== true || disp.maxDepth === 0) {
121
+ return { level: 'absent', fallback: 'single-agent inline / SDK sub-session' };
122
+ }
123
+ // maxDepth < 0 means unbounded
124
+ const isUnbounded = typeof disp.maxDepth === 'number' && Number.isFinite(disp.maxDepth) && disp.maxDepth < 0;
125
+ const depth = (typeof disp.maxDepth === 'number' && Number.isFinite(disp.maxDepth)) ? disp.maxDepth : 0;
126
+ const isFullDepth = isUnbounded || (disp.nested === true && depth >= 2);
127
+ if (isFullDepth) {
128
+ // Fail-closed: return 'full' ONLY when subagentToolkit is explicitly 'full';
129
+ // any other value (read-only, undocumented, unknown, missing) → degraded.
130
+ if (disp.subagentToolkit === 'full') {
131
+ return { level: 'full', fallback: '' };
132
+ }
133
+ return { level: 'degraded', fallback: 'restricted/undocumented subagent toolkit — limited dispatch surface' };
134
+ }
135
+ // flat (maxDepth===1)
136
+ return { level: 'degraded', fallback: 'flat dispatch — waves run inline' };
137
+ }
138
+ case 'model': {
139
+ const mm = axes.modelMode;
140
+ if (mm === 'active')
141
+ return { level: 'full', fallback: '' };
142
+ if (mm === 'passive')
143
+ return { level: 'degraded', fallback: 'instruction-injection / per-agent model field' };
144
+ return UNKNOWN;
145
+ }
146
+ case 'hooks': {
147
+ const hb = axes.hookBus;
148
+ if (hb === 'host')
149
+ return { level: 'full', fallback: '' };
150
+ if (hb === 'engine')
151
+ return { level: 'degraded', fallback: 'engine-owned bus' };
152
+ if (hb === 'none')
153
+ return { level: 'absent', fallback: 'rule-text instructions' };
154
+ return UNKNOWN;
155
+ }
156
+ case 'state': {
157
+ const si = axes.stateIO;
158
+ if (si === 'filesystem')
159
+ return { level: 'full', fallback: '' };
160
+ if (si === 'sandboxed-storage')
161
+ return { level: 'degraded', fallback: 'sandboxed storage' };
162
+ if (si === 'session-log-append')
163
+ return { level: 'degraded', fallback: 'append-only session log' };
164
+ return UNKNOWN;
165
+ }
166
+ case 'artifact': {
167
+ const cs = axes.commandSurface;
168
+ if (cs === 'slash-file' || cs === 'slash-programmatic')
169
+ return { level: 'full', fallback: '' };
170
+ if (cs === 'slash-toml' || cs === 'prose-only')
171
+ return { level: 'degraded', fallback: 'menu / @-only' };
172
+ if (cs === 'palette')
173
+ return { level: 'absent', fallback: 'palette + chat participant; skills become LM tools' };
174
+ return UNKNOWN;
175
+ }
176
+ default:
177
+ return UNKNOWN;
178
+ }
179
+ }
180
+ // ---------------------------------------------------------------------------
181
+ // profileOf
182
+ // ---------------------------------------------------------------------------
183
+ /**
184
+ * Classify a partial set of integration axes into a named profile.
185
+ * Returns null when no profile can be determined.
186
+ */
187
+ function profileOf(axes) {
188
+ const a = axes;
189
+ if (a.embeddingMode === 'imperative' && a.runtime === 'sandboxed-web')
190
+ return 'ide';
191
+ if (a.embeddingMode === 'imperative')
192
+ return 'programmatic-cli';
193
+ if (a.embeddingMode === 'declarative')
194
+ return 'declarative-cli';
195
+ return null;
196
+ }
197
+ const DEFAULT_ENGINE = {
198
+ protocolVersion: PROTOCOL_VERSION,
199
+ axes: {
200
+ embeddingMode: 'imperative',
201
+ commandSurface: 'slash-file',
202
+ dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: true },
203
+ modelMode: 'active',
204
+ hookBus: 'host',
205
+ stateIO: 'filesystem',
206
+ transport: 'mcp',
207
+ runtime: 'node',
208
+ },
209
+ known: HOST_INTEGRATION_AXES,
210
+ };
211
+ // ---------------------------------------------------------------------------
212
+ // negotiateHostCapabilities
213
+ // ---------------------------------------------------------------------------
214
+ /**
215
+ * Negotiate host integration capabilities against an engine.
216
+ *
217
+ * POST-CONDITION: every effective scalar axis value is in engine.known[axis].
218
+ * effective never contains a value the host didn't declare AND the engine
219
+ * cannot drive.
220
+ *
221
+ * NEVER throws. Returns a fresh object each call (mutation-safe).
222
+ */
223
+ function negotiateHostCapabilities(host, engine = DEFAULT_ENGINE) {
224
+ const warnings = [];
225
+ const h = host;
226
+ // Warn if protocolVersion is present but not a finite number
227
+ if (h.protocolVersion !== undefined && (typeof h.protocolVersion !== 'number' || !Number.isFinite(h.protocolVersion))) {
228
+ warnings.push(`host protocolVersion is not a finite number — using engine version ${engine.protocolVersion}`);
229
+ }
230
+ const hostPV = (typeof h.protocolVersion === 'number' && Number.isFinite(h.protocolVersion)) ? h.protocolVersion : engine.protocolVersion;
231
+ const enginePV = engine.protocolVersion;
232
+ // Warn if host declares a newer protocol version
233
+ if (hostPV > enginePV) {
234
+ warnings.push(`host protocolVersion ${hostPV} newer than engine ${enginePV} — capabilities beyond version ${enginePV} not trusted`);
235
+ }
236
+ // ---------------------------------------------------------------------------
237
+ // Helper: negotiate a single scalar axis
238
+ // ---------------------------------------------------------------------------
239
+ function negotiateScalar(axis) {
240
+ const knownValues = engine.known[axis];
241
+ const hostVal = h[axis];
242
+ const engineVal = engine.axes[axis];
243
+ const safeDefault = SAFE_DEFAULTS[axis];
244
+ if (hostVal === undefined || hostVal === null) {
245
+ // Host did not declare this axis
246
+ warnings.push(`host did not declare '${axis}'`);
247
+ return safeDefault;
248
+ }
249
+ if (hostVal === UNDOCUMENTED) {
250
+ // Host declared the undocumented sentinel — treat as fail-closed (degrade to safe default)
251
+ warnings.push(`host axis '${axis}' is undocumented — degraded closed`);
252
+ return safeDefault;
253
+ }
254
+ if (!knownValues.includes(hostVal)) {
255
+ // Host declared an unknown/future value — NEVER copy into effective
256
+ warnings.push(`host declared unknown '${axis}' value '${String(hostVal)}' — not trusted (host protocolVersion ${hostPV} vs engine ${enginePV})`);
257
+ return safeDefault;
258
+ }
259
+ // Engine capability cap: if the engine can't drive the host's value,
260
+ // use the engine's lesser capability.
261
+ // For modelMode: 'active' > 'passive' — if host wants active but engine
262
+ // is passive, cap to passive.
263
+ if (axis === 'modelMode') {
264
+ if (hostVal === 'active' && engineVal === 'passive')
265
+ return 'passive';
266
+ }
267
+ return hostVal;
268
+ }
269
+ // Negotiate all scalar axes
270
+ const effectiveEmbeddingMode = negotiateScalar('embeddingMode');
271
+ const effectiveCommandSurface = negotiateScalar('commandSurface');
272
+ const effectiveModelMode = negotiateScalar('modelMode');
273
+ const effectiveHookBus = negotiateScalar('hookBus');
274
+ const effectiveStateIO = negotiateScalar('stateIO');
275
+ const effectiveTransport = negotiateScalar('transport');
276
+ const effectiveRuntime = negotiateScalar('runtime');
277
+ // ---------------------------------------------------------------------------
278
+ // Dispatch struct negotiation
279
+ // ---------------------------------------------------------------------------
280
+ const hostDispatch = (typeof h.dispatch === 'object' && h.dispatch !== null)
281
+ ? h.dispatch
282
+ : null;
283
+ const engineDispatch = engine.axes.dispatch;
284
+ let effectiveNamedDispatch;
285
+ let effectiveNested;
286
+ let effectiveBackground;
287
+ let effectiveBackgroundDispatch;
288
+ let effectiveSubagentToolkit;
289
+ let effectiveMaxDepth;
290
+ if (hostDispatch === null) {
291
+ // Host didn't declare dispatch at all — fail-closed to most-restrictive values
292
+ warnings.push(`host did not declare 'dispatch'`);
293
+ effectiveNamedDispatch = false;
294
+ effectiveNested = false;
295
+ effectiveBackground = false;
296
+ effectiveBackgroundDispatch = false;
297
+ effectiveSubagentToolkit = 'read-only';
298
+ effectiveMaxDepth = 0;
299
+ }
300
+ else {
301
+ // N1: observability warnings for 'undocumented' sentinel on dispatch fields
302
+ if (hostDispatch.namedDispatch === 'undocumented') {
303
+ warnings.push(`dispatch.namedDispatch is undocumented — degraded closed`);
304
+ }
305
+ if (hostDispatch.nested === 'undocumented') {
306
+ warnings.push(`dispatch.nested is undocumented — degraded closed`);
307
+ }
308
+ if (hostDispatch.background === 'undocumented') {
309
+ warnings.push(`dispatch.background is undocumented — degraded closed`);
310
+ }
311
+ if (hostDispatch.subagentToolkit === 'undocumented') {
312
+ warnings.push(`dispatch.subagentToolkit is undocumented — degraded closed (read-only)`);
313
+ }
314
+ if (hostDispatch.backgroundDispatch === 'undocumented') {
315
+ warnings.push(`dispatch.backgroundDispatch is undocumented — degraded closed`);
316
+ }
317
+ effectiveNamedDispatch = (hostDispatch.namedDispatch === true) && engineDispatch.namedDispatch;
318
+ effectiveNested = (hostDispatch.nested === true) && engineDispatch.nested;
319
+ effectiveBackground = (hostDispatch.background === true) && engineDispatch.background;
320
+ effectiveBackgroundDispatch = (hostDispatch.backgroundDispatch === true) && engineDispatch.backgroundDispatch;
321
+ // subagentToolkit: fail closed to read-only unless explicitly 'full'
322
+ // (an 'undocumented' or 'read-only' value → read-only)
323
+ const hostToolkit = hostDispatch.subagentToolkit === 'full' ? 'full' : 'read-only';
324
+ const engineToolkit = engineDispatch.subagentToolkit === 'read-only' ? 'read-only' : 'full';
325
+ effectiveSubagentToolkit = (hostToolkit === 'read-only' || engineToolkit === 'read-only') ? 'read-only' : 'full';
326
+ // maxDepth: missing/non-number/non-finite → 0 + warning
327
+ let hostMaxDepth;
328
+ if (typeof hostDispatch.maxDepth !== 'number' || !Number.isFinite(hostDispatch.maxDepth)) {
329
+ warnings.push(`host dispatch.maxDepth is missing or not a number — treating as 0`);
330
+ hostMaxDepth = 0;
331
+ }
332
+ else {
333
+ hostMaxDepth = hostDispatch.maxDepth;
334
+ }
335
+ // Treat negative as +Infinity for the min, then if result is +Infinity emit -1
336
+ const hDepthNum = hostMaxDepth < 0 ? Infinity : hostMaxDepth;
337
+ const eDepthNum = engineDispatch.maxDepth < 0 ? Infinity : engineDispatch.maxDepth;
338
+ const minDepth = Math.min(hDepthNum, eDepthNum);
339
+ effectiveMaxDepth = minDepth === Infinity ? -1 : minDepth;
340
+ // If namedDispatch is false, cap maxDepth/nested/background/backgroundDispatch to 0/false/false/false (struct consistency)
341
+ if (!effectiveNamedDispatch) {
342
+ effectiveMaxDepth = 0;
343
+ effectiveNested = false;
344
+ effectiveBackground = false;
345
+ effectiveBackgroundDispatch = false;
346
+ }
347
+ }
348
+ const effectiveDispatch = {
349
+ namedDispatch: effectiveNamedDispatch,
350
+ nested: effectiveNested,
351
+ maxDepth: effectiveMaxDepth,
352
+ background: effectiveBackground,
353
+ subagentToolkit: effectiveSubagentToolkit,
354
+ backgroundDispatch: effectiveBackgroundDispatch,
355
+ };
356
+ // ---------------------------------------------------------------------------
357
+ // Assemble effective axes
358
+ // ---------------------------------------------------------------------------
359
+ const effective = {
360
+ embeddingMode: effectiveEmbeddingMode,
361
+ commandSurface: effectiveCommandSurface,
362
+ dispatch: effectiveDispatch,
363
+ modelMode: effectiveModelMode,
364
+ hookBus: effectiveHookBus,
365
+ stateIO: effectiveStateIO,
366
+ transport: effectiveTransport,
367
+ runtime: effectiveRuntime,
368
+ };
369
+ // ---------------------------------------------------------------------------
370
+ // Compute points (fresh objects — mutation-safe)
371
+ // ---------------------------------------------------------------------------
372
+ const points = {};
373
+ for (const point of INTERFACE_POINTS) {
374
+ const hostDeg = degradationFor(point, host);
375
+ const effectiveDeg = degradationFor(point, effective);
376
+ points[point] = {
377
+ hostLevel: hostDeg.level,
378
+ effectiveLevel: effectiveDeg.level,
379
+ fallback: effectiveDeg.fallback,
380
+ };
381
+ }
382
+ // protocolVersion: min of host and engine
383
+ const resultProtocolVersion = Math.min(hostPV, enginePV);
384
+ return {
385
+ protocolVersion: resultProtocolVersion,
386
+ effective,
387
+ points,
388
+ warnings: [...warnings], // fresh copy
389
+ };
390
+ }
391
+ function shouldFlattenDispatch(dispatch) {
392
+ if (!dispatch || typeof dispatch !== 'object')
393
+ return true;
394
+ const canBackground = dispatch.background === true && dispatch.backgroundDispatch === true;
395
+ return !canBackground;
396
+ }
397
+ // ---------------------------------------------------------------------------
398
+ // Managed-hook event surface per hookEvents dialect (ADR-1239 / ADR-1016)
399
+ // ---------------------------------------------------------------------------
400
+ // Host-fireable MANAGED-hook events per `hookEvents` dialect. `hookEvents` is the
401
+ // managed-hook dialect — the event names GSD writes into a DECLARATIVE host's
402
+ // settings.json (claude = SessionStart/PreToolUse/…; gemini = BeforeTool/AfterTool).
403
+ // This is DISTINCT from the extension-system event surface (below): a host's
404
+ // plugin/extension API fires a different, plugin-owned event set. The two must
405
+ // not be conflated (ADR-1239 amendment / #1943 — the former 'opencode-subset'
406
+ // `hookEvents` value was this conflation; it is now `extensionEvents: opencode`).
407
+ const HOOK_EVENT_SURFACES = Object.freeze({
408
+ claude: Object.freeze(['SessionStart', 'PreToolUse', 'PostToolUse', 'Stop', 'SessionEnd', 'PreCompact']),
409
+ gemini: Object.freeze(['SessionStart', 'BeforeTool', 'AfterTool', 'SessionEnd']),
410
+ });
411
+ /**
412
+ * Resolve the managed-hook event surface for a `hookEvents` dialect.
413
+ * Returns null for unknown/missing dialects (fail-closed). Pure, never throws.
414
+ */
415
+ function hookEventSurfaceFor(hookEvents) {
416
+ if (typeof hookEvents !== 'string')
417
+ return null;
418
+ return HOOK_EVENT_SURFACES[hookEvents] || null;
419
+ }
420
+ // ---------------------------------------------------------------------------
421
+ // Extension-system event surface (ADR-1239 amendment / #1943)
422
+ // ---------------------------------------------------------------------------
423
+ // The events a host's PLUGIN/EXTENSION API exposes — for imperative-embedding
424
+ // hosts that load GSD as a plugin. This is a SEPARATE vocabulary + descriptor
425
+ // field (`extensionEvents`) from `hookEvents`: hookEvents = the managed-hook
426
+ // dialect (declarative hosts' settings.json); extensionEvents = the plugin-owned
427
+ // event subset (imperative hosts' extension API). They are not the same thing.
428
+ //
429
+ // Values are documentation-sourced (ADR-1239 §research): OpenCode ~25 plugin
430
+ // events (session/tool/file/permission); pi ~30 fine-grained extension events;
431
+ // 'none' = the host exposes no extension surface and the engine owns the bus
432
+ // (VS Code). Declarative hosts (no plugin API) do not set `extensionEvents`.
433
+ const EXTENSION_EVENT_SURFACES = Object.freeze({
434
+ opencode: Object.freeze([
435
+ 'session.created', 'session.idle', 'experimental.session.compacting',
436
+ 'tool.execute.before', 'tool.execute.after', 'file.edited',
437
+ ]),
438
+ pi: Object.freeze(['tool_call']),
439
+ none: Object.freeze([]),
440
+ });
441
+ /**
442
+ * Resolve the extension-system event surface for an `extensionEvents` dialect.
443
+ * Returns null for unknown/missing dialects (fail-closed). Pure, never throws.
444
+ *
445
+ * A non-null result is what makes an `extensionEvents` value a CONSUMED value
446
+ * rather than reserved vocab. For 'opencode' it carries NO workflow-phase events
447
+ * — the engine owns phase sequencing internally on such hosts (ADR-1239 §OpenCode).
448
+ */
449
+ function extensionEventSurfaceFor(extensionEvents) {
450
+ if (typeof extensionEvents !== 'string')
451
+ return null;
452
+ return EXTENSION_EVENT_SURFACES[extensionEvents] || null;
453
+ }
454
+ module.exports = {
455
+ PROTOCOL_VERSION,
456
+ UNDOCUMENTED,
457
+ HOST_INTEGRATION_AXES,
458
+ INTERFACE_POINTS,
459
+ PROFILE_BASELINES,
460
+ DEFAULT_ENGINE,
461
+ HOOK_EVENT_SURFACES,
462
+ EXTENSION_EVENT_SURFACES,
463
+ degradationFor,
464
+ profileOf,
465
+ negotiateHostCapabilities,
466
+ shouldFlattenDispatch,
467
+ hookEventSurfaceFor,
468
+ extensionEventSurfaceFor,
469
+ };