@mrc2204/opencode-bridge 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,25 @@
1
+ # OpenCode-side plugin boundary
2
+
3
+ This directory is the intended home for the canonical OpenCode-side callback plugin source.
4
+
5
+ ## Current transitional state
6
+ The active runtime-tested plugin file currently lives at:
7
+ - `.opencode/plugins/openclaw-bridge-callback.ts`
8
+
9
+ That location is convenient for project-local loading in OpenCode.
10
+
11
+ ## Current source-of-truth
12
+ Canonical OpenCode-side plugin source now lives at:
13
+ - `opencode-plugin/openclaw-bridge-callback.ts`
14
+
15
+ The runtime-loaded project-local file remains:
16
+ - `.opencode/plugins/openclaw-bridge-callback.ts`
17
+
18
+ but it should be treated as a thin re-export shim for local OpenCode loading during development.
19
+
20
+ ## Intended next step
21
+ Promote this boundary further so the repository can ship:
22
+ - OpenClaw-side bridge artifact
23
+ - OpenCode-side callback plugin artifact
24
+
25
+ from one source-of-truth without mixing runtime test placement with canonical source layout.
@@ -0,0 +1,186 @@
1
+ import { appendFileSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { buildPluginCallbackDedupeKey, parseTaggedSessionTitle } from "../src/shared-contracts";
4
+
5
+ function asString(value: unknown): string | undefined {
6
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
7
+ }
8
+
9
+ function ensureAuditDir(directory: string) {
10
+ mkdirSync(directory, { recursive: true });
11
+ return directory;
12
+ }
13
+
14
+ function getAuditPath(directory: string) {
15
+ const auditDir = asString(process.env.OPENCLAW_BRIDGE_AUDIT_DIR) || join(directory, ".opencode");
16
+ ensureAuditDir(auditDir);
17
+ return join(auditDir, "bridge-callback-audit.jsonl");
18
+ }
19
+
20
+ function appendAudit(directory: string, record: any) {
21
+ const path = getAuditPath(directory);
22
+ appendFileSync(path, JSON.stringify({ ...record, created_at: new Date().toISOString() }) + "\n", "utf8");
23
+ }
24
+
25
+ function getOpenClawAuditPath() {
26
+ const explicit = asString(process.env.OPENCLAW_BRIDGE_OPENCLAW_AUDIT_PATH);
27
+ if (explicit) {
28
+ ensureAuditDir(join(explicit, ".."));
29
+ return explicit;
30
+ }
31
+ const home = asString(process.env.HOME);
32
+ if (!home) return null;
33
+ const auditDir = join(home, ".openclaw", "opencode-bridge", "audit");
34
+ ensureAuditDir(auditDir);
35
+ return join(auditDir, "callbacks.jsonl");
36
+ }
37
+
38
+ function appendOpenClawAudit(record: any) {
39
+ const path = getOpenClawAuditPath();
40
+ if (!path) return;
41
+ appendFileSync(path, JSON.stringify({ ...record, createdAt: new Date().toISOString() }) + "\n", "utf8");
42
+ }
43
+
44
+ function buildCallbackPayload(tags: Record<string, string>, eventType: string) {
45
+ const agentId = tags.requested || tags.requested_agent_id;
46
+ const sessionKey = tags.callbackSession || tags.callback_target_session_key;
47
+ const sessionId = tags.callbackSessionId || tags.callback_target_session_id;
48
+ if (!agentId || !sessionKey) return null;
49
+ return {
50
+ message: `OpenCode plugin event=${eventType} run=${tags.runId || tags.run_id || "unknown"} task=${tags.taskId || tags.task_id || "unknown"}`,
51
+ name: "OpenCode",
52
+ agentId,
53
+ sessionKey,
54
+ ...(sessionId ? { sessionId } : {}),
55
+ wakeMode: "now",
56
+ deliver: false,
57
+ };
58
+ }
59
+
60
+ async function postCallback(directory: string, payload: any, meta?: { eventType?: string; sessionId?: string; runId?: string; taskId?: string; requestedAgentId?: string; resolvedAgentId?: string; callbackTargetSessionKey?: string; callbackTargetSessionId?: string }) {
61
+ const hookBaseUrl = asString(process.env.OPENCLAW_HOOK_BASE_URL);
62
+ const hookToken = asString(process.env.OPENCLAW_HOOK_TOKEN);
63
+ if (!hookBaseUrl || !hookToken) {
64
+ appendAudit(directory, { ok: false, status: 0, reason: "missing_hook_env", payload, meta });
65
+ appendOpenClawAudit({
66
+ taskId: meta?.taskId,
67
+ runId: meta?.runId,
68
+ agentId: payload?.agentId,
69
+ requestedAgentId: meta?.requestedAgentId,
70
+ resolvedAgentId: meta?.resolvedAgentId,
71
+ sessionKey: undefined,
72
+ callbackTargetSessionKey: meta?.callbackTargetSessionKey,
73
+ callbackTargetSessionId: meta?.callbackTargetSessionId,
74
+ event: meta?.eventType,
75
+ callbackStatus: 0,
76
+ callbackOk: false,
77
+ callbackBody: "missing_hook_env",
78
+ });
79
+ return { ok: false, status: 0, reason: "missing_hook_env" };
80
+ }
81
+ const response = await fetch(`${hookBaseUrl.replace(/\/$/, "")}/hooks/agent`, {
82
+ method: "POST",
83
+ headers: {
84
+ Authorization: `Bearer ${hookToken}`,
85
+ "Content-Type": "application/json",
86
+ },
87
+ body: JSON.stringify(payload),
88
+ });
89
+ const text = await response.text();
90
+ appendAudit(directory, { ok: response.ok, status: response.status, body: text, payload, meta });
91
+ const openClawAuditRecord = {
92
+ taskId: meta?.taskId,
93
+ runId: meta?.runId,
94
+ agentId: payload?.agentId,
95
+ requestedAgentId: meta?.requestedAgentId,
96
+ resolvedAgentId: meta?.resolvedAgentId,
97
+ sessionKey: undefined,
98
+ callbackTargetSessionKey: meta?.callbackTargetSessionKey,
99
+ callbackTargetSessionId: meta?.callbackTargetSessionId,
100
+ event: meta?.eventType,
101
+ callbackStatus: response.status,
102
+ callbackOk: response.ok,
103
+ callbackBody: text,
104
+ };
105
+ appendAudit(directory, { phase: "openclaw_audit_mirror", record: openClawAuditRecord });
106
+ appendOpenClawAudit(openClawAuditRecord);
107
+ return { ok: response.ok, status: response.status, body: text };
108
+ }
109
+
110
+ const callbackDedupe = new Set<string>();
111
+ const sessionTagCache = new Map<string, Record<string, string>>();
112
+
113
+ function readSessionId(event: any): string | undefined {
114
+ return (
115
+ asString(event?.session?.id) ||
116
+ asString(event?.session?.sessionID) ||
117
+ asString(event?.data?.session?.id) ||
118
+ asString(event?.data?.sessionID) ||
119
+ asString(event?.properties?.sessionID) ||
120
+ asString(event?.properties?.info?.sessionID) ||
121
+ asString(event?.properties?.info?.id) ||
122
+ asString(event?.payload?.sessionID)
123
+ );
124
+ }
125
+
126
+ function isTerminalEvent(_event: any, type: string): boolean {
127
+ return type === "session.idle" || type === "session.error";
128
+ }
129
+
130
+ export const OpenClawBridgeCallbackPlugin = async ({ client, directory }: any) => {
131
+ await client.app.log({
132
+ body: {
133
+ service: "openclaw-bridge-callback",
134
+ level: "info",
135
+ message: "OpenClaw bridge callback plugin initialized",
136
+ extra: { directory },
137
+ },
138
+ });
139
+
140
+ return {
141
+ event: async ({ event }: any) => {
142
+ const type = asString(event?.type) || "unknown";
143
+ const sessionId = readSessionId(event);
144
+ const title =
145
+ asString(event?.session?.title) ||
146
+ asString(event?.data?.session?.title) ||
147
+ asString(event?.data?.title) ||
148
+ asString(event?.properties?.info?.title) ||
149
+ asString(event?.properties?.title) ||
150
+ asString(event?.payload?.session?.title) ||
151
+ asString(event?.payload?.title);
152
+ const parsedTags = parseTaggedSessionTitle(title);
153
+ if (sessionId && parsedTags && (parsedTags.callbackSession || parsedTags.callback_target_session_key)) {
154
+ sessionTagCache.set(sessionId, parsedTags);
155
+ }
156
+ const tags = parsedTags || (sessionId ? sessionTagCache.get(sessionId) || null : null);
157
+ appendAudit(directory, { phase: "event_seen", event_type: type, session_id: sessionId, title, tags, raw: event });
158
+ if (!tags || !(tags.callbackSession || tags.callback_target_session_key)) return;
159
+ if (!isTerminalEvent(event, type)) return;
160
+ const dedupeKey = buildPluginCallbackDedupeKey({ sessionId, runId: tags.runId || tags.run_id });
161
+ if (callbackDedupe.has(dedupeKey)) {
162
+ appendAudit(directory, { phase: "deduped", event_type: type, session_id: sessionId, dedupeKey, tags });
163
+ return;
164
+ }
165
+ callbackDedupe.add(dedupeKey);
166
+ const payload = buildCallbackPayload(tags, type);
167
+ if (!payload) {
168
+ appendAudit(directory, { phase: "skipped_no_payload", event_type: type, session_id: sessionId, tags });
169
+ return;
170
+ }
171
+ await postCallback(directory, payload, {
172
+ eventType: type,
173
+ sessionId,
174
+ runId: tags.runId || tags.run_id,
175
+ taskId: tags.taskId || tags.task_id,
176
+ requestedAgentId: tags.requested || tags.requested_agent_id,
177
+ resolvedAgentId: tags.resolved || tags.resolved_agent_id,
178
+ callbackTargetSessionKey: tags.callbackSession || tags.callback_target_session_key,
179
+ callbackTargetSessionId: tags.callbackSessionId || tags.callback_target_session_id,
180
+ });
181
+ },
182
+ };
183
+ };
184
+
185
+ export default OpenClawBridgeCallbackPlugin;
186
+
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@mrc2204/opencode-bridge",
3
- "version": "0.1.2",
4
- "description": "OpenClaw ↔ OpenCode bridge plugin for routing, callback, SSE probing, and runtime-ops scaffolding.",
3
+ "version": "0.1.3",
4
+ "description": "OpenClaw ↔ OpenCode bridge plugin for hybrid routing, callback orchestration, and multi-project-safe runtime control.",
5
5
  "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
6
+ "main": "./dist/src/index.js",
7
+ "types": "./dist/src/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "types": "./dist/index.d.ts",
11
- "import": "./dist/index.js",
12
- "default": "./dist/index.js"
10
+ "types": "./dist/src/index.d.ts",
11
+ "import": "./dist/src/index.js",
12
+ "default": "./dist/src/index.js"
13
13
  }
