@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.
- package/.claude-plugin/marketplace.json +20 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +711 -0
- package/agents/gsd-advisor-researcher.md +2 -0
- package/agents/gsd-ai-researcher.md +1 -1
- package/agents/gsd-assumptions-analyzer.md +2 -0
- package/agents/gsd-code-fixer.md +2 -0
- package/agents/gsd-code-reviewer.md +2 -0
- package/agents/gsd-codebase-mapper.md +2 -0
- package/agents/gsd-debugger.md +2 -0
- package/agents/gsd-doc-writer.md +2 -0
- package/agents/gsd-eval-auditor.md +2 -0
- package/agents/gsd-executor.md +9 -6
- package/agents/gsd-integration-checker.md +2 -0
- package/agents/gsd-nyquist-auditor.md +2 -0
- package/agents/gsd-phase-researcher.md +2 -0
- package/agents/gsd-plan-checker.md +2 -0
- package/agents/gsd-planner.md +2 -0
- package/agents/gsd-project-researcher.md +2 -0
- package/agents/gsd-research-synthesizer.md +2 -0
- package/agents/gsd-roadmapper.md +2 -0
- package/agents/gsd-security-auditor.md +2 -0
- package/agents/gsd-ui-auditor.md +2 -0
- package/agents/gsd-ui-checker.md +2 -0
- package/agents/gsd-ui-researcher.md +2 -0
- package/agents/gsd-verifier.md +5 -2
- package/bin/gsd-mcp-server.js +31 -0
- package/bin/install.js +411 -1146
- package/commands/gsd/review.md +6 -0
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +134 -8
- package/gsd-core/bin/lib/adapter-declarative.cjs +35 -0
- package/gsd-core/bin/lib/adapter-imperative.cjs +52 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +231 -0
- package/gsd-core/bin/lib/capability-lifecycle.cjs +7 -7
- package/gsd-core/bin/lib/capability-loader.cjs +45 -9
- package/gsd-core/bin/lib/capability-lock.cjs +2 -2
- package/gsd-core/bin/lib/capability-registry.cjs +891 -82
- package/gsd-core/bin/lib/capability-source.cjs +26 -11
- package/gsd-core/bin/lib/capability-validator.cjs +222 -2
- package/gsd-core/bin/lib/cli-skew-check.cjs +44 -0
- package/gsd-core/bin/lib/command-aliases.cjs +8 -0
- package/gsd-core/bin/lib/commands.cjs +2 -1
- package/gsd-core/bin/lib/config.cjs +27 -0
- package/gsd-core/bin/lib/embedding-adapter.cjs +27 -0
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +70 -0
- package/gsd-core/bin/lib/frontmatter.cjs +53 -6
- package/gsd-core/bin/lib/handshake-serialized.cjs +70 -0
- package/gsd-core/bin/lib/hook-bus.cjs +81 -0
- package/gsd-core/bin/lib/host-integration-sdk.cjs +53 -0
- package/gsd-core/bin/lib/host-integration.cjs +469 -0
- package/gsd-core/bin/lib/init.cjs +35 -7
- package/gsd-core/bin/lib/install-engine.cjs +755 -0
- package/gsd-core/bin/lib/install-profiles.cjs +35 -4
- package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
- package/gsd-core/bin/lib/mcp-server.cjs +194 -0
- package/gsd-core/bin/lib/milestone.cjs +68 -40
- package/gsd-core/bin/lib/model-adapter.cjs +50 -0
- package/gsd-core/bin/lib/phase-id.cjs +18 -0
- package/gsd-core/bin/lib/phase.cjs +57 -90
- package/gsd-core/bin/lib/phases-command-router.cjs +4 -3
- package/gsd-core/bin/lib/planning-workspace.cjs +1 -1
- package/gsd-core/bin/lib/probe-core.cjs +132 -2
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +129 -13
- package/gsd-core/bin/lib/roadmap-command-router.cjs +3 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +21 -11
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +3 -2
- package/gsd-core/bin/lib/roadmap.cjs +33 -22
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +65 -9
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +54 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +5 -2
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1 -1
- package/gsd-core/bin/lib/runtime-name-policy.cjs +160 -30
- package/gsd-core/bin/lib/shell-command-projection.cjs +16 -0
- package/gsd-core/bin/lib/stale-bake-guard.cjs +254 -0
- package/gsd-core/bin/lib/state-command-router.cjs +4 -0
- package/gsd-core/bin/lib/state-io.cjs +55 -0
- package/gsd-core/bin/lib/state-transition.cjs +1603 -0
- package/gsd-core/bin/lib/state.cjs +327 -683
- package/gsd-core/bin/lib/surface.cjs +4 -1
- package/gsd-core/bin/lib/validate.cjs +2 -1
- package/gsd-core/bin/lib/verify.cjs +6 -4
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +12 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +28 -0
- package/gsd-core/bin/lib/workstream.cjs +4 -4
- package/gsd-core/bin/shared/config-schema.manifest.json +9 -0
- package/gsd-core/references/agent-skills-bootstrap.md +60 -0
- package/gsd-core/references/honest-verifier.md +105 -0
- package/gsd-core/references/model-profiles.md +27 -0
- package/gsd-core/references/reviewer-instances.md +99 -0
- package/gsd-core/workflows/autonomous.md +30 -32
- package/gsd-core/workflows/complete-milestone.md +6 -10
- package/gsd-core/workflows/execute-phase.md +1 -1
- package/gsd-core/workflows/forensics.md +3 -3
- package/gsd-core/workflows/help/modes/full.md +1 -1
- package/gsd-core/workflows/manager.md +15 -15
- package/gsd-core/workflows/milestone-summary.md +3 -3
- package/gsd-core/workflows/new-milestone.md +6 -0
- package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +42 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +102 -0
- package/gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md +23 -0
- package/gsd-core/workflows/plan-phase.md +4 -159
- package/gsd-core/workflows/review.md +33 -2
- package/gsd-core/workflows/thread.md +4 -4
- package/gsd-core/workflows/verify-phase.md +11 -4
- package/gsd-core/workflows/verify-work.md +1 -2
- package/hooks/dist/gsd-graphify-update.sh +7 -1
- package/hooks/gsd-graphify-update.sh +7 -1
- package/package.json +6 -4
- package/scripts/ci-test-scope.cjs +38 -9
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
- package/scripts/lint-regression-test-names.allowlist.json +3 -0
- package/scripts/lint-test-file-count.allowlist.json +19 -5
- package/scripts/mutation-matrix.cjs +45 -3
- package/scripts/prompt-injection-scan.sh +8 -0
- package/scripts/run-tests.cjs +51 -1
- package/scripts/sync-manifest-versions.cjs +66 -14
- package/skills/gsd-review/SKILL.md +6 -0
- 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
|
+
};
|