feat(#1943): extensionEvents vocabulary — separate from hookEvents (extension-system surface) (#1946)

* feat(#1943): extensionEvents vocabulary — separate from hookEvents (extension-system surface)

* fix(#1943): re-export VALID_EXTENSION_EVENTS from gen-capability-registry (test import path)

* fix(#1943): regenerate capability-registry.cjs + add changeset fragment

* fix(#1943): import VALID_EXTENSION_EVENTS from validator, not gen-capability-registry (golden parity)
This commit is contained in:
Tom Boucher
2026-07-02 21:45:35 -04:00
committed by GitHub
parent d3d689a5e9
commit b51cbf96cf
11 changed files with 157 additions and 47 deletions

View File

@@ -40,6 +40,7 @@ const SDK = Object.freeze({
profileOf: hostIntegration.profileOf,
degradationFor: hostIntegration.degradationFor,
hookEventSurfaceFor: hostIntegration.hookEventSurfaceFor,
extensionEventSurfaceFor: hostIntegration.extensionEventSurfaceFor,
shouldFlattenDispatch: hostIntegration.shouldFlattenDispatch,
// ── Embedding + engine adapters ──────────────────────────────────────────

View File

@@ -513,39 +513,66 @@ function shouldFlattenDispatch(dispatch: UnvalidatedDispatch): boolean {
}
// ---------------------------------------------------------------------------
// Hook-event surface per hookEvents dialect (ADR-1239 Phase D / #1682)
// Managed-hook event surface per hookEvents dialect (ADR-1239 / ADR-1016)
// ---------------------------------------------------------------------------
// The set of host-fireable hook events for each hookEvents dialect. This is the
// CONSUMER of the reserved 'opencode-subset' dialect (previously zero consumers):
// it lets the engine ask which events a host's bus actually exposes, so it knows
// workflow-phase hooks (plan:pre / verify:post / …) are NOT available on an
// opencode-subset host and the engine must own phase sequencing internally
// (ADR-1239 §OpenCode binding). 'claude' = full Claude surface; 'gemini' =
// Gemini's BeforeTool/AfterTool family; 'opencode-subset' = OpenCode's
// session/tool/file subset (no workflow-phase events).
// Host-fireable MANAGED-hook events per `hookEvents` dialect. `hookEvents` is the
// managed-hook dialect — the event names GSD writes into a DECLARATIVE host's
// settings.json (claude = SessionStart/PreToolUse/…; gemini = BeforeTool/AfterTool).
// This is DISTINCT from the extension-system event surface (below): a host's
// plugin/extension API fires a different, plugin-owned event set. The two must
// not be conflated (ADR-1239 amendment / #1943 — the former 'opencode-subset'
// `hookEvents` value was this conflation; it is now `extensionEvents: opencode`).
const HOOK_EVENT_SURFACES: Readonly<Record<string, readonly string[]>> = Object.freeze({
claude: Object.freeze(['SessionStart', 'PreToolUse', 'PostToolUse', 'Stop', 'SessionEnd', 'PreCompact']),
gemini: Object.freeze(['SessionStart', 'BeforeTool', 'AfterTool', 'SessionEnd']),
'opencode-subset': Object.freeze([
'session.created', 'session.idle', 'experimental.session.compacting',
'tool.execute.before', 'tool.execute.after', 'file.edited',
]),
});
/**
* Resolve the host-fireable hook-event surface for a hookEvents dialect.
* Resolve the managed-hook event surface for a `hookEvents` dialect.
* Returns null for unknown/missing dialects (fail-closed). Pure, never throws.
*
* A non-null result for 'opencode-subset' is what makes that dialect a CONSUMED
* value rather than reserved vocab: callers can ask `hookEventSurfaceFor('opencode-subset')`
* and learn the host fires no workflow-phase events.
*/
function hookEventSurfaceFor(hookEvents: unknown): readonly string[] | null {
if (typeof hookEvents !== 'string') return null;
return HOOK_EVENT_SURFACES[hookEvents] || null;
}
// ---------------------------------------------------------------------------
// Extension-system event surface (ADR-1239 amendment / #1943)
// ---------------------------------------------------------------------------
// The events a host's PLUGIN/EXTENSION API exposes — for imperative-embedding
// hosts that load GSD as a plugin. This is a SEPARATE vocabulary + descriptor
// field (`extensionEvents`) from `hookEvents`: hookEvents = the managed-hook
// dialect (declarative hosts' settings.json); extensionEvents = the plugin-owned
// event subset (imperative hosts' extension API). They are not the same thing.
//
// Values are documentation-sourced (ADR-1239 §research): OpenCode ~25 plugin
// events (session/tool/file/permission); pi ~30 fine-grained extension events;
// 'none' = the host exposes no extension surface and the engine owns the bus
// (VS Code). Declarative hosts (no plugin API) do not set `extensionEvents`.
const EXTENSION_EVENT_SURFACES: Readonly<Record<string, readonly string[]>> = Object.freeze({
opencode: Object.freeze([
'session.created', 'session.idle', 'experimental.session.compacting',
'tool.execute.before', 'tool.execute.after', 'file.edited',
]),
pi: Object.freeze(['tool_call']),
none: Object.freeze([]),
});
/**
* Resolve the extension-system event surface for an `extensionEvents` dialect.
* Returns null for unknown/missing dialects (fail-closed). Pure, never throws.
*
* A non-null result is what makes an `extensionEvents` value a CONSUMED value
* rather than reserved vocab. For 'opencode' it carries NO workflow-phase events
* — the engine owns phase sequencing internally on such hosts (ADR-1239 §OpenCode).
*/
function extensionEventSurfaceFor(extensionEvents: unknown): readonly string[] | null {
if (typeof extensionEvents !== 'string') return null;
return EXTENSION_EVENT_SURFACES[extensionEvents] || null;
}
// ---------------------------------------------------------------------------
// Module export (CommonJS — matches existing src/*.cts pattern)
// ---------------------------------------------------------------------------
@@ -558,9 +585,11 @@ export = {
PROFILE_BASELINES,
DEFAULT_ENGINE,
HOOK_EVENT_SURFACES,
EXTENSION_EVENT_SURFACES,
degradationFor,
profileOf,
negotiateHostCapabilities,
shouldFlattenDispatch,
hookEventSurfaceFor,
extensionEventSurfaceFor,
};