14
14
  },
15
15
  "private": false,
@@ -23,9 +23,13 @@
23
23
  },
24
24
  "scripts": {
25
25
  "clean": "rm -rf dist",
26
- "build": "tsup src/index.ts src/observability.ts --format esm --dts --clean",
26
+ "build": "tsup --config tsup.config.ts",
27
27
  "typecheck": "tsc --noEmit",
28
28
  "test": "tsx test/run-tests.ts",
29
+ "materialize:opencode-plugin:project": "node ./scripts/materialize-opencode-plugin.mjs --mode project",
30
+ "materialize:opencode-plugin:global": "node ./scripts/materialize-opencode-plugin.mjs --mode global",
31
+ "install:bridge:project": "node ./scripts/install-bridge.mjs --mode project",
32
+ "install:bridge:global": "node ./scripts/install-bridge.mjs --mode global",
29
33
  "prepublishOnly": "npm run build && npm run test"
30
34
  },
31
35
  "devDependencies": {
@@ -36,12 +40,16 @@
36
40
  },
37
41
  "openclaw": {
38
42
  "extensions": [
39
- "./dist/index.js"
43
+ "./dist/src/index.js"
40
44
  ]
41
45
  },
42
46
  "files": [
43
47
  "dist/",
44
48
  "skills/",
49
+ "opencode-plugin/",
50
+ "scripts/materialize-opencode-plugin.mjs",
51
+ "scripts/install-bridge.mjs",
52
+ "src/shared-contracts.ts",
45
53
  "README.md",
46
54
  "README.en.md",
47
55
  "openclaw.plugin.json"
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ import { resolve, dirname } from "node:path";
3
+ import process from "node:process";
4
+ import { spawnSync } from "node:child_process";
5
+
6
+ function parseArgs(argv) {
7
+ const args = { mode: "project", target: undefined, skipOpenClaw: false, skipOpencode: false };
8
+ for (let i = 0; i < argv.length; i += 1) {
9
+ const arg = argv[i];
10
+ if (arg === "--mode") args.mode = argv[++i] || args.mode;
11
+ else if (arg === "--target") args.target = argv[++i];
12
+ else if (arg === "--skip-openclaw") args.skipOpenClaw = true;
13
+ else if (arg === "--skip-opencode") args.skipOpencode = true;
14
+ }
15
+ return args;
16
+ }
17
+
18
+ function run(command, args, options = {}) {
19
+ const result = spawnSync(command, args, {
20
+ stdio: "inherit",
21
+ shell: false,
22
+ ...options,
23
+ });
24
+ if (result.status !== 0) {
25
+ process.exit(result.status || 1);
26
+ }
27
+ }
28
+
29
+ const args = parseArgs(process.argv.slice(2));
30
+ const repoRoot = resolve(dirname(new URL(import.meta.url).pathname), "..");
31
+ const target = args.target ? resolve(args.target) : process.cwd();
32
+
33
+ if (!args.skipOpenClaw) {
34
+ run("openclaw", ["plugins", "install", "-l", repoRoot]);
35
+ }
36
+
37
+ if (!args.skipOpencode) {
38
+ const materializeArgs = [resolve(repoRoot, "scripts", "materialize-opencode-plugin.mjs"), "--mode", args.mode];
39
+ if (args.mode === "project") {
40
+ materializeArgs.push("--target", target);
41
+ } else if (args.target) {
42
+ materializeArgs.push("--target", target);
43
+ }
44
+ run("node", materializeArgs, { cwd: repoRoot });
45
+ }
46
+
47
+ const summary = {
48
+ ok: true,
49
+ mode: args.mode,
50
+ repoRoot,
51
+ target,
52
+ steps: {
53
+ openclawInstalled: !args.skipOpenClaw,
54
+ opencodeMaterialized: !args.skipOpencode,
55
+ },
56
+ note: args.mode === "global"
57
+ ? "Global mode installed OpenClaw plugin locally and materialized OpenCode plugin into global config dir."
58
+ : "Project mode installed OpenClaw plugin locally and materialized OpenCode plugin into the target project's .opencode directory.",
59
+ };
60
+ console.log(JSON.stringify(summary, null, 2));
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ import { mkdirSync, copyFileSync, writeFileSync, existsSync, readFileSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import process from "node:process";
5
+
6
+ function parseArgs(argv) {
7
+ const args = { mode: "project", target: undefined, force: false };
8
+ for (let i = 0; i < argv.length; i += 1) {
9
+ const arg = argv[i];
10
+ if (arg === "--mode") args.mode = argv[++i] || args.mode;
11
+ else if (arg === "--target") args.target = argv[++i];
12
+ else if (arg === "--force") args.force = true;
13
+ }
14
+ return args;
15
+ }
16
+
17
+ function ensurePluginEntry(configPath, pluginRef) {
18
+ if (!existsSync(configPath)) return { updated: false, reason: "missing_config" };
19
+ const raw = readFileSync(configPath, "utf8");
20
+ const data = JSON.parse(raw);
21
+ const plugins = Array.isArray(data.plugin) ? data.plugin : [];
22
+ if (!plugins.includes(pluginRef)) {
23
+ data.plugin = [...plugins, pluginRef];
24
+ writeFileSync(configPath, JSON.stringify(data, null, 2) + "\n", "utf8");
25
+ return { updated: true, reason: "added_plugin_ref" };
26
+ }
27
+ return { updated: false, reason: "already_present" };
28
+ }
29
+
30
+ const args = parseArgs(process.argv.slice(2));
31
+ const repoRoot = resolve(dirname(new URL(import.meta.url).pathname), "..");
32
+ const pluginArtifact = join(repoRoot, "dist", "opencode-plugin", "openclaw-bridge-callback.js");
33
+ const sharedChunkArtifact = join(repoRoot, "dist", "chunk-TDVN5AFB.js");
34
+ if (!existsSync(pluginArtifact)) {
35
+ console.error(`Missing built plugin artifact: ${pluginArtifact}. Run npm run build first.`);
36
+ process.exit(1);
37
+ }
38
+ if (!existsSync(sharedChunkArtifact)) {
39
+ console.error(`Missing shared chunk artifact: ${sharedChunkArtifact}. Run npm run build first.`);
40
+ process.exit(1);
41
+ }
42
+
43
+ const targetBase = args.target
44
+ ? resolve(args.target)
45
+ : args.mode === "global"
46
+ ? resolve(process.env.HOME || "~", ".config", "opencode")
47
+ : repoRoot;
48
+
49
+ const pluginDir = args.mode === "global"
50
+ ? join(targetBase, "plugins")
51
+ : join(targetBase, ".opencode", "plugins");
52
+ const targetFile = join(pluginDir, "openclaw-bridge-callback.js");
53
+
54
+ mkdirSync(pluginDir, { recursive: true });
55
+ copyFileSync(pluginArtifact, targetFile);
56
+
57
+ let configUpdate = { updated: false, reason: "not_requested" };
58
+ if (args.mode === "global") {
59
+ const configPath = join(targetBase, "opencode.json");
60
+ configUpdate = ensurePluginEntry(configPath, "./plugins/openclaw-bridge-callback.js");
61
+ } else {
62
+ const configPath = join(targetBase, ".opencode", "opencode.json");
63
+ configUpdate = ensurePluginEntry(configPath, "./plugins/openclaw-bridge-callback.js");
64
+ }
65
+
66
+ console.log(JSON.stringify({
67
+ ok: true,
68
+ mode: args.mode,
69
+ sourceFile: pluginArtifact,
70
+ targetFile,
71
+ configUpdate,
72
+ note: args.mode === "global"
73
+ ? "Copy complete. Global OpenCode config was auto-patched when present."
74
+ : "Copy complete. Project-local .opencode/opencode.json was updated when present.",
75
+ }, null, 2));
@@ -280,71 +280,81 @@ Escalate to orchestrator (`scrum`/owner session) when:
280
280
 
281
281
  ---
282
282
 
283
- ## Minimal runtime bridge (v0-min)
283
+ ## Execution strategy (current standard)
284
284
 
285
- To avoid ad-hoc `exec opencode ...`, use standard entrypoint:
285
+ Use a **hybrid execution model**:
286
286
 
287
- - `node scripts/opencode-coding-runner.mjs --workstream coding_execution ...`
287
+ - **CLI-direct** for lightweight one-shot execution
288
+ - **serve/plugin mode** for canonical callback, event-driven lifecycle, observability, and multi-project-safe control plane
288
289
 
289
- Bridge rules:
290
+ Rules:
290
291
 
291
- - Accept only `workstream=coding_execution`.
292
- - Other workstreams are rejected with exit code `2`.
293
- - Runner must always emit metadata JSON (stdout + `--out` file) with at least:
294
- - `invocation.command/rendered`
295
- - `opencode.agent/model`
296
- - `process.exit_code`
297
- - `result.status/error`
298
- - `evidence.git_commit`
292
+ - Do not assume `serve` is required for every coding task.
293
+ - Do not assume `CLI-direct` is sufficient when callback/lifecycle tracking matters.
294
+ - Choose the lane deliberately and report which lane was used.
299
295
 
300
- Suggested packet usage:
296
+ ### Prefer CLI-direct when
297
+ - the task is lightweight or one-shot
298
+ - no callback or long-lived lifecycle tracking is required
299
+ - no serve/session registry management is needed
301
300
 
302
- - `--packet <path-to-packet.json>` to auto-map `task_id`, `owner_agent`, `objective`.
301
+ ### Prefer serve/plugin mode when
302
+ - callback correctness matters
303
+ - event-driven lifecycle handling is required
304
+ - multi-project safety matters
305
+ - observability/session/event introspection is required
306
+ - multiple tasks may reuse the same project-bound runtime
303
307
 
304
308
  ## OpenCode Bridge usage (current team standard)
305
309
 
306
310
  Current team workflow after planning is:
307
311
 
308
312
  1. `using-superpowers`
309
- 2. `brainstorming` (khi cần làm design/spec)
313
+ 2. `brainstorming` (when design/spec clarification is needed)
310
314
  3. `writing-plans`
311
315
  4. `execute`
312
316
  5. `verification-before-completion`
313
317
 
314
- Trong bước **execute**, nếu routing đi vào OpenCode lane thì phải ưu tiên dùng **`opencode-bridge`** / bridge contract hiện tại thay ad-hoc pattern cũ.
318
+ During **execute**, if routing goes into the OpenCode lane, use the bridge-aware strategy and choose one of these execution lanes explicitly:
319
+
320
+ ### Lane A — CLI-direct
321
+ Use for lightweight tasks where callback/lifecycle tracking is not the main requirement.
322
+
323
+ Rules:
324
+ - bind the repo explicitly with `--dir <absolute-repo-path>`
325
+ - prefer explicit `--model` if agent resolution is uncertain
326
+ - report clearly that the task used CLI-direct execution
327
+
328
+ ### Lane B — serve/plugin mode (canonical callback lane)
329
+ Use when callback correctness, observability, event-driven lifecycle handling, or multi-project-safe control plane is required.
330
+
331
+ Rules:
332
+ - bind execution to one project-bound serve instance only
333
+ - keep routing envelope fields explicit
334
+ - treat `/hooks/agent` as the primary callback path
335
+ - use OpenCode-side plugin callback for terminal lifecycle signaling
315
336
 
316
337
  ### Current implementation status (important)
317
- Bridge hiện đã chứng minh được các capability cốt lõi:
338
+ The bridge stack has already proven these capabilities:
318
339
  - routing envelope build
319
340
  - callback payload build
320
- - callback execution thật về `/hooks/agent`
321
- - status artifact persistence
322
- - mini listener runner / SSE probe ở mức PoC
323
- - runtime assumption: **1 project = 1 OpenCode serve instance**
324
-
325
- ### How to think about execution now
326
- - **Do not** assume one shared `opencode serve` is safe for many projects.
327
- - **Do** bind every coding task to:
328
- - `project_id`
329
- - `repo_root`
330
- - `opencode_server_url`
331
- - `task_id`
332
- - `run_id`
333
- - `agent_id`
334
- - `session_key`
335
- - **Do** treat `/hooks/agent` as callback primary.
336
- - **Do not** use `cron` or `group:sessions` as the callback mechanism.
341
+ - callback execution to `/hooks/agent`
342
+ - run-status artifact persistence
343
+ - serve binding fail-closed by `repo_root`
344
+ - OpenCode-side plugin callback path with `session.idle` as canonical trigger
345
+ - materialized OpenCode-side plugin artifact and install flow
337
346
 
338
347
  ### Practical guidance for agents
339
348
  When handing off into OpenCode execution:
340
349
  - mention the intended repo explicitly
341
350
  - ensure the execution packet is file-driven
342
- - ensure the repo binding uses `--dir <repo>` or an equivalent project-bound path
351
+ - ensure repo binding is explicit (`--dir <repo>` for CLI-direct, project-bound serve for serve/plugin mode)
343
352
  - prefer bridge-aware execution over free-form `opencode run` whenever the flow needs:
344
353
  - callback
345
354
  - task/run tracking
346
355
  - serve/session registry
347
356
  - multi-agent lane routing
357
+ - event-driven terminal signaling
348
358
 
349
359
  ### Mandatory reporting after execution handoff
350
360
  Outer agents should report:
@@ -584,25 +594,22 @@ Use when you need to resolve which OpenCode serve should be used for a given:
584
594
  #### `opencode_build_envelope`
585
595
  Use when you are about to delegate a concrete task into OpenCode lane and need the canonical routing envelope.
586
596
 
587
- #### `opencode_build_callback`
588
- Use when mapping a known OpenCode event into a callback payload for OpenClaw.
597
+ #### `opencode_execute_task`
598
+ Use as the standard serve/plugin-mode execution entrypoint:
599
+ - resolve/spawn project-bound serve
600
+ - create session
601
+ - send prompt async
602
+ - start watcher path
603
+ - persist run artifact
589
604
 
590
- #### `opencode_execute_callback`
591
- Use when executing the callback into `/hooks/agent` for a payload that has already been built.
592
-
593
- #### `opencode_probe_sse`
594
- Use when verifying that OpenCode serve is alive and emitting SSE events.
605
+ #### `opencode_run_status`
606
+ Use to inspect run state and event-derived lifecycle summary.
595
607
 
596
- #### `opencode_listen_once`
597
- Use for a small, single-shot proof that the bridge can:
598
- - read an event
599
- - normalize it
600
- - callback
601
- - write artifact
608
+ #### `opencode_run_events`
609
+ Use to inspect normalized SSE event output for a run/session.
602
610
 
603
- #### `opencode_listen_loop`
604
- Use for baseline runtime-ops experiments where repeated event consumption and lifecycle handling are needed.
605
- Treat it as baseline runtime manager logic, not a production-perfect daemon.
611
+ #### `opencode_session_tail`
612
+ Use to inspect session messages and diff/tail evidence when available.
606
613
 
607
614
  #### `opencode_run_status`
608
615
  Use to inspect the artifact state of a previously handled run.
@@ -642,21 +649,23 @@ Use to mark a serve as stopped and send shutdown for a project serve entry.
642
649
  When a task must enter OpenCode execution lane, use this order:
643
650
 
644
651
  1. Prepare/verify execution packet via the outer workflow (`using-superpowers` → `brainstorming` if needed → `writing-plans`)
645
- 2. Resolve project/server:
646
- - `opencode_resolve_project`
652
+ 2. Classify execution shape:
653
+ - CLI-direct
654
+ - serve/plugin mode
655
+ 3. If using **CLI-direct**:
656
+ - run with explicit `--dir <repo>`
657
+ - prefer explicit `--model` if needed
658
+ - report no callback expectation unless a separate tracking path is in place
659
+ 4. If using **serve/plugin mode**:
660
+ - resolve project/server via `opencode_resolve_project`
647
661
  - or spawn one via `opencode_serve_spawn`
648
- 3. Build routing envelope:
649
- - `opencode_build_envelope`
650
- 4. Verify serve/event path as needed:
651
- - `opencode_probe_sse`
652
- - `opencode_listen_once`
653
- 5. Build and/or execute callbacks:
654
- - `opencode_build_callback`
655
- - `opencode_execute_callback`
656
- - or `opencode_callback_from_event`
657
- 6. Check run artifact/status:
662
+ - build routing envelope via `opencode_build_envelope`
663
+ - keep callback expectation explicit through `/hooks/agent`
664
+ 5. For serve/plugin mode, inspect artifacts as needed:
658
665
  - `opencode_run_status`
659
- 7. Only then proceed to outer verification:
666
+ - `opencode_run_events`
667
+ - `opencode_session_tail`
668
+ 6. Only then proceed to outer verification:
660
669
  - `verification-before-completion`
661
670
 
662
671
  ### Reporting requirements
@@ -672,14 +681,15 @@ When handing work into OpenCode lane, the outer agent should report:
672
681
  #### Do
673
682
  - Do keep OpenCode execution project-bound.
674
683
  - Do keep callback/session routing explicit.
684
+ - Do choose execution lane explicitly: CLI-direct vs serve/plugin mode.
675
685
  - Do use `opencode-orchestration` when coordination or bridge semantics matter.
676
686
  - Do use `verification-before-completion` before claiming completion.
677
687
 
678
688
  #### Don’t
679
- - Don’t use ad-hoc `opencode run` when the work needs callback, lifecycle tracking, or registry-aware execution.
689
+ - Don’t use ad-hoc `opencode run` for callback/lifecycle-sensitive work without explicit lane selection.
680
690
  - Don’t assume a single serve is multi-project-safe.
681
691
  - Don’t assume bridge/runtime-manager features are production-perfect without verification.
682
- - Don’t over-claim that the bridge is fully autonomous when only PoC/baseline behavior has been verified.
692
+ - Don’t over-claim that the bridge is fully autonomous when only functionally verified/hardened behavior is available.
683
693
 
684
694
  ## Guardrails
685
695
 
@@ -0,0 +1,58 @@
1
+ export type BridgeSessionTagFields = {
2
+ runId: string;
3
+ taskId: string;
4
+ requested: string;
5
+ resolved: string;
6
+ callbackSession: string;
7
+ callbackSessionId?: string;
8
+ projectId?: string;
9
+ repoRoot?: string;
10
+ };
11
+
12
+ export type OpenCodePluginCallbackAuditRecord = {
13
+ phase?: string;
14
+ event_type?: string;
15
+ session_id?: string;
16
+ title?: string;
17
+ tags?: Record<string, string> | null;
18
+ dedupeKey?: string;
19
+ ok?: boolean;
20
+ status?: number;
21
+ reason?: string;
22
+ body?: string;
23
+ payload?: any;
24
+ raw?: any;
25
+ created_at: string;
26
+ };
27
+
28
+ export function buildTaggedSessionTitle(fields: BridgeSessionTagFields): string {
29
+ return [
30
+ `${fields.taskId}`,
31
+ `runId=${fields.runId}`,
32
+ `taskId=${fields.taskId}`,
33
+ `requested=${fields.requested}`,
34
+ `resolved=${fields.resolved}`,
35
+ `callbackSession=${fields.callbackSession}`,
36
+ ...(fields.callbackSessionId ? [`callbackSessionId=${fields.callbackSessionId}`] : []),
37
+ ...(fields.projectId ? [`projectId=${fields.projectId}`] : []),
38
+ ...(fields.repoRoot ? [`repoRoot=${fields.repoRoot}`] : []),
39
+ ].join(" ");
40
+ }
41
+
42
+ export function parseTaggedSessionTitle(title?: string) {
43
+ if (!title || !title.trim()) return null;
44
+ const tags: Record<string, string> = {};
45
+ for (const token of title.split(/\s+/)) {
46
+ const idx = token.indexOf("=");
47
+ if (idx <= 0) continue;
48
+ const key = token.slice(0, idx).trim();
49
+ const raw = token.slice(idx + 1).trim();
50
+ if (!key || !raw) continue;
51
+ tags[key] = raw;
52
+ }
53
+ return Object.keys(tags).length > 0 ? tags : null;
54
+ }
55
+
56
+ export function buildPluginCallbackDedupeKey(input: { sessionId?: string; runId?: string }) {
57
+ return `${input.sessionId || "no-session"}|${input.runId || "no-run"}`;
58
+ }
File without changes