Skip to content

feat: extend the hook contracts to opencode and Crush - #217

Merged
omartelo merged 1 commit into
mainfrom
feat/opencode-crush-hooks
Aug 11, 2026
Merged

omartelo merged 1 commit into
mainfrom
feat/opencode-crush-hooks

Conversation

@omartelo

@omartelo omartelo commented Aug 11, 2026 •

Copy link
Copy Markdown
Collaborator

Documents the hook contracts for the two harnesses lich could already spawn but never heard from, and pins the payloads their clients send. Contract-first: this is the half that lands before omartelo/lich-plugin#10, which implements it.

What changes

Prose and fixtures only — no endpoint moved. provider has accepted any id in providers.Registry since the field existed, and opencode and crush have been in that registry since they became spawnable. Each contract's mapping table gains a column per harness, and fixtures/session-start.jsonl gains a line per provider id so both sides assert the same bytes.

What the prose adds is the part the code cannot say — which of each harness's events maps onto which report, and which reports it cannot make at all.

opencode reports all four. Its session.status carries the state rather than implying it, and its idle is the turn ending, which is this contract's done. Nothing there reports lich's own idle: the event that would say "the CLI has left" is the opencode server exiting with the plugin inside it.

Crush reports two. PreToolUse is the only event Crush has. A busy with nothing to end it would pin a spinner to a card — wrong for longer than it is right — so the state and title columns are empty on purpose, and the plugin registers only the session id and the git-status refresh. That refresh runs one tool early, which is named in the ceilings rather than papered over.

Neither resumes yet. Both spell it (opencode --session <id>, crush --session <id>), but ResumeAvailable only offers a resume it has proof of, and both keep conversations in SQLite where there is no per-session file to glob. The id is stored today so the day one is wired the sessions already carry it.

How it was checked

Every row was taken off a real run against a stub listener, not off the harnesses' docs:

  • Crush — crush run in an isolated XDG_CONFIG_HOME, with the plugin's existing scripts registered. All three reports arrived on the one PreToolUse event; tool names came back lower-cased (view to read, write to write) and the stdin envelope is {event, session_id, cwd, tool_name, tool_input}.
  • opencode — opencode serve driven through its own HTTP API against a stub OpenAI-compatible provider, so a turn with a tool call was deterministic and cost nothing. Confirmed session.created, session.status (busy/idle), permission.asked, file.edited, tool.execute.before and session.updated carrying the title, plus that the plugin receives event.properties (not event.data).

Test plan

  • go test ./... — 1193 pass, including the fixture suite over the two new lines
  • gofmt -l . and go vet ./... clean
  • biome check, vitest run (793), tsc --noEmit clean

Each contract's mapping table gains a column per harness, so the two
newest ones are documented where the other two already are, and
session-start gains a fixture line for each provider id — the payload
their clients POST is now pinned as bytes like every other.

The endpoints needed nothing: `provider` has accepted any id in
providers.Registry since it existed, and opencode and Crush have been in
that registry since they were spawnable. What the prose adds is the part
the code cannot say — which of each harness's events maps onto which
report, and which reports it cannot make at all:

- opencode reports all four. Its `session.status` carries the state
  rather than implying it, and its `idle` is the turn ending, which is
  this contract's `done`. Nothing there reports lich's own `idle`: the
  event that would say "the CLI has left" is the server exiting with the
  plugin inside it.
- Crush reports two. `PreToolUse` is the only event it has, so a `busy`
  with nothing to end it would pin a spinner to a card — worse for
  longer than it is right. It reports its session id and refreshes git
  status; the refresh runs one tool early, which the ceilings name.

Both spell resume (`--session <id>`), and neither is wired: lich only
offers a resume it has proof of, and both keep conversations in SQLite
where there is no per-session file to glob. The id is stored today so
the day one is wired the sessions already carry it.

Every row was taken off a real run of each CLI against a stub listener.
@omartelo
omartelo merged commit 514961e into main Aug 11, 2026
3 checks passed
@omartelo
omartelo deleted the feat/opencode-crush-hooks branch August 11, 2026 11:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant