feat(cli): surface trace messages to the user in opencode - #3434
feat(cli): surface trace messages to the user in opencode#3434migmartri wants to merge 1 commit into
Conversation
Implement SystemMessage and AnnounceToUser for the opencode trace provider, so the session-start banner and the post-push session link reach the user as they already do in Claude Code. opencode defines no hook-response protocol of its own, so the hook writes a small JSON contract to stdout and the generated plugin reads it, showing the message as a TUI toast and relaying it to the model through the shell tool's output. The blank lines that frame the banner in Claude Code's transcript move from the shared session-start handler into the Claude provider, since a toast supplies its own frame, and the model-relay instruction is now shared between providers. The banner's dashboard lookup runs alongside session tracking rather than after it. Assisted-by: Claude Code Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev> Chainloop-Trace-Sessions: da0b9348-ee7e-479b-97af-edc7c49e0bbd
AI Session Checks — 🟢 90% · ✅ 0 failing
|
| Status | Attribution | File | Lines |
|---|---|---|---|
| created | ai | app/cli/internal/trace/opencode/announce_test.go |
+137 / -0 |
| modified | ai | app/cli/internal/trace/opencode/provider.go |
+52 / -13 |
| modified | ai | .opencode/plugins/chainloop-trace.ts |
+45 / -8 |
| modified | ai | app/cli/internal/trace/opencode/hooks.go |
+45 / -8 |
| modified | ai | app/cli/internal/trace/opencode/testdata/plugin_full.ts |
+45 / -8 |
| modified | ai | app/cli/internal/trace/opencode/testdata/plugin_tracerun.ts |
+45 / -8 |
| modified | ai | app/cli/internal/trace/claude/announce_test.go |
+45 / -0 |
| modified | ai | app/cli/pkg/action/trace_agent_hook.go |
+31 / -7 |
| modified | ai | app/cli/internal/trace/opencode/hooks_test.go |
+30 / -0 |
| modified | ai | app/cli/internal/trace/claude/provider.go |
+9 / -3 |
| modified | ai | app/cli/internal/trace/provider.go |
+9 / -0 |
| modified | ai | app/cli/internal/trace/providers/capabilities_test.go |
+2 / -2 |
Policies (4)
| Status | Policy | Material | Messages |
|---|---|---|---|
| ✅ Passed | ai-config-ai-agents-allowed |
ai-coding-session-da0b93 |
- |
| ✅ Passed | ai-config-no-dangerous-commands |
ai-coding-session-da0b93 |
- |
| ✅ Passed | ai-config-no-secrets |
ai-coding-session-da0b93 |
- |
| ✅ Passed | ai-config-mcp-servers-allowed |
ai-coding-session-da0b93 |
- |
Security Checks — ⚠️ 1 failing
✅ secret-scan
| Status | Policy | Messages |
|---|---|---|
| ✅ Passed | secrets-detection |
- |
✅ sast-scan
| Status | Policy | Messages |
|---|---|---|
| ✅ Passed | owasp-top10-2025 |
- |
| ✅ Passed | sast |
- |
| ✅ Passed | cwe-top25 |
- |
| ✅ Passed | cwe-top26-40-cusp |
- |
PR info
| Status | Policy | Messages |
|---|---|---|
| ✅ Passed | pr-user-story-linked |
- |
pr-min-approvals |
PR/MR #3434 has 0 approving reviews, 1 required. | |
| ✅ Passed | pr-description-required |
- |
⏭️ 3 scans not applied
| Scan | Reason |
|---|---|
vulnerability-scan |
no manifest/lockfile changed |
github-actions-scan |
no workflow files changed |
iac-scan |
no IaC files changed |
Powered by Chainloop and Chainloop Trace
There was a problem hiding this comment.
3 issues found across 12 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/cli/internal/trace/opencode/hooks.go">
<violation number="1" location="app/cli/internal/trace/opencode/hooks.go:138">
P2: When the TUI request is slow or unavailable, awaiting `toast` blocks completion of the shell tool and delays the model relay. Fire the best-effort toast without awaiting it so the tool output can be returned immediately.</violation>
</file>
<file name="app/cli/internal/trace/opencode/provider.go">
<violation number="1" location="app/cli/internal/trace/opencode/provider.go:217">
P2: When a user upgrades the CLI without rerunning `chainloop trace init`, this line sends JSON through the old generated plugin, which does not capture and parse hook stdout; the user sees raw JSON instead of a toast or model relay. Add a plugin-version/migration check or require regeneration before emitting this response.
(Based on your team's feedback about cross-component and version compatibility.)</violation>
</file>
<file name="app/cli/internal/trace/opencode/testdata/plugin_full.ts">
<violation number="1" location="app/cli/internal/trace/opencode/testdata/plugin_full.ts:117">
P2: `output.output` is mutated without guarding the tool result. When a bash tool is aborted, `output` can be null, and this line then throws a TypeError that rejects the `tool.execute.after` hook; when `output.output` is undefined it injects literal "undefined\n\n..." into the output the model reads. Every other failure path here logs and continues, so guard this one too: only append when a string output is present.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| const sessionID = event.properties?.info?.id ?? "" | ||
| await fire("session-start", { session_id: sessionID, hook_event_name: "session.created" }) | ||
| const res = await fire("session-start", { session_id: sessionID, hook_event_name: "session.created" }) | ||
| if (res.message) await toast(res.message) |
There was a problem hiding this comment.
P2: When the TUI request is slow or unavailable, awaiting toast blocks completion of the shell tool and delays the model relay. Fire the best-effort toast without awaiting it so the tool output can be returned immediately.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/cli/internal/trace/opencode/hooks.go, line 138:
<comment>When the TUI request is slow or unavailable, awaiting `toast` blocks completion of the shell tool and delays the model relay. Fire the best-effort toast without awaiting it so the tool output can be returned immediately.</comment>
<file context>
@@ -89,22 +99,43 @@ export const ChainloopTrace: Plugin = async ({ $ }) => {
const sessionID = event.properties?.info?.id ?? ""
- await fire("session-start", { session_id: sessionID, hook_event_name: "session.created" })
+ const res = await fire("session-start", { session_id: sessionID, hook_event_name: "session.created" })
+ if (res.message) await toast(res.message)
}
{{SessionEndBlock}}
</file context>
| if (res.message) await toast(res.message) | |
| if (res.message) void toast(res.message) |
| // is reserved for it: the hook's logging goes to stderr and to the trace log | ||
| // file, so nothing else can corrupt what the plugin parses. | ||
| func writeHookResponse(resp *hookResponse) error { | ||
| return json.NewEncoder(os.Stdout).Encode(resp) |
There was a problem hiding this comment.
P2: When a user upgrades the CLI without rerunning chainloop trace init, this line sends JSON through the old generated plugin, which does not capture and parse hook stdout; the user sees raw JSON instead of a toast or model relay. Add a plugin-version/migration check or require regeneration before emitting this response.
(Based on your team's feedback about cross-component and version compatibility.)
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/cli/internal/trace/opencode/provider.go, line 217:
<comment>When a user upgrades the CLI without rerunning `chainloop trace init`, this line sends JSON through the old generated plugin, which does not capture and parse hook stdout; the user sees raw JSON instead of a toast or model relay. Add a plugin-version/migration check or require regeneration before emitting this response.
(Based on your team's feedback about cross-component and version compatibility.) </comment>
<file context>
@@ -158,24 +159,62 @@ func (p *Provider) CleanupAfterEdit(store *state.Store, input *trace.HookInput)
+// is reserved for it: the hook's logging goes to stderr and to the trace log
+// file, so nothing else can corrupt what the plugin parses.
+func writeHookResponse(resp *hookResponse) error {
+ return json.NewEncoder(os.Stdout).Encode(resp)
}
</file context>
| // channels: the toast reaches the user now, the tool output reaches | ||
| // the model, whose reply outlives the toast. | ||
| if (res.message) await toast(res.message) | ||
| if (res.relayToModel) output.output = output.output + "\n\n" + res.relayToModel |
There was a problem hiding this comment.
P2: output.output is mutated without guarding the tool result. When a bash tool is aborted, output can be null, and this line then throws a TypeError that rejects the tool.execute.after hook; when output.output is undefined it injects literal "undefined\n\n..." into the output the model reads. Every other failure path here logs and continues, so guard this one too: only append when a string output is present.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/cli/internal/trace/opencode/testdata/plugin_full.ts, line 117:
<comment>`output.output` is mutated without guarding the tool result. When a bash tool is aborted, `output` can be null, and this line then throws a TypeError that rejects the `tool.execute.after` hook; when `output.output` is undefined it injects literal "undefined\n\n..." into the output the model reads. Every other failure path here logs and continues, so guard this one too: only append when a string output is present.</comment>
<file context>
@@ -71,13 +102,19 @@ export const ChainloopTrace: Plugin = async ({ $ }) => {
+ // channels: the toast reaches the user now, the tool output reaches
+ // the model, whose reply outlives the toast.
+ if (res.message) await toast(res.message)
+ if (res.relayToModel) output.output = output.output + "\n\n" + res.relayToModel
return
}
</file context>
Extends the user-facing trace messages to opencode. Until now only Claude Code received them: the session-start banner naming where evidence is going, and the link to the attested session after a push.
opencode defines no hook-response protocol of its own, so the hook writes a small JSON contract to stdout and the generated opencode plugin reads it, putting the message on two channels — a TUI toast, which reaches the user directly, and the shell tool's output, which reaches the model so it can repeat the link in its reply.
Two adjustments to the shared layer came with it. The blank lines that frame the banner in Claude Code's transcript move from the shared session-start handler into the Claude provider, since a toast supplies its own frame and any provider now receives the banner unadorned. The instruction that asks the model to repeat a message verbatim is now a single shared constant rather than a copy per provider. The banner's dashboard lookup also runs alongside session tracking instead of after it, so the two waits no longer add up at session start.
Follow-up to #3429 / #3430, which introduced the session link for Claude Code.
This pull request was produced with AI assistance (Claude Code); the commit carries an
Assisted-bytrailer.🤖 Posted by Maximus bot (Claude Code) on behalf of @migmartri