@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.
- package/README.en.md +73 -94
- package/README.md +73 -94
- package/dist/opencode-plugin/openclaw-bridge-callback.d.ts +5 -0
- package/dist/opencode-plugin/openclaw-bridge-callback.js +179 -0
- package/dist/{chunk-LCJRXKI3.js → src/chunk-OVQ5X54C.js} +4 -3
- package/dist/src/chunk-TDVN5AFB.js +36 -0
- package/dist/{index.js → src/index.js} +480 -14
- package/dist/{observability.d.ts → src/observability.d.ts} +1 -0
- package/dist/{observability.js → src/observability.js} +1 -1
- package/dist/src/shared-contracts.d.ts +33 -0
- package/dist/src/shared-contracts.js +10 -0
- package/openclaw.plugin.json +2 -2
- package/opencode-plugin/README.md +25 -0
- package/opencode-plugin/openclaw-bridge-callback.ts +186 -0
- package/package.json +17 -9
- package/scripts/install-bridge.mjs +60 -0
- package/scripts/materialize-opencode-plugin.mjs +75 -0
- package/skills/opencode-orchestration/SKILL.md +76 -66
- package/src/shared-contracts.ts +58 -0
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
|
@@ -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.
|
|
4
|
-
"description": "OpenClaw ↔ OpenCode bridge plugin for routing, callback
|
|
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
|
|
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
|
-
##
|
|
283
|
+
## Execution strategy (current standard)
|
|
284
284
|
|
|
285
|
-
|
|
285
|
+
Use a **hybrid execution model**:
|
|
286
286
|
|
|
287
|
-
-
|
|
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
|
-
|
|
290
|
+
Rules:
|
|
290
291
|
|
|
291
|
-
-
|
|
292
|
-
-
|
|
293
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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` (
|
|
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
|
-
|
|
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
|
-
|
|
338
|
+
The bridge stack has already proven these capabilities:
|
|
318
339
|
- routing envelope build
|
|
319
340
|
- callback payload build
|
|
320
|
-
- callback execution
|
|
321
|
-
- status artifact persistence
|
|
322
|
-
-
|
|
323
|
-
-
|
|
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
|
|
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
|
-
#### `
|
|
588
|
-
Use
|
|
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
|
-
#### `
|
|
591
|
-
Use
|
|
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
|
-
#### `
|
|
597
|
-
Use
|
|
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
|
-
#### `
|
|
604
|
-
Use
|
|
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.
|
|
646
|
-
-
|
|
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
|
-
|
|
649
|
-
- `
|
|
650
|
-
|
|
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
|
-
|
|
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`
|
|
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
|
|
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
|