cool-workflow 0.1.86 → 0.1.87

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.
Files changed (54) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +8 -1
  4. package/apps/architecture-review/app.json +1 -1
  5. package/apps/architecture-review-fast/app.json +1 -1
  6. package/apps/end-to-end-golden-path/app.json +1 -1
  7. package/apps/pr-review-fix-ci/app.json +1 -1
  8. package/apps/release-cut/app.json +1 -1
  9. package/apps/research-synthesis/app.json +1 -1
  10. package/dist/capability-registry.js +24 -0
  11. package/dist/cli/command-surface.js +80 -10
  12. package/dist/cli.js +2 -1
  13. package/dist/doctor.js +13 -4
  14. package/dist/execution-backend.js +8 -0
  15. package/dist/operator-ux/format.js +21 -15
  16. package/dist/operator-ux.js +2 -1
  17. package/dist/orchestrator.js +172 -74
  18. package/dist/term.js +93 -0
  19. package/dist/version.js +1 -1
  20. package/docs/agent-delegation-drive.7.md +24 -6
  21. package/docs/cli-mcp-parity.7.md +12 -2
  22. package/docs/contract-migration-tooling.7.md +4 -0
  23. package/docs/control-plane-scheduling.7.md +4 -0
  24. package/docs/durable-state-and-locking.7.md +4 -0
  25. package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
  26. package/docs/execution-backends.7.md +4 -0
  27. package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
  28. package/docs/multi-agent-eval-replay-harness.7.md +4 -0
  29. package/docs/multi-agent-operator-ux.7.md +4 -0
  30. package/docs/node-snapshot-diff-replay.7.md +4 -0
  31. package/docs/observability-cost-accounting.7.md +4 -0
  32. package/docs/project-index.md +9 -3
  33. package/docs/real-execution-backends.7.md +4 -0
  34. package/docs/release-and-migration.7.md +4 -0
  35. package/docs/release-history.md +10 -0
  36. package/docs/release-tooling.7.md +12 -0
  37. package/docs/run-registry-control-plane.7.md +4 -0
  38. package/docs/run-retention-reclamation.7.md +4 -0
  39. package/docs/state-explosion-management.7.md +4 -0
  40. package/docs/team-collaboration.7.md +4 -0
  41. package/docs/web-desktop-workbench.7.md +4 -0
  42. package/manifest/plugin.manifest.json +1 -1
  43. package/package.json +1 -1
  44. package/scripts/agents/agent-adapter-core.js +180 -0
  45. package/scripts/agents/builtin-templates.json +4 -1
  46. package/scripts/agents/codex-agent.js +134 -0
  47. package/scripts/agents/gemini-agent.js +115 -0
  48. package/scripts/agents/opencode-agent.js +119 -0
  49. package/scripts/canonical-apps.js +4 -4
  50. package/scripts/dogfood-release.js +1 -1
  51. package/scripts/golden-path.js +4 -4
  52. package/scripts/parity-check.js +6 -5
  53. package/scripts/release-check.js +3 -3
  54. package/scripts/release-gate.sh +1 -1
@@ -205,6 +205,14 @@ an ungated tag gives a plain caveat instead and warns on stderr — the notes ne
205
205
  claim a review that is not there. Test seam: `CW_RELEASE_FLOW_GH_CMD` puts a stub in place of the `gh`
206
206
  binary (spawned `shell:false`) so the smoke runs it offline.
207
207
 
208
+ ### npm publishing trust
209
+
210
+ The `npm-publish` GitHub Action uses npm Trusted Publishing. GitHub gives the
211
+ job a short OIDC token, npm checks that the request is from this repo and
212
+ workflow, and npm adds provenance for the package. The workflow does not use a
213
+ long-lived `NPM_TOKEN`, so no publish token with two-factor bypass is kept in
214
+ GitHub secrets.
215
+
208
216
  0.1.51
209
217
 
210
218
  0.1.76
@@ -231,3 +239,7 @@ Loaders fail closed on corrupt state; store writes are made safe under more than
231
239
  0.1.85
232
240
 
233
241
  0.1.86
