feat(#1682): OpenCode session.idle + opencode-subset dialect + Claude parity — Slice 1b/c (#1930)

* feat(#1682): OpenCode session.idle + opencode-subset dialect + Claude parity — Slice 1b/c

- Plugin (.opencode/plugins/gsd-core.js): handle session.idle (↔ Claude Stop
  lifecycle point; no-op sentinel — state already persisted to .planning/).
  Completes the compaction/idle pair (#1914 shipped compaction).
- Declare hookEvents: 'opencode-subset' in the OpenCode descriptor — the
  reserved dialect now has a real consumer (no longer zero-consumer).
- host-integration.cts: add HOOK_EVENT_SURFACES + hookEventSurfaceFor() — the
  pure consumer that resolves a dialect to its host-fireable event surface.
  opencode-subset = session/tool/file subset with NO workflow-phase events
  (engine owns phase sequencing; ADR-1239 §OpenCode binding).
- Tests: hookEventSurfaceFor unit tests (claude/gemini/opencode-subset/null);
  plugin session.idle no-throw; compaction breadcrumb; opencode-subset surface
  parity vs the plugin's handlers (Claude parity).

* fix(#1682): don't declare hookEvents on opencode (hooksSurface:none invariant)

The repo invariant couples runtime.hookEvents to the managed settings.json hook
surface: hooksSurface:'none' runtimes (opencode — plugin owns hooks) must NOT
declare hookEvents. Declaring 'opencode-subset' there violated 3 capability-
registry invariants + 2 install-plan golden masters + opencode golden parity.

The opencode-subset dialect is still IMPLEMENTED — just not via the legacy
descriptor field: hookEventSurfaceFor() (host-integration.cts) is its consumer,
and the OpenCode plugin consumes the subset events at runtime (session.idle
added here; compaction shipped in #1914). Declaring it on the descriptor would
require weakening the hooksSurface:none ⇔ no-hookEvents invariant (flagged for
decision).

* fix(#1682): refresh opencode golden parity for plugins/gsd-core.js (session.idle)

* docs(changeset): OpenCode session.idle + opencode-subset dialect (#1682)

* docs(changeset): backfill PR #1930
This commit is contained in:
Tom Boucher
2026-07-02 16:16:00 -04:00
committed by GitHub
parent 541de6894f
commit 325e9fad4b
6 changed files with 134 additions and 1 deletions

View File

@@ -512,6 +512,40 @@ function shouldFlattenDispatch(dispatch: UnvalidatedDispatch): boolean {
return !canBackground;
}
// ---------------------------------------------------------------------------
// Hook-event surface per hookEvents dialect (ADR-1239 Phase D / #1682)
// ---------------------------------------------------------------------------
// 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).
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.
* 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;
}
// ---------------------------------------------------------------------------
// Module export (CommonJS — matches existing src/*.cts pattern)
// ---------------------------------------------------------------------------
@@ -523,8 +557,10 @@ export = {
INTERFACE_POINTS,
PROFILE_BASELINES,
DEFAULT_ENGINE,
HOOK_EVENT_SURFACES,
degradationFor,
profileOf,
negotiateHostCapabilities,
shouldFlattenDispatch,
hookEventSurfaceFor,
};