cool-workflow 0.1.85 → 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 +53 -332
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- package/apps/architecture-review-fast/workflow.js +2 -0
- 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-core.js +131 -3
- package/dist/capability-registry.js +108 -50
- package/dist/cli/command-surface.js +83 -10
- package/dist/cli.js +2 -1
- package/dist/doctor.js +50 -5
- package/dist/execution-backend.js +8 -0
- package/dist/mcp/tool-call.js +428 -0
- package/dist/mcp/tool-definitions.js +1027 -0
- package/dist/mcp-surface.js +5 -1442
- package/dist/onramp.js +421 -0
- package/dist/operator-ux/format.js +21 -15
- package/dist/operator-ux.js +2 -1
- package/dist/orchestrator.js +190 -87
- package/dist/term.js +93 -0
- package/dist/version.js +1 -1
- package/dist/workbench-host.js +1 -8
- package/docs/agent-delegation-drive.7.md +45 -8
- package/docs/cli-mcp-parity.7.md +41 -6
- package/docs/contract-migration-tooling.7.md +6 -0
- package/docs/control-plane-scheduling.7.md +6 -0
- package/docs/dogfood-one-real-repo.7.md +9 -1
- package/docs/durable-state-and-locking.7.md +6 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +6 -0
- package/docs/execution-backends.7.md +6 -0
- package/docs/getting-started.md +40 -2
- package/docs/index.md +51 -38
- package/docs/multi-agent-cli-mcp-surface.7.md +6 -0
- package/docs/multi-agent-eval-replay-harness.7.md +6 -0
- package/docs/multi-agent-operator-ux.7.md +6 -0
- package/docs/node-snapshot-diff-replay.7.md +6 -0
- package/docs/observability-cost-accounting.7.md +6 -0
- package/docs/project-index.md +24 -7
- package/docs/real-execution-backends.7.md +6 -0
- package/docs/release-and-migration.7.md +7 -1
- package/docs/release-history.md +352 -0
- package/docs/release-tooling.7.md +28 -0
- package/docs/run-registry-control-plane.7.md +6 -0
- package/docs/run-retention-reclamation.7.md +6 -0
- package/docs/state-explosion-management.7.md +6 -0
- package/docs/team-collaboration.7.md +6 -0
- package/docs/web-desktop-workbench.7.md +6 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +1 -1
- package/package.json +2 -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/architecture-review-fast.js +44 -2
- package/scripts/canonical-apps.js +4 -4
- package/scripts/dogfood-release.js +55 -133
- package/scripts/golden-path.js +4 -4
- package/scripts/onramp-check.js +47 -0
- package/scripts/parity-check.js +741 -2
- package/scripts/release-check.js +5 -3
- package/scripts/release-gate.sh +1 -1
- package/scripts/source-context.js +31 -5
- package/scripts/version-sync-check.js +4 -2
|
@@ -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
|
+
});
|
|
@@ -308,11 +308,13 @@ function buildMetrics(started, contextText, sourceContextElapsedMs, fastReview,
|
|
|
308
308
|
const steps = Array.isArray(fastReview?.steps) ? fastReview.steps : [];
|
|
309
309
|
const handleKinds = countBy(steps.map((step) => step && step.handleKind).filter(Boolean));
|
|
310
310
|
const actions = countBy(steps.map((step) => step && step.action).filter(Boolean));
|
|
311
|
+
const taskMetrics = buildTaskMetrics(fastReview, steps);
|
|
311
312
|
return {
|
|
312
313
|
totalElapsedMs: elapsedMs(started),
|
|
313
314
|
sourceContext: {
|
|
314
315
|
elapsedMs: sourceContextElapsedMs,
|
|
315
|
-
bytes: Buffer.byteLength(contextText, "utf8")
|
|
316
|
+
bytes: Buffer.byteLength(contextText, "utf8"),
|
|
317
|
+
digest: `sha256:${crypto.createHash("sha256").update(contextText, "utf8").digest("hex")}`
|
|
316
318
|
},
|
|
317
319
|
fastReview: {
|
|
318
320
|
elapsedMs: fastReviewElapsedMs,
|
|
@@ -323,12 +325,52 @@ function buildMetrics(started, contextText, sourceContextElapsedMs, fastReview,
|
|
|
323
325
|
actions,
|
|
324
326
|
handleKinds,
|
|
325
327
|
resultCacheHits: Number(handleKinds["result-cache"] || 0),
|
|
326
|
-
agentSpawns: steps.filter((step) => step && step.backendId === "agent" && step.handleKind && step.handleKind !== "result-cache").length
|
|
328
|
+
agentSpawns: steps.filter((step) => step && step.backendId === "agent" && step.handleKind && step.handleKind !== "result-cache").length,
|
|
329
|
+
taskMetrics
|
|
327
330
|
},
|
|
328
331
|
...(fullReviewScheduleElapsedMs === undefined ? {} : { fullReviewSchedule: { elapsedMs: fullReviewScheduleElapsedMs } })
|
|
329
332
|
};
|
|
330
333
|
}
|
|
331
334
|
|
|
335
|
+
function buildTaskMetrics(fastReview, steps) {
|
|
336
|
+
const statePath = stringArg(fastReview?.statePath);
|
|
337
|
+
const run = readRunState(statePath);
|
|
338
|
+
const tasks = Array.isArray(run?.tasks) ? run.tasks : [];
|
|
339
|
+
const byTask = new Map(tasks.map((task) => [task.id, task]));
|
|
340
|
+
return steps
|
|
341
|
+
.filter((step) => step && step.taskId)
|
|
342
|
+
.map((step) => {
|
|
343
|
+
const task = byTask.get(step.taskId) || {};
|
|
344
|
+
return {
|
|
345
|
+
phase: step.phase || task.phase || "",
|
|
346
|
+
taskId: step.taskId,
|
|
347
|
+
action: step.action,
|
|
348
|
+
status: step.status,
|
|
349
|
+
elapsedMs: taskElapsedMs(task),
|
|
350
|
+
handleKind: step.handleKind || "",
|
|
351
|
+
agentSpawned: Boolean(step.backendId === "agent" && step.handleKind && step.handleKind !== "result-cache"),
|
|
352
|
+
resultCacheHit: step.handleKind === "result-cache",
|
|
353
|
+
reportedModel: step.reportedModel || ""
|
|
354
|
+
};
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function readRunState(statePath) {
|
|
359
|
+
if (!statePath) return null;
|
|
360
|
+
try {
|
|
361
|
+
return JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
362
|
+
} catch {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function taskElapsedMs(task) {
|
|
368
|
+
const started = Date.parse(task?.startedAt || task?.dispatchedAt || "");
|
|
369
|
+
const completed = Date.parse(task?.completedAt || "");
|
|
370
|
+
if (!Number.isFinite(started) || !Number.isFinite(completed)) return null;
|
|
371
|
+
return Math.max(0, completed - started);
|
|
372
|
+
}
|
|
373
|
+
|
|
332
374
|
function countBy(values) {
|
|
333
375
|
const counts = {};
|
|
334
376
|
for (const value of values) counts[String(value)] = (counts[String(value)] || 0) + 1;
|
|
@@ -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`);
|