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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +8 -1
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- package/apps/end-to-end-golden-path/app.json +1 -1
- package/apps/pr-review-fix-ci/app.json +1 -1
- package/apps/release-cut/app.json +1 -1
- package/apps/research-synthesis/app.json +1 -1
- package/dist/capability-registry.js +24 -0
- package/dist/cli/command-surface.js +80 -10
- package/dist/cli.js +2 -1
- package/dist/doctor.js +13 -4
- package/dist/execution-backend.js +8 -0
- package/dist/operator-ux/format.js +21 -15
- package/dist/operator-ux.js +2 -1
- package/dist/orchestrator.js +172 -74
- package/dist/term.js +93 -0
- package/dist/version.js +1 -1
- package/docs/agent-delegation-drive.7.md +24 -6
- package/docs/cli-mcp-parity.7.md +12 -2
- package/docs/contract-migration-tooling.7.md +4 -0
- package/docs/control-plane-scheduling.7.md +4 -0
- package/docs/durable-state-and-locking.7.md +4 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
- package/docs/execution-backends.7.md +4 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
- package/docs/multi-agent-eval-replay-harness.7.md +4 -0
- package/docs/multi-agent-operator-ux.7.md +4 -0
- package/docs/node-snapshot-diff-replay.7.md +4 -0
- package/docs/observability-cost-accounting.7.md +4 -0
- package/docs/project-index.md +9 -3
- package/docs/real-execution-backends.7.md +4 -0
- package/docs/release-and-migration.7.md +4 -0
- package/docs/release-history.md +10 -0
- package/docs/release-tooling.7.md +12 -0
- package/docs/run-registry-control-plane.7.md +4 -0
- package/docs/run-retention-reclamation.7.md +4 -0
- package/docs/state-explosion-management.7.md +4 -0
- package/docs/team-collaboration.7.md +4 -0
- package/docs/web-desktop-workbench.7.md +4 -0
- package/manifest/plugin.manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/agents/agent-adapter-core.js +180 -0
- package/scripts/agents/builtin-templates.json +4 -1
- package/scripts/agents/codex-agent.js +134 -0
- package/scripts/agents/gemini-agent.js +115 -0
- package/scripts/agents/opencode-agent.js +119 -0
- package/scripts/canonical-apps.js +4 -4
- package/scripts/dogfood-release.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/parity-check.js +6 -5
- package/scripts/release-check.js +3 -3
- 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.
|
|
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
|
@@ -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
|
+
"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.
|
|
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.
|
|
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.
|
|
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`);
|