@mrc2204/opencode-bridge 0.1.1 → 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.
@@ -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:
@@ -500,6 +510,38 @@ Agents must describe these limits honestly and avoid over-claiming completion.
500
510
 
501
511
  ### Execution lane assumptions (MANDATORY)
502
512
 
513
+ ### Current-session callback integrity (MANDATORY)
514
+
515
+ Bridge-aware execution must preserve caller identity and callback destination explicitly.
516
+
517
+ Required fields to preserve whenever available:
518
+ - `requested_agent_id`
519
+ - `resolved_agent_id`
520
+ - `origin_session_key`
521
+ - `origin_session_id`
522
+ - `callback_target_session_key`
523
+ - `callback_target_session_id`
524
+
525
+ Operational rules:
526
+ - Callback must return to the **current caller session**, not an arbitrary latest/execution session.
527
+ - If bridge/runtime cannot resolve the requested execution agent cleanly, it must **fail fast** instead of silently falling back to a default agent.
528
+ - If callback target session information is missing or inconsistent, treat that as a routing integrity error, not a soft warning.
529
+ - Reporting/trace should always expose:
530
+ - requested agent
531
+ - resolved execution agent
532
+ - callback target session key/id
533
+ - whether fallback was used
534
+
535
+ ### Runtime hygiene after completion (MANDATORY)
536
+
537
+ When a task or epic is marked `done/closed`, any OpenCode serve/process that was started only for that execution must be shut down immediately unless there is an explicit reason to keep it alive.
538
+
539
+ Operational rules:
540
+ - Do not leave lingering OpenCode serves after work completes.
541
+ - Treat serve shutdown as part of completion hygiene, not optional cleanup.
542
+ - If a serve remains alive intentionally, the agent must report the reason explicitly.
543
+
544
+
503
545
  1. **One project = one OpenCode serve instance**
504
546
  - Do not assume one shared serve is safe for multiple repos.
505
547
  - Always bind execution to a single project/repo root.
@@ -552,25 +594,22 @@ Use when you need to resolve which OpenCode serve should be used for a given:
552
594
  #### `opencode_build_envelope`
553
595
  Use when you are about to delegate a concrete task into OpenCode lane and need the canonical routing envelope.
554
596
 
555
- #### `opencode_build_callback`
556
- 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
557
604
 
558
- #### `opencode_execute_callback`
559
- Use when executing the callback into `/hooks/agent` for a payload that has already been built.
560
-
561
- #### `opencode_probe_sse`
562
- 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.
563
607
 
564
- #### `opencode_listen_once`
565
- Use for a small, single-shot proof that the bridge can:
566
- - read an event
567
- - normalize it
568
- - callback
569
- - write artifact
608
+ #### `opencode_run_events`
609
+ Use to inspect normalized SSE event output for a run/session.
570
610
 
571
- #### `opencode_listen_loop`
572
- Use for baseline runtime-ops experiments where repeated event consumption and lifecycle handling are needed.
573
- 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.
574
613
 
575
614
  #### `opencode_run_status`
576
615
  Use to inspect the artifact state of a previously handled run.
@@ -610,21 +649,23 @@ Use to mark a serve as stopped and send shutdown for a project serve entry.
610
649
  When a task must enter OpenCode execution lane, use this order:
611
650
 
612
651
  1. Prepare/verify execution packet via the outer workflow (`using-superpowers` → `brainstorming` if needed → `writing-plans`)
613
- 2. Resolve project/server:
614
- - `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`
615
661
  - or spawn one via `opencode_serve_spawn`
616
- 3. Build routing envelope:
617
- - `opencode_build_envelope`
618
- 4. Verify serve/event path as needed:
619
- - `opencode_probe_sse`
620
- - `opencode_listen_once`
621
- 5. Build and/or execute callbacks:
622
- - `opencode_build_callback`
623
- - `opencode_execute_callback`
624
- - or `opencode_callback_from_event`
625
- 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:
626
665
  - `opencode_run_status`
627
- 7. Only then proceed to outer verification:
666
+ - `opencode_run_events`
667
+ - `opencode_session_tail`
668
+ 6. Only then proceed to outer verification:
628
669
  - `verification-before-completion`
629
670
 
630
671
  ### Reporting requirements
@@ -640,14 +681,15 @@ When handing work into OpenCode lane, the outer agent should report:
640
681
  #### Do
641
682
  - Do keep OpenCode execution project-bound.
642
683
  - Do keep callback/session routing explicit.
684
+ - Do choose execution lane explicitly: CLI-direct vs serve/plugin mode.
643
685
  - Do use `opencode-orchestration` when coordination or bridge semantics matter.
644
686
  - Do use `verification-before-completion` before claiming completion.
645
687
 
646
688
  #### Don’t
647
- - 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.
648
690
  - Don’t assume a single serve is multi-project-safe.
649
691
  - Don’t assume bridge/runtime-manager features are production-perfect without verification.
