Skip to content

feat(cli): extend the OpenCode plugin to every documented context surface - #1487

Merged
DeusData merged 4 commits into
DeusData:mainfrom
JCVIDev:feat/opencode-context-hooks
Aug 31, 2026
Merged

feat(cli): extend the OpenCode plugin to every documented context surface#1487
DeusData merged 4 commits into
DeusData:mainfrom
JCVIDev:feat/opencode-context-hooks

Conversation

@JCVIDev

@JCVIDev JCVIDev commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

OpenCode is the closest client to Claude-parity for models CBM has no first-party hook dialect for (DeepSeek, etc.): it already gets MCP + the skill + the three read-only agents, but its generated plugin only covered one of the four context surfaces other clients get through hook configuration. This PR extends cbm_client_adapter_opencode() so the generated cbm-augment.ts covers every surface OpenCode documents — and nothing it doesn't:

  • grep/glob graph lookup (existing surface, improved): the plugin now pipes the event through hook-augment and unwraps hookSpecificOutput.additionalContext instead of pasting the raw Claude JSON envelope into the tool output.
  • Post-read index-coverage notes: tool.execute.after for read maps OpenCode's filePath arg to the PostToolUse/Read event hook-augment already accepts.
  • Session-start tier routing: OpenCode documents no context-output lifecycle hook, so the SessionStart note is carried on the first tool result of each session (deduped per sessionID).
  • Post-compaction reinjection: via the documented experimental.session.compacting hook, appending to the mutable output.context array.

