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

@@ -21,8 +21,39 @@ const {
degradationFor,
profileOf,
negotiateHostCapabilities,
hookEventSurfaceFor,
HOOK_EVENT_SURFACES,
} = hi;
describe('hookEventSurfaceFor (hookEvents dialect consumer — #1682)', () => {
test('returns the full Claude surface for "claude"', () => {
const s = hookEventSurfaceFor('claude');
assert.ok(s && s.includes('PreToolUse') && s.includes('PostToolUse') && s.includes('Stop'));
});
test('returns the Gemini BeforeTool/AfterTool surface for "gemini"', () => {
const s = hookEventSurfaceFor('gemini');
assert.ok(s && s.includes('BeforeTool') && s.includes('AfterTool'));
});
test('CONSUMES "opencode-subset": OpenCode session/tool/file subset with NO workflow-phase events', () => {
const s = hookEventSurfaceFor('opencode-subset');
assert.ok(s, 'opencode-subset must resolve (non-null) — it is consumed, not reserved');
assert.ok(s.includes('experimental.session.compacting'));
assert.ok(s.includes('session.idle'));
assert.ok(s.includes('tool.execute.before') && s.includes('tool.execute.after'));
assert.ok(!s.some((e) => /plan:|verify:|ship:|execute:/.test(e)),
'opencode-subset fires no workflow-phase events (engine owns phase sequencing)');
});
test('returns null for unknown / missing / non-string dialect (fail-closed)', () => {
assert.equal(hookEventSurfaceFor('nope'), null);
assert.equal(hookEventSurfaceFor(undefined), null);
assert.equal(hookEventSurfaceFor(123), null);
});
test('HOOK_EVENT_SURFACES is frozen + covers exactly the 3 dialects', () => {
assert.equal(Object.isFrozen(HOOK_EVENT_SURFACES), true);
assert.deepEqual(Object.keys(HOOK_EVENT_SURFACES).sort(), ['claude', 'gemini', 'opencode-subset']);
});
});
// ---------------------------------------------------------------------------
// CONTRACT-PIN: constants and vocabulary
// ---------------------------------------------------------------------------