650
- - 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.
651
693
 
652
694
  ## Guardrails
653
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
+ }
@@ -1,176 +0,0 @@
1
- // src/observability.ts
2
- function parseSseFramesFromBuffer(input) {
3
- const normalized = input.replace(/\r\n/g, "\n");
4
- const parts = normalized.split("\n\n");
5
- const remainder = parts.pop() ?? "";
6
- const frames = [];
7
- for (const rawFrame of parts) {
8
- const lines = rawFrame.split("\n");
9
- let event;
10
- let id;
11
- let retry;
12
- const dataLines = [];
13
- for (const line of lines) {
14
- if (!line || line.startsWith(":")) continue;
15
- const idx = line.indexOf(":");
16
- const field = idx >= 0 ? line.slice(0, idx).trim() : line.trim();
17
- const value = idx >= 0 ? line.slice(idx + 1).replace(/^\s/, "") : "";
18
- if (field === "event") event = value;
19
- else if (field === "id") id = value;
20
- else if (field === "retry") {
21
- const n = Number(value);
22
- if (Number.isFinite(n)) retry = n;
23
- } else if (field === "data") dataLines.push(value);
24
- }
25
- if (dataLines.length <= 0) continue;
26
- frames.push({
27
- event,
28
- id,
29
- retry,
30
- data: dataLines.join("\n"),
31
- raw: rawFrame
32
- });
33
- }
34
- return { frames, remainder };
35
- }
36
- function parseSseData(data) {
37
- const trimmed = data.trim();
38
- if (!trimmed) return null;
39
- try {
40
- return JSON.parse(trimmed);
41
- } catch {
42
- return { raw: trimmed };
43
- }
44
- }
45
- function unwrapGlobalPayload(raw) {
46
- let current = raw;
47
- const wrappers = [];
48
- for (let i = 0; i < 3; i += 1) {
49
- if (!current || typeof current !== "object") break;
50
- if ("payload" in current && current.payload !== void 0) {
51
- wrappers.push("payload");
52
- current = current.payload;
53
- continue;
54
- }
55
- if ("data" in current && current.data !== void 0 && Object.keys(current).length <= 4) {
56
- wrappers.push("data");
57
- current = current.data;
58
- continue;
59
- }
60
- break;
61
- }
62
- return { payload: current, wrappers };
63
- }
64
- function pickFirstString(obj, keys) {
65
- if (!obj || typeof obj !== "object") return void 0;
66
- for (const key of keys) {
67
- const value = obj[key];
68
- if (typeof value === "string" && value.trim()) return value.trim();
69
- }
70
- return void 0;
71
- }
72
- function normalizeOpenCodeEvent(raw) {
73
- const text = JSON.stringify(raw || {}).toLowerCase();
74
- if (text.includes("permission") || text.includes("question.asked")) {
75
- return { kind: "permission.requested", summary: "OpenCode \u0111ang ch\u1EDD permission/user input", raw };
76
- }
77
- if (text.includes("error") || text.includes("failed")) {
78
- return { kind: "task.failed", summary: "OpenCode task failed", raw };
79
- }
80
- if (text.includes("stalled")) {
81
- return { kind: "task.stalled", summary: "OpenCode task stalled", raw };
82
- }
83
- if (text.includes("complete") || text.includes("completed") || text.includes("done")) {
84
- return { kind: "task.completed", summary: "OpenCode task completed", raw };
85
- }
86
- if (text.includes("progress") || text.includes("delta") || text.includes("assistant")) {
87
- return { kind: "task.progress", summary: "OpenCode task made progress", raw };
88
- }
89
- if (text.includes("start") || text.includes("created") || text.includes("connected")) {
90
- return { kind: "task.started", summary: "OpenCode task/server started", raw };
91
- }
92
- return { kind: null, raw };
93
- }
94
- function normalizeTypedEventV1(frame, scope) {
95
- const parsed = parseSseData(frame.data);
96
- const unwrapped = unwrapGlobalPayload(parsed);
97
- const normalized = normalizeOpenCodeEvent(unwrapped.payload);
98
- return {
99
- schema: "opencode.event.v1",
100
- scope,
101
- eventName: frame.event,
102
- eventId: frame.id,
103
- kind: normalized.kind,
104
- summary: normalized.summary,
105
- runId: pickFirstString(unwrapped.payload, ["run_id", "runId"]),
106
- taskId: pickFirstString(unwrapped.payload, ["task_id", "taskId"]),
107
- sessionId: pickFirstString(unwrapped.payload, ["session_id", "sessionId", "session"]),
108
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
109
- wrappers: unwrapped.wrappers,
110
- payload: unwrapped.payload
111
- };
112
- }
113
- function asString(value) {
114
- return typeof value === "string" && value.trim() ? value.trim() : void 0;
115
- }
116
- function scoreSessionCandidate(candidate, ctx) {
117
- const id = asString(candidate?.id) || "";
118
- const candidateText = JSON.stringify(candidate || {}).toLowerCase();
119
- const runId = ctx.runId?.toLowerCase();
120
- const taskId = ctx.taskId?.toLowerCase();
121
- const sessionKey = ctx.sessionKey?.toLowerCase();
122
- const artifactSessionId = ctx.artifactSessionId;
123
- let score = 0;
124
- if (artifactSessionId && id === artifactSessionId) score += 1e3;
125
- if (runId && candidateText.includes(runId)) score += 120;
126
- if (taskId && candidateText.includes(taskId)) score += 70;
127
- if (sessionKey && candidateText.includes(sessionKey)) score += 60;
128
- if (runId && id.toLowerCase().includes(runId)) score += 30;
129
- if (taskId && id.toLowerCase().includes(taskId)) score += 20;
130
- score += Math.max(0, 20 - ctx.recencyRank);
131
- return score;
132
- }
133
- function resolveSessionId(input) {
134
- if (input.explicitSessionId) {
135
- return { sessionId: input.explicitSessionId, strategy: "explicit", score: 9999 };
136
- }
137
- const list = Array.isArray(input.sessionList) ? input.sessionList : [];
138
- const withRecency = [...list].map((item, idx) => {
139
- const updated = Number(item?.time?.updated || 0);
140
- return { item, idx, updated };
141
- }).sort((a, b) => b.updated - a.updated).map((x, rank) => ({ ...x, rank }));
142
- if (input.artifactSessionId) {
143
- const direct = withRecency.find((x) => asString(x.item?.id) === input.artifactSessionId);
144
- if (direct) {
145
- return { sessionId: input.artifactSessionId, strategy: "artifact", score: 1e3 };
146
- }
147
- }
148
- let best = { score: -1 };
149
- for (const candidate of withRecency) {
150
- const id = asString(candidate.item?.id);
151
- if (!id) continue;
152
- const score = scoreSessionCandidate(candidate.item, {
153
- runId: input.runId,
154
- taskId: input.taskId,
155
- sessionKey: input.sessionKey,
156
- artifactSessionId: input.artifactSessionId,
157
- recencyRank: candidate.rank
158
- });
159
- if (score > best.score) best = { id, score };
160
- }
161
- if (best.id && best.score > 0) {
162
- return { sessionId: best.id, strategy: "scored_fallback", score: best.score };
163
- }
164
- const latest = withRecency.find((x) => asString(x.item?.id));
165
- if (latest) return { sessionId: asString(latest.item?.id), strategy: "latest", score: 0 };
166
- return { strategy: "none", score: -1 };
167
- }
168
-
169
- export {
170
- parseSseFramesFromBuffer,
171
- parseSseData,
172
- unwrapGlobalPayload,
173
- normalizeOpenCodeEvent,
174
- normalizeTypedEventV1,
175
- resolveSessionId
176
- };
@@ -1,52 +0,0 @@
1
- type OpenCodeEventKind = "task.started" | "task.progress" | "permission.requested" | "task.stalled" | "task.failed" | "task.completed";
2
- type EventScope = "session" | "global";
3
- type SseFrame = {
4
- event?: string;
5
- id?: string;
6
- retry?: number;
7
- data: string;
8
- raw: string;
9
- };
10
- type TypedEventV1 = {
11
- schema: "opencode.event.v1";
12
- scope: EventScope;
13
- eventName?: string;
14
- eventId?: string;
15
- kind: OpenCodeEventKind | null;
16
- summary?: string;
17
- runId?: string;
18
- taskId?: string;
19
- sessionId?: string;
20
- timestamp: string;
21
- wrappers: string[];
22
- payload: any;
23
- };
24
- declare function parseSseFramesFromBuffer(input: string): {
25
- frames: SseFrame[];
26
- remainder: string;
27
- };
28
- declare function parseSseData(data: string): any;
29
- declare function unwrapGlobalPayload(raw: any): {
30
- payload: any;
31
- wrappers: string[];
32
- };
33
- declare function normalizeOpenCodeEvent(raw: any): {
34
- kind: OpenCodeEventKind | null;
35
- summary?: string;
36
- raw: any;
37
- };
38
- declare function normalizeTypedEventV1(frame: SseFrame, scope: EventScope): TypedEventV1;
39
- declare function resolveSessionId(input: {
40
- explicitSessionId?: string;
41
- runId?: string;
42
- taskId?: string;
43
- sessionKey?: string;
44
- artifactSessionId?: string;
45
- sessionList?: any[];
46
- }): {
47
- sessionId?: string;
48
- strategy: "explicit" | "artifact" | "scored_fallback" | "latest" | "none";
49
- score?: number;
50
- };
51
-
52
- export { type EventScope, type OpenCodeEventKind, type SseFrame, type TypedEventV1, normalizeOpenCodeEvent, normalizeTypedEventV1, parseSseData, parseSseFramesFromBuffer, resolveSessionId, unwrapGlobalPayload };
File without changes