242
+
243
+ ## 0.1.87 (v0.1.87)
244
+
245
+ npm test parallel, 4-vendor wrappers (Claude/Codex/Gemini/OpenCode), Homebrew-style CLI UX (colors/did-you-mean/categorized help/error tips/cw info/cw search/cw man/doctor --fix), post-success summaries, agent execution timing
@@ -412,3 +412,7 @@ No other change to this page in v0.1.84.
412
412
  0.1.85
413
413
 
414
414
  0.1.86
415
+
416
+ ## 0.1.87 (v0.1.87)
417
+
418
+ npm test parallel, 4-vendor wrappers (Claude/Codex/Gemini/OpenCode), Homebrew-style CLI UX (colors/did-you-mean/categorized help/error tips/cw info/cw search/cw man/doctor --fix), post-success summaries, agent execution timing
@@ -212,3 +212,7 @@ No other change to this page in v0.1.84.
212
212
  0.1.85
213
213
 
214
214
  0.1.86
215
+
216
+ ## 0.1.87 (v0.1.87)
217
+
218
+ npm test parallel, 4-vendor wrappers (Claude/Codex/Gemini/OpenCode), Homebrew-style CLI UX (colors/did-you-mean/categorized help/error tips/cw info/cw search/cw man/doctor --fix), post-success summaries, agent execution timing
@@ -290,3 +290,7 @@ No other change to this page in v0.1.84.
290
290
  0.1.85
291
291
 
292
292
  0.1.86
293
+
294
+ ## 0.1.87 (v0.1.87)
295
+
296
+ npm test parallel, 4-vendor wrappers (Claude/Codex/Gemini/OpenCode), Homebrew-style CLI UX (colors/did-you-mean/categorized help/error tips/cw info/cw search/cw man/doctor --fix), post-success summaries, agent execution timing
@@ -226,3 +226,7 @@ No other change to this page in v0.1.84.
226
226
  0.1.85
227
227
 
228
228
  0.1.86
229
+
230
+ ## 0.1.87 (v0.1.87)
231
+
232
+ npm test parallel, 4-vendor wrappers (Claude/Codex/Gemini/OpenCode), Homebrew-style CLI UX (colors/did-you-mean/categorized help/error tips/cw info/cw search/cw man/doctor --fix), post-success summaries, agent execution timing
@@ -234,3 +234,7 @@ No other change to this page in v0.1.84.
234
234
  0.1.85
235
235
 
236
236
  0.1.86
237
+
238
+ ## 0.1.87 (v0.1.87)
239
+
240
+ npm test parallel, 4-vendor wrappers (Claude/Codex/Gemini/OpenCode), Homebrew-style CLI UX (colors/did-you-mean/categorized help/error tips/cw info/cw search/cw man/doctor --fix), post-success summaries, agent execution timing
@@ -2,7 +2,7 @@
2
2
  "_comment": "SINGLE SOURCE OF TRUTH for every vendor manifest. Edit THIS file, then run `npm run gen:manifests`. Do NOT hand-edit the generated vendor manifests (.claude-plugin/, .codex-plugin/, .agents/, .mcp.json) — `npm run gen:manifests -- --check` (run by release:check) will fail if they drift from this source.",
3
3
  "identity": {
4
4
  "name": "cool-workflow",
5
- "version": "0.1.86",
5
+ "version": "0.1.87",
6
6
  "license": "BSD-2-Clause",
7
7
  "homepage": "https://github.com/coo1white/cool-workflow",
8
8
  "author": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cool-workflow",
3
- "version": "0.1.86",
3
+ "version": "0.1.87",
4
4
  "bin": {
5
5
  "cool-workflow": "scripts/cw.js",
6
6
  "cw": "scripts/cw.js"
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("node:fs");
5
+
6
+ const RESULT_CONTRACT = `
7
+ === HOW TO RETURN YOUR ANSWER (overrides any 'write to result.md' instruction above) ===
8
+ You have NO file-write access. Do NOT attempt to write, create, or edit any file -
9
+ result.md is persisted FOR YOU from your final message, so writing it yourself is
10
+ neither needed nor possible. Use ONLY read-only tools (read files, grep, list).
11
+ Respond with ONLY your FINAL answer as Markdown, and it MUST END WITH a fenced
12
+ cw:result block that EXACTLY follows this schema:
13
+
14
+ \`\`\`cw:result
15
+ {
16
+ "summary": "one-paragraph direct answer",
17
+ "findings": [
18
+ {
19
+ "id": "unique-kebab-id",
20
+ "title": "short risk title",
21
+ "severity": "P0",
22
+ "classification": "real",
23
+ "evidence": ["path/to/file.ts:42"]
24
+ }
25
+ ],
26
+ "evidence": ["path/to/file.ts:42", "path/to/other.ts:10"]
27
+ }
28
+ \`\`\`
29
+
30
+ HARD RULES (the result is REJECTED otherwise):
31
+ - Every object in "findings" MUST have a unique "id" (non-empty string).
32
+ - "classification", if present, MUST be one of: real, conditional, non-issue, unknown.
33
+ - Any finding with "severity" P0, P1, or P2 MUST include a NON-EMPTY "evidence" array.
34
+ - The top-level "evidence" array MUST be NON-EMPTY with REAL file:line locators from this repo.
35
+ - If you have no structured findings, use "findings": [] (empty) - never omit a finding's id.`;
36
+
37
+ function buildPrompt(inputPath) {
38
+ return `${fs.readFileSync(inputPath, "utf8")}\n${RESULT_CONTRACT}`;
39
+ }
40
+
41
+ function streamEnabled(env = process.env) {
42
+ return env.CW_AGENT_STREAM === "1" && env.CW_NO_STREAM !== "1";
43
+ }
44
+
45
+ function traceEnabled(env = process.env, stderr = process.stderr) {
46
+ return streamEnabled(env) && Boolean(stderr.isTTY);
47
+ }
48
+
49
+ function trace(line, env = process.env, stderr = process.stderr) {
50
+ if (!traceEnabled(env, stderr)) return;
51
+ stderr.write(`${line}\n`);
52
+ }
53
+
54
+ function shortText(value, max = 100) {
55
+ const text = String(value || "").replace(/\s+/g, " ").trim();
56
+ if (!text) return "";
57
+ return text.length > max ? `${text.slice(0, max - 3)}...` : text;
58
+ }
59
+
60
+ function maybeObject(value) {
61
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
62
+ }
63
+
64
+ function firstString(...values) {
65
+ for (const value of values) if (typeof value === "string" && value.trim()) return value.trim();
66
+ return undefined;
67
+ }
68
+
69
+ function firstObject(...values) {
70
+ for (const value of values) {
71
+ const obj = maybeObject(value);
72
+ if (obj) return obj;
73
+ }
74
+ return undefined;
75
+ }
76
+
77
+ function modelFromEvent(ev) {
78
+ const msg = maybeObject(ev.message);
79
+ return firstString(ev.model, ev.model_id, ev.modelId, msg && msg.model, ev.provider_model);
80
+ }
81
+
82
+ function usageFromEvent(ev) {
83
+ const msg = maybeObject(ev.message);
84
+ return firstObject(ev.usage, ev.token_usage, ev.tokenUsage, msg && msg.usage);
85
+ }
86
+
87
+ function toolNameFromEvent(ev) {
88
+ const item = maybeObject(ev.item);
89
+ const call = maybeObject(ev.tool_call || ev.toolCall);
90
+ return firstString(ev.name, ev.tool, ev.tool_name, ev.toolName, item && item.name, call && call.name);
91
+ }
92
+
93
+ function toolArgFromEvent(ev) {
94
+ const input = maybeObject(ev.input) || maybeObject(ev.arguments) || maybeObject(ev.args) || maybeObject(ev.item && ev.item.input);
95
+ if (!input) return "";
96
+ return firstString(input.file_path, input.path, input.pattern, input.command, input.query, input.url, input.cmd) || "";
97
+ }
98
+
99
+ function textFromEvent(ev) {
100
+ const delta = maybeObject(ev.delta);
101
+ const msg = maybeObject(ev.message);
102
+ return firstString(ev.text, ev.delta, ev.content, ev.output, delta && delta.text, msg && msg.content);
103
+ }
104
+
105
+ function renderJsonEvent(provider, ev, state) {
106
+ if (!ev || typeof ev !== "object") return;
107
+ const model = modelFromEvent(ev);
108
+ if (model && !state.model) state.model = model;
109
+ const usage = usageFromEvent(ev);
110
+ if (usage) state.usage = usage;
111
+
112
+ const type = String(ev.type || ev.event || ev.kind || "");
113
+ const toolName = toolNameFromEvent(ev);
114
+ if (toolName || /tool|command/i.test(type)) {
115
+ const arg = shortText(toolArgFromEvent(ev), 80);
116
+ trace(` -> ${toolName || type}${arg ? ` ${arg}` : ""}`);
117
+ return;
118
+ }
119
+
120
+ const text = textFromEvent(ev);
121
+ if (text && /assistant|message|delta|text|response|output/i.test(type || "text")) {
122
+ trace(` ${shortText(text, 240)}`);
123
+ } else if (/turn|step|summary|status/i.test(type)) {
124
+ const status = firstString(ev.status, ev.status_detail, ev.summary, ev.message);
125
+ if (status) trace(` . ${provider}: ${shortText(status, 160)}`);
126
+ }
127
+ }
128
+
129
+ function parseJsonLines(provider, chunk, state, onLine) {
130
+ state.buffer = `${state.buffer || ""}${chunk}`;
131
+ let nl;
132
+ while ((nl = state.buffer.indexOf("\n")) >= 0) {
133
+ const line = state.buffer.slice(0, nl).trim();
134
+ state.buffer = state.buffer.slice(nl + 1);
135
+ if (!line) continue;
136
+ if (onLine) onLine(line);
137
+ let ev;
138
+ try {
139
+ ev = JSON.parse(line);
140
+ } catch {
141
+ continue;
142
+ }
143
+ renderJsonEvent(provider, ev, state);
144
+ }
145
+ }
146
+
147
+ function flushJsonLines(provider, state, onLine) {
148
+ const line = String(state.buffer || "").trim();
149
+ state.buffer = "";
150
+ if (!line) return;
151
+ if (onLine) onLine(line);
152
+ try {
153
+ renderJsonEvent(provider, JSON.parse(line), state);
154
+ } catch {
155
+ /* ignore incomplete/non-JSON tail */
156
+ }
157
+ }
158
+
159
+ function writeResult(resultPath, resultText) {
160
+ if (typeof resultText !== "string" || !resultText.trim()) {
161
+ throw new Error("agent produced no final result");
162
+ }
163
+ fs.writeFileSync(resultPath, resultText, "utf8");
164
+ }
165
+
166
+ function emitReport(model, usage, resultText) {
167
+ process.stdout.write(JSON.stringify({ model, usage, result: resultText }));
168
+ }
169
+
170
+ module.exports = {
171
+ RESULT_CONTRACT,
172
+ buildPrompt,
173
+ streamEnabled,
174
+ traceEnabled,
175
+ trace,
176
+ parseJsonLines,
177
+ flushJsonLines,
178
+ writeResult,
179
+ emitReport
180
+ };
@@ -2,6 +2,9 @@
2
2
  "schemaVersion": 1,
3
3
  "comment": "Builtin agent-delegation templates as DATA, not a hand-edited kernel TS literal (FreeBSD-audit L15). Each entry maps a vendor name to the wrapper script in THIS directory that `builtin:<name>` (or CW_AGENT_COMMAND=builtin:<name>) resolves to. Adding a vendor is a content/distribution step: drop a wrapper script here + add a line below — NO kernel edit. Still pure config: the wrapper is an out-of-process delegation script; CW never imports or calls a model API.",
4
4
  "templates": {
5
- "claude": "claude-p-agent.js"
5
+ "claude": "claude-p-agent.js",
6
+ "codex": "codex-agent.js",
7
+ "gemini": "gemini-agent.js",
8
+ "opencode": "opencode-agent.js"
6
9
  }
7
10
  }
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // codex-agent.js - Codex CLI adapter for CW Agent Delegation Drive.
5
+ //
6
+ // This is a CONFIG wrapper, not a CW runtime dependency. CW spawns this script
7
+ // out-of-process; this script spawns `codex exec` out-of-process. Vendor JSONL
8
+ // parsing stays here in userland policy. CW core only captures stdout and, when
9
+ // opted in, forwards stderr.
10
+ //
11
+ // Contract:
12
+ // argv[2] = {{input}} worker input.md
13
+ // argv[3] = {{result}} worker result.md to persist
14
+ //
15
+ // stdout: one JSON object { model, usage, result } for CW provenance.
16
+ // stderr: optional live trace when CW_AGENT_STREAM=1 and attached to a TTY.
17
+
18
+ const fs = require("node:fs");
19
+ const path = require("node:path");
20
+ const { spawn } = require("node:child_process");
21
+ const {
22
+ buildPrompt,
23
+ emitReport,
24
+ flushJsonLines,
25
+ parseJsonLines,
26
+ trace,
27
+ writeResult
28
+ } = require("./agent-adapter-core");
29
+
30
+ const inputPath = process.argv[2];
31
+ const resultPath = process.argv[3];
32
+ if (!inputPath || !resultPath) {
33
+ process.stderr.write("usage: codex-agent.js <inputPath> <resultPath> (CW substitutes {{input}} {{result}})\n");
34
+ process.exit(2);
35
+ }
36
+
37
+ const finalPath = path.join(path.dirname(resultPath), `.codex-last-message-${process.pid}.md`);
38
+ try {
39
+ fs.rmSync(finalPath, { force: true });
40
+ } catch {
41
+ /* best effort */
42
+ }
43
+
44
+ const prompt = buildPrompt(inputPath);
45
+ const state = { provider: "codex", buffer: "", model: undefined, usage: undefined };
46
+ const capturedStdout = [];
47
+ let childStderr = "";
48
+ function recordJsonLine(line) {
49
+ capturedStdout.push(line);
50
+ try {
51
+ JSON.parse(line);
52
+ } catch {
53
+ state.invalidJson = true;
54
+ }
55
+ }
56
+
57
+ trace("* codex: reading the repo (read-only)...");
58
+
59
+ const args = [
60
+ "exec",
61
+ "--json",
62
+ "--output-last-message",
63
+ finalPath,
64
+ "--sandbox",
65
+ "read-only",
66
+ "--ask-for-approval",
67
+ "never",
68
+ "--color",
69
+ "never",
70
+ "-"
71
+ ];
72
+
73
+ const child = spawn("codex", args, {
74
+ stdio: ["pipe", "pipe", "pipe"],
75
+ shell: false
76
+ });
77
+
78
+ child.stdin.setDefaultEncoding("utf8");
79
+ child.stdin.end(prompt);
80
+
81
+ child.stdout.setEncoding("utf8");
82
+ child.stdout.on("data", (chunk) => {
83
+ parseJsonLines("codex", chunk, state, recordJsonLine);
84
+ });
85
+
86
+ child.stderr.setEncoding("utf8");
87
+ child.stderr.on("data", (chunk) => {
88
+ childStderr += chunk;
89
+ if (process.env.CW_AGENT_STREAM === "1" && process.env.CW_NO_STREAM !== "1" && process.stderr.isTTY) {
90
+ process.stderr.write(chunk);
91
+ }
92
+ });
93
+
94
+ child.on("error", (error) => {
95
+ process.stderr.write(`codex spawn failed: ${error.message}\n`);
96
+ process.exit(1);
97
+ });
98
+
99
+ child.on("close", (code) => {
100
+ flushJsonLines("codex", state, recordJsonLine);
101
+ if (code !== 0) {
102
+ const detail = childStderr.trim() || `codex exited ${code === null ? "(timeout/killed)" : code}`;
103
+ process.stderr.write(`${detail}\n`);
104
+ process.exit(code === null ? 1 : code);
105
+ }
106
+ if (state.invalidJson) {
107
+ process.stderr.write("codex --json produced a non-JSONL stdout line - refusing to trust the result\n");
108
+ process.exit(1);
109
+ }
110
+
111
+ let resultText = "";
112
+ try {
113
+ resultText = fs.readFileSync(finalPath, "utf8");
114
+ } catch {
115
+ process.stderr.write("codex produced no final output file - refusing to fabricate a result\n");
116
+ process.exit(1);
117
+ } finally {
118
+ try {
119
+ fs.rmSync(finalPath, { force: true });
120
+ } catch {
121
+ /* best effort */
122
+ }
123
+ }
124
+
125
+ try {
126
+ writeResult(resultPath, resultText);
127
+ } catch (error) {
128
+ process.stderr.write(`codex produced no final result: ${error.message}\n`);
129
+ process.exit(1);
130
+ }
131
+
132
+ trace("* done - result captured");
133
+ emitReport(state.model, state.usage, resultText);
134
+ });
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // gemini-agent.js - Gemini CLI adapter for CW Agent Delegation Drive.
5
+ //
6
+ // This is a CONFIG wrapper, not a CW runtime dependency. CW spawns this script
7
+ // out-of-process; this script spawns `gemini -p` out-of-process. Vendor NDJSON
8
+ // parsing stays here in userland policy.
9
+ //
10
+ // Contract:
11
+ // argv[2] = {{input}} worker input.md
12
+ // argv[3] = {{result}} worker result.md to persist
13
+ //
14
+ // stdout: one JSON object { model, usage, result } for CW provenance.
15
+ // stderr: optional live trace when CW_AGENT_STREAM=1 and attached to a TTY.
16
+
17
+ const { spawn } = require("node:child_process");
18
+ const {
19
+ buildPrompt,
20
+ emitReport,
21
+ flushJsonLines,
22
+ parseJsonLines,
23
+ trace,
24
+ writeResult
25
+ } = require("./agent-adapter-core");
26
+
27
+ const inputPath = process.argv[2];
28
+ const resultPath = process.argv[3];
29
+ if (!inputPath || !resultPath) {
30
+ process.stderr.write("usage: gemini-agent.js <inputPath> <resultPath> (CW substitutes {{input}} {{result}})\n");
31
+ process.exit(2);
32
+ }
33
+
34
+ const prompt = buildPrompt(inputPath);
35
+ const state = { provider: "gemini", buffer: "", model: undefined, usage: undefined, textFragments: [], finalResult: undefined };
36
+ let childStderr = "";
37
+ function recordJsonLine(line) {
38
+ let ev;
39
+ try {
40
+ ev = JSON.parse(line);
41
+ } catch {
42
+ state.invalidJson = true;
43
+ return;
44
+ }
45
+ // Prefer the final result event text; delta events are incremental fallback.
46
+ if (ev.result && typeof ev.result === "string") {
47
+ state.finalResult = ev.result;
48
+ } else {
49
+ const text = typeof ev.text === "string" ? ev.text : (ev.delta ? (typeof ev.delta === "string" ? ev.delta : ev.delta.text) : undefined);
50
+ if (typeof text === "string" && text.trim()) state.textFragments.push(text);
51
+ }
52
+ }
53
+
54
+ trace("* gemini: reading the repo (read-only)...");
55
+
56
+ const args = [
57
+ "-p",
58
+ prompt,
59
+ "--output-format",
60
+ "stream-json",
61
+ "--approval-mode",
62
+ "plan"
63
+ ];
64
+
65
+ const child = spawn("gemini", args, {
66
+ stdio: ["ignore", "pipe", "pipe"],
67
+ shell: false
68
+ });
69
+
70
+ child.stdout.setEncoding("utf8");
71
+ child.stdout.on("data", (chunk) => {
72
+ parseJsonLines("gemini", chunk, state, recordJsonLine);
73
+ });
74
+
75
+ child.stderr.setEncoding("utf8");
76
+ child.stderr.on("data", (chunk) => {
77
+ childStderr += chunk;
78
+ if (process.env.CW_AGENT_STREAM === "1" && process.env.CW_NO_STREAM !== "1" && process.stderr.isTTY) {
79
+ process.stderr.write(chunk);
80
+ }
81
+ });
82
+
83
+ child.on("error", (error) => {
84
+ process.stderr.write(`gemini spawn failed: ${error.message}\n`);
85
+ process.exit(1);
86
+ });
87
+
88
+ child.on("close", (code) => {
89
+ flushJsonLines("gemini", state, recordJsonLine);
90
+ if (code !== 0) {
91
+ const detail = childStderr.trim() || `gemini exited ${code === null ? "(timeout/killed)" : code}`;
92
+ process.stderr.write(`${detail}\n`);
93
+ process.exit(code === null ? 1 : code);
94
+ }
95
+ if (state.invalidJson) {
96
+ process.stderr.write("gemini --output-format stream-json produced a non-JSONL stdout line - refusing to trust the result\n");
97
+ process.exit(1);
98
+ }
99
+
100
+ const resultText = state.finalResult || state.textFragments.join("\n\n");
101
+ if (!resultText.trim()) {
102
+ process.stderr.write("gemini produced no result text - refusing to fabricate a result\n");
103
+ process.exit(1);
104
+ }
105
+
106
+ try {
107
+ writeResult(resultPath, resultText);
108
+ } catch (error) {
109
+ process.stderr.write(`gemini produced no final result: ${error.message}\n`);
110
+ process.exit(1);
111
+ }
112
+
113
+ trace("* done - result captured");
114
+ emitReport(state.model, state.usage, resultText);
115
+ });
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // opencode-agent.js - OpenCode CLI adapter for CW Agent Delegation Drive.
5
+ //
6
+ // This is a CONFIG wrapper, not a CW runtime dependency. CW spawns this script
7
+ // out-of-process; this script spawns `opencode run` out-of-process. Vendor JSONL
8
+ // parsing stays here in userland policy.
9
+ //
10
+ // Contract:
11
+ // argv[2] = {{input}} worker input.md
12
+ // argv[3] = {{result}} worker result.md to persist
13
+ //
14
+ // stdout: one JSON object { model, usage, result } for CW provenance.
15
+ // stderr: optional live trace when CW_AGENT_STREAM=1 and attached to a TTY.
16
+ //
17
+ // NOTE: --dangerously-skip-permissions is used because OpenCode lacks a native
18
+ // --read-only or --allowed-tools flag. CW's sandbox layer enforces write safety
19
+ // via execution-backend boundary controls. If OpenCode adds a cleaner read-only
20
+ // flag, prefer that here.
21
+
22
+ const { spawn } = require("node:child_process");
23
+ const {
24
+ buildPrompt,
25
+ emitReport,
26
+ flushJsonLines,
27
+ parseJsonLines,
28
+ trace,
29
+ writeResult
30
+ } = require("./agent-adapter-core");
31
+
32
+ const inputPath = process.argv[2];
33
+ const resultPath = process.argv[3];
34
+ if (!inputPath || !resultPath) {
35
+ process.stderr.write("usage: opencode-agent.js <inputPath> <resultPath> (CW substitutes {{input}} {{result}})\n");
36
+ process.exit(2);
37
+ }
38
+
39
+ const prompt = buildPrompt(inputPath);
40
+ const state = { provider: "opencode", buffer: "", model: undefined, usage: undefined, textFragments: [], finalResult: undefined };
41
+ let childStderr = "";
42
+ function recordJsonLine(line) {
43
+ let ev;
44
+ try {
45
+ ev = JSON.parse(line);
46
+ } catch {
47
+ state.invalidJson = true;
48
+ return;
49
+ }
50
+ if (ev.result && typeof ev.result === "string") {
51
+ state.finalResult = ev.result;
52
+ } else {
53
+ const text = typeof ev.text === "string" ? ev.text : (ev.delta ? (typeof ev.delta === "string" ? ev.delta : ev.delta.text) : undefined);
54
+ if (typeof text === "string" && text.trim()) state.textFragments.push(text);
55
+ }
56
+ }
57
+
58
+ trace("* opencode: reading the repo...");
59
+
60
+ const args = [
61
+ "run",
62
+ "--format",
63
+ "json",
64
+ "--dangerously-skip-permissions",
65
+ "--prompt",
66
+ prompt
67
+ ];
68
+
69
+ const child = spawn("opencode", args, {
70
+ stdio: ["ignore", "pipe", "pipe"],
71
+ shell: false
72
+ });
73
+
74
+ child.stdout.setEncoding("utf8");
75
+ child.stdout.on("data", (chunk) => {
76
+ parseJsonLines("opencode", chunk, state, recordJsonLine);
77
+ });
78
+
79
+ child.stderr.setEncoding("utf8");
80
+ child.stderr.on("data", (chunk) => {
81
+ childStderr += chunk;
82
+ if (process.env.CW_AGENT_STREAM === "1" && process.env.CW_NO_STREAM !== "1" && process.stderr.isTTY) {
83
+ process.stderr.write(chunk);
84
+ }
85
+ });
86
+
87
+ child.on("error", (error) => {
88
+ process.stderr.write(`opencode spawn failed: ${error.message}\n`);
89
+ process.exit(1);
90
+ });
91
+
92
+ child.on("close", (code) => {
93
+ flushJsonLines("opencode", state, recordJsonLine);
94
+ if (code !== 0) {
95
+ const detail = childStderr.trim() || `opencode exited ${code === null ? "(timeout/killed)" : code}`;
96
+ process.stderr.write(`${detail}\n`);
97
+ process.exit(code === null ? 1 : code);
98
+ }
99
+ if (state.invalidJson) {
100
+ process.stderr.write("opencode --format json produced a non-JSONL stdout line - refusing to trust the result\n");
101
+ process.exit(1);
102
+ }
103
+
104
+ const resultText = state.finalResult || state.textFragments.join("\n\n");
105
+ if (!resultText.trim()) {
106
+ process.stderr.write("opencode produced no result text - refusing to fabricate a result\n");
107
+ process.exit(1);
108
+ }
109
+
110
+ try {
111
+ writeResult(resultPath, resultText);
112
+ } catch (error) {
113
+ process.stderr.write(`opencode produced no final result: ${error.message}\n`);
114
+ process.exit(1);
115
+ }
116
+
117
+ trace("* done - result captured");
118
+ emitReport(state.model, state.usage, resultText);
119
+ });
@@ -83,7 +83,7 @@ const canonicalApps = [
83
83
  "--source",
84
84
  "plugins/cool-workflow/docs/workflow-app-framework.7.md",
85
85
  "--scope",
86
- "Cool Workflow v0.1.86",
86
+ "Cool Workflow v0.1.87",
87
87
  "--freshness",
88
88
  "as of release preparation"
89
89
  ]
@@ -117,14 +117,14 @@ function main() {
117
117
  assert.ok(summary, `${app.id} must appear in app list`);
118
118
  assert.equal(summary.sourceKind, "app-directory");
119
119
  assert.equal(summary.legacy, false);
120
- assert.equal(summary.version, "0.1.86");
120
+ assert.equal(summary.version, "0.1.87");
121
121
 
122
122
  const validation = runJson(["app", "validate", manifestPath]);
123
123
  assert.equal(validation.valid, true, `${app.id} manifest must validate`);
124
124
 
125
125
  const shown = runJson(["app", "show", app.id]);
126
126
  assert.equal(shown.app.id, app.id);
127
- assert.equal(shown.app.version, "0.1.86");
127
+ assert.equal(shown.app.version, "0.1.87");
128
128
  assert.ok(shown.app.metadata.canonical, `${app.id} must be marked canonical`);
129
129
  assert.ok(shown.app.sandboxProfiles.length > 0, `${app.id} must declare sandbox profiles`);
130
130
  assertTaskIdsUnique(shown);
@@ -135,7 +135,7 @@ function main() {
135
135
  const plan = runJson(["plan", app.id, ...app.args(workspace)]);
136
136
  const state = JSON.parse(fs.readFileSync(plan.statePath, "utf8"));
137
137
  assert.equal(state.workflow.app.id, app.id);
138
- assert.equal(state.workflow.app.version, "0.1.86");
138
+ assert.equal(state.workflow.app.version, "0.1.87");
139
139
  assert.equal(state.workflow.app.metadata.canonical, true);
140
140
  assert.ok(state.tasks.some((task) => task.requiresEvidence), `${app.id} plan must include evidence gates`);
141
141
  assert.ok(state.tasks.every((task) => task.sandboxProfileId), `${app.id} plan must include sandbox hints`);