Every path stays fail-open (all failures resolve to ''; context is only ever appended), consistent with the hook-augment contract, and the plugin body remains marker-free so cbm_text_upsert_managed_block owns the lifecycle (regenerated on install/update, per the #616 lesson).

Why only these surfaces

Checked against the published OpenCode plugin docs: tool.execute.before/after, the event bus, and experimental.session.compacting are the documented context-capable hooks; there is no chat.message-style hook. Per CBM policy the generator does not invent undocumented vendor surfaces, so session-start context rides on the first tool result rather than a fabricated lifecycle hook.

Testing

README's OpenCode matrix row updated to describe the four surfaces.

🤖 Generated with Claude Code

…face

The generated OpenCode adapter covered only the grep/glob graph lookup, so
OpenCode sessions missed the read-coverage and lifecycle context other
clients get through their hook configuration. Extend the plugin to the
surfaces OpenCode documents:

- unwrap hook-augment's additionalContext envelope so plain text, not raw
  JSON, lands in the tool output
- post-read index-coverage notes via tool.execute.after, mapping OpenCode's
  filePath argument to the file_path key the default dialect reads
- session-start tier routing injected once per sessionID on the first tool
  result, since OpenCode documents no context-output lifecycle hook
- post-compaction reinjection through the documented experimental
  session.compacting surface (output.context)

Every failure path resolves to the empty string: the adapter can degrade to
a no-op but never blocks a tool call. Pin the new surfaces in
test_agent_clients (31/31 pass; the DeusData#616 payload guard stays green) and
record the wider surface in the README client matrix.

Signed-off-by: jc <jcvi007@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@JCVIDev
JCVIDev requested a review from DeusData as a code owner August 7, 2026 13:41
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

@DeusData DeusData added enhancement New feature or request editor/integration Editor compatibility and CLI integration labels Aug 10, 2026
@DeusData DeusData added this to the 0.11.0-rc milestone Aug 10, 2026
@DeusData DeusData added ux/behavior Display bugs, docs, adoption UX security Security vulnerabilities, hardening priority/normal Standard review queue; useful PR with ordinary maintainer urgency. labels Aug 10, 2026
@DeusData

Copy link
Copy Markdown
Owner

Thank you for extending the OpenCode integration across the documented context surfaces. Because the change touches several event payloads and execution paths, it needs a careful correctness and security-aware integration review. I have routed it to 0.11.0-rc with normal priority; the queue is currently full, so detailed feedback may take a little time.

JCVIDev and others added 3 commits August 27, 2026 01:42
Maintainer mechanics on top of the accepted feature: merge current main and
format the 24 violations in the edited region of client_adapter.c — the
only red CI leg. No content change; the generated plugin is byte-identical
apart from whitespace in the C literals' layout.

Co-authored-by: jc <jcvi007@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

@JCVIDev — the reserved fit decision is made: accepted, all four surfaces. OpenCode reaches context parity with the first-tier clients, including the experimental.session.compacting reinjection — with the churn risk of that experimental API consciously accepted as ours to own and monitor, not a reason to ship less.

What carried it, concretely: the review built your branch, drove all four hooks end-to-end against a real emitted install (plain text appended where main pastes raw JSON today, dedup holding across calls, a second session getting its own note, compaction pushing into output.context, everything failing open on a cold daemon), and verified your surface inventory against OpenCode's live docs — "every documented context-capable surface" holds, including the observation that the #616-era chat-transform hook has vanished from their docs, which is exactly the churn class we're accepting. Your envelope unwrap also fixes a real shipped defect, independent of everything else.

I've pushed the two mechanical items to your branch so this doesn't cost you a round-trip: a merge of current main and a clang-format pass over the edited generator block (the 24 violations that were the only red CI leg — no content change; the emitted plugin is unchanged). Your commits and authorship are untouched. CI runs the full matrix now; merge on green, --merge as always.

Also noted with appreciation: #1483 and #1484, both real, both filed from your own verification run. That's the diligence standard this queue wishes were normal. One residual worth a comment from you if you know it offhand: whether input.sessionID is actually populated on tool.execute.after in the current SDK — the docs don't say, and if it's absent the session-note surface silently no-ops (nothing breaks; the README row would just overclaim).

@DeusData
DeusData merged commit 1e3f2fd into DeusData:main Aug 31, 2026
62 of 65 checks passed
@DeusData

Copy link
Copy Markdown
Owner

Merged as 1e3f2fd2. All four surfaces, as ruled on 28 August.

Two things from your work are worth recording rather than leaving in a merged diff:

The envelope unwrap fixed a real shipped defect that was independent of the feature you were adding. Main was pasting raw JSON where plain text belonged. You could have added the four hooks and left that alone — noticing it and fixing it while you were in there is the part that does not show up in a line count.

The surface inventory held up under checking. "Every documented context-capable surface" was verified against OpenCode's live docs, including the observation that the #616-era chat-transform hook has since disappeared from them. That is exactly the churn class this integration signs up for, and you documented it rather than quietly coding around it — which is what made accepting the experimental.session.compacting reinjection a decision we could take with our eyes open instead of a risk we discovered later.

The behaviour was checked end-to-end against a real emitted install before merge, not read: all four hooks driven live, dedup holding across calls, a second session getting its own note, compaction pushing into output.context, and every path failing open on a cold daemon.

Thank you — a strong first contribution, and the kind that leaves the integration better documented than it found it.

@JCVIDev

JCVIDev commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Apologies for the silence. This landed in a stretch of heavy workload and I simply never made it back to the thread — your question of 28 August deserved better than three days of nothing followed by a merge notification. That one is on me.

Thank you for taking the PR seriously, and for the pace once it reached the front of the queue: the fit decision on the 28th and merged on the 31st. I'm particularly grateful you pushed the merge of main and the clang-format pass to the branch yourself rather than sending it back for another round-trip, and that the review was a live end-to-end run rather than a read of the diff. That is more attention than a first contribution has any right to expect.

On the residual: yes, input.sessionID is populated on tool.execute.after. I checked the trigger call sites inside the OpenCode binary (v1.18.25) rather than the docs, which are still silent on it:

yield* i.trigger("tool.execute.after",
  { tool: u.id, sessionID: H.sessionID, callID: H.callID, args: b },
  V);

sessionID is on the input object at all five trigger sites — native tools, the three MCP resource tools, generic MCP tools, and the subtask path — and @opencode-ai/plugin types it as a required string, in contrast to shell.env where it is sessionID?: string. The distinction looks deliberate. So the session-note surface does fire, and the README row does not overclaim.

The same call sites also confirm what #1737 reported: args is on input, not output. qtchen-vault had that right on 19 August from the typings, and the binary agrees. That issue postdates this branch by twelve days, so it was not there to find when I wrote the block — but the expression was already wrong in the code I was editing, I carried it forward without checking it, and I extended what it reaches.

Two things about it are mine to own:

It widened the defect. Before this PR the only consumer of args was the grep/glob lookup. The block I added routes the post-read coverage note through the same const args = output?.args ?? {}, so args.filePath ?? args.file_path ?? args.path is always undefined and the guard drops the call before hook-augment is ever invoked. Two inert surfaces now instead of one — and the README row I updated describes both.

It is the other half of the conflict on #1933. You took that one as yours on that thread; the file it collides with is mine, so let me carry the part I can. To spare Chewji9875 a re-read of my diff: on current main the restructured block still has exactly one args binding, at src/cli/client_adapter.c:318, and both consumers read from it — tool_input: args at 325 and the filePath lookup at 329. So their one-line edit does still land where they intend, and const args = input?.args ?? {} now fixes both surfaces at once rather than one. I'll say the same on that thread.

One note, offered tentatively because it rests on a guess about a harness I have not seen: if the payloads in that end-to-end run were constructed rather than captured, they would have been built from the plugin's own assumption — args on output — at which point the plugin reads them correctly and the run passes. That would explain why a real live check could sit on top of this without surfacing it, and why the three behaviours it did pin down are exactly the ones that do not depend on args. If that is the shape of it, a payload captured from a live session would be worth having for every client whose event shape we match from the outside. If it is not, ignore me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

editor/integration Editor compatibility and CLI integration enhancement New feature or request priority/normal Standard review queue; useful PR with ordinary maintainer urgency. security Security vulnerabilities, hardening ux/behavior Display bugs, docs, adoption UX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants