Skip to content

feat(cli): surface trace messages to the user in opencode - #3434

Open
migmartri wants to merge 1 commit into
mainfrom
feat/opencode-trace-announcements
Open

feat(cli): surface trace messages to the user in opencode#3434
migmartri wants to merge 1 commit into
mainfrom
feat/opencode-trace-announcements

Conversation

@migmartri

@migmartri migmartri commented Sep 11, 2026

Copy link
Copy Markdown
Member

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-by trailer.

🤖 Posted by Maximus bot (Claude Code) on behalf of @migmartri

Review in cubic

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
@chainloop-platform

chainloop-platform Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

AI Session Checks — 🟢 90% · ✅ 0 failing

Avg score Sessions Failing policies Attribution Files Lines Total Duration
🟢 90% 1 ✅ 0 100% AI / 0% Human 12 +495 / -57 5h28m21s

🟢 90% — 100% AI — ✅ All policies passing

Sep 11, 2026 10:12 UTC · 5h28m21s · $23.51 · 486 in / 135.5k out · claude-code 2.1.268 (claude-opus-5)

View session details ↗

Change Summary

  • Extends the opencode trace provider and plugin to surface session-start and follow-up trace messages.
  • Adds and updates Go and TypeScript tests plus golden plugin fixtures for the new opencode behavior.
  • Simplifies shared trace banner handling by moving Claude-specific padding into its provider and overlapping dashboard lookup with session tracking.

AI Session Overall Score

🟢 90% — Aligned, well-verified implementation with only modest upfront planning.

AI Session Analysis Breakdown

🟢 95% · verification

🟢 The session went failing-test first, then reran targeted, broad, race, and post-rebase checks to green. · High Impact

🟢 92% · alignment

🟢 The assistant’s feature summary matched the later command-backed verification and touched areas. · High Impact

🟢 91% · user-trust-signal

No notes.

🟢 90% · solution-quality

No notes.

🟢 89% · scope-discipline

🟢 The only scope expansion came from the user’s explicit /simplify follow-up. · Medium Impact

🟡 72% · context-and-planning

🟠 The initial cross-provider Go and TypeScript implementation began without a visible shared plan or TODO list. · Medium Severity

💡 For multi-file cross-language work, sketch the file-by-file plan before editing so tradeoffs are explicit early.


File Attribution

████████████████████ 100% AI / 0% Human

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 -
⚠️ Failed 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

View attestation ↗


Powered by Chainloop and Chainloop Trace

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

View Feedback

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

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