cool-workflow 0.1.86 → 0.1.88
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 +111 -68
- 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/agent-config.js +42 -1
- package/dist/capability-core.js +9 -4
- package/dist/capability-registry.js +48 -0
- package/dist/cli/command-surface.js +182 -11
- package/dist/cli.js +2 -1
- package/dist/doctor.js +27 -5
- package/dist/drive.js +222 -16
- package/dist/execution-backend.js +12 -4
- package/dist/loop-expansion.js +60 -0
- package/dist/onramp.js +25 -0
- package/dist/operator-ux/format.js +21 -15
- package/dist/operator-ux.js +2 -1
- package/dist/orchestrator/lifecycle-operations.js +134 -3
- package/dist/orchestrator.js +122 -76
- package/dist/run-export.js +106 -2
- package/dist/state-node.js +13 -3
- package/dist/state.js +21 -0
- package/dist/telemetry-attestation.js +30 -6
- package/dist/telemetry-demo.js +29 -1
- package/dist/telemetry-ledger.js +6 -0
- package/dist/term.js +93 -0
- package/dist/version.js +1 -1
- package/dist/worker-accept/telemetry-ledger.js +12 -2
- package/dist/workflow-api.js +33 -0
- package/dist/workflow-app-framework.js +20 -0
- package/docs/agent-delegation-drive.7.md +28 -6
- package/docs/capability-topology-registry.7.md +69 -46
- package/docs/cli-mcp-parity.7.md +22 -2
- package/docs/contract-migration-tooling.7.md +8 -0
- package/docs/control-plane-scheduling.7.md +8 -0
- package/docs/durable-state-and-locking.7.md +8 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +8 -0
- package/docs/execution-backends.7.md +8 -0
- package/docs/launch/launch-kit.md +9 -9
- package/docs/multi-agent-cli-mcp-surface.7.md +8 -0
- package/docs/multi-agent-eval-replay-harness.7.md +8 -0
- package/docs/multi-agent-operator-ux.7.md +8 -0
- package/docs/node-snapshot-diff-replay.7.md +8 -0
- package/docs/observability-cost-accounting.7.md +8 -0
- package/docs/project-index.md +26 -5
- package/docs/readme-v0.1.87-full.md +301 -0
- package/docs/real-execution-backends.7.md +8 -0
- package/docs/release-and-migration.7.md +8 -0
- package/docs/release-history.md +10 -0
- package/docs/release-tooling.7.md +16 -0
- package/docs/report-verifiable-bundle.7.md +34 -2
- package/docs/run-registry-control-plane.7.md +18 -0
- package/docs/run-retention-reclamation.7.md +8 -0
- package/docs/state-explosion-management.7.md +8 -0
- package/docs/team-collaboration.7.md +8 -0
- package/docs/trust-model.md +6 -4
- package/docs/web-desktop-workbench.7.md +8 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +1 -1
- package/package.json +8 -5
- package/scripts/agents/agent-adapter-core.js +180 -0
- package/scripts/agents/builtin-templates.json +5 -1
- package/scripts/agents/claude-p-agent.js +7 -33
- package/scripts/agents/codex-agent.js +134 -0
- package/scripts/agents/cw-attest-wrap.js +9 -1
- 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/coverage-gate.js +15 -1
- package/scripts/cw.js +0 -0
- 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-flow.js +49 -2
- package/scripts/release-gate.sh +1 -1
- package/tsconfig.json +3 -1
|
@@ -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 !== "0" && 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 !== "0" && 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.88",
|
|
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.88");
|
|
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.88");
|
|
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.88");
|
|
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`);
|
package/scripts/coverage-gate.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
//
|
|
26
26
|
// Usage: node scripts/coverage-gate.js [--min 80] [--concurrency <n|auto>]
|
|
27
27
|
|
|
28
|
-
const { spawn } = require("node:child_process");
|
|
28
|
+
const { spawn, spawnSync } = require("node:child_process");
|
|
29
29
|
const fs = require("node:fs");
|
|
30
30
|
const os = require("node:os");
|
|
31
31
|
const path = require("node:path");
|
|
@@ -50,11 +50,25 @@ if (!Number.isFinite(floor) || floor < 0 || floor > 100) {
|
|
|
50
50
|
|
|
51
51
|
const covDir = fs.mkdtempSync(path.join(os.tmpdir(), "cw-coverage-"));
|
|
52
52
|
|
|
53
|
+
// Pre-check: ensure dist/ is built before entering the coverage merge phase
|
|
54
|
+
// (parity with the `test` and `test:ci` package.json scripts).
|
|
55
|
+
{
|
|
56
|
+
const cli = path.join(packageDir, "dist", "cli.js");
|
|
57
|
+
const check = spawnSync(process.execPath, [cli, "version"], { cwd: packageDir, stdio: "pipe", encoding: "utf8" });
|
|
58
|
+
const out = String(check.stdout || "").trim();
|
|
59
|
+
if (check.status !== 0 || !out) {
|
|
60
|
+
process.stderr.write(`${SELF}: dist/cli.js version failed (exit ${check.status}) — build may be stale. Run \`npm run build\` first.\n`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
53
65
|
function runSuite() {
|
|
54
66
|
return new Promise((resolve) => {
|
|
55
67
|
const args = [path.join(packageDir, "test", "run-all.js")];
|
|
56
68
|
const concurrency = flagValue("--concurrency");
|
|
57
69
|
if (concurrency) args.push("--concurrency", concurrency);
|
|
70
|
+
const sample = flagValue("--sample");
|
|
71
|
+
if (sample) args.push("--sample", sample);
|
|
58
72
|
const child = spawn(process.execPath, args, {
|
|
59
73
|
cwd: packageDir,
|
|
60
74
|
stdio: "inherit",
|
package/scripts/cw.js
CHANGED
|
File without changes
|
|
@@ -6,7 +6,7 @@ const fs = require("node:fs");
|
|
|
6
6
|
const path = require("node:path");
|
|
7
7
|
const { CoolWorkflowRunner } = require("../dist/orchestrator.js");
|
|
8
8
|
|
|
9
|
-
const TARGET_VERSION = "0.1.
|
|
9
|
+
const TARGET_VERSION = "0.1.88";
|
|
10
10
|
const PREVIOUS_VERSION = "0.1.31";
|
|
11
11
|
const pluginRoot = path.resolve(__dirname, "..");
|
|
12
12
|
const repoRoot = path.resolve(pluginRoot, "..", "..");
|
package/scripts/golden-path.js
CHANGED
|
@@ -33,7 +33,7 @@ function main() {
|
|
|
33
33
|
const appValidation = runJson(["app", "validate", "end-to-end-golden-path"], pluginRoot);
|
|
34
34
|
assert.equal(appValidation.valid, true);
|
|
35
35
|
assert.equal(appValidation.summary.id, "end-to-end-golden-path");
|
|
36
|
-
assert.equal(appValidation.summary.version, "0.1.
|
|
36
|
+
assert.equal(appValidation.summary.version, "0.1.88");
|
|
37
37
|
|
|
38
38
|
const plan = runJson(
|
|
39
39
|
[
|
|
@@ -42,7 +42,7 @@ function main() {
|
|
|
42
42
|
"--repo",
|
|
43
43
|
tmp,
|
|
44
44
|
"--question",
|
|
45
|
-
"Prove the deterministic v0.1.
|
|
45
|
+
"Prove the deterministic v0.1.88 end-to-end golden path."
|
|
46
46
|
],
|
|
47
47
|
pluginRoot
|
|
48
48
|
);
|
|
@@ -52,7 +52,7 @@ function main() {
|
|
|
52
52
|
|
|
53
53
|
let state = readJson(plan.statePath);
|
|
54
54
|
assert.equal(state.workflow.app.id, "end-to-end-golden-path");
|
|
55
|
-
assert.equal(state.workflow.app.version, "0.1.
|
|
55
|
+
assert.equal(state.workflow.app.version, "0.1.88");
|
|
56
56
|
assert.equal(state.loopStage, "interpret");
|
|
57
57
|
|
|
58
58
|
const dispatch = runJson(["dispatch", plan.runId, "--limit", "1", "--sandbox", "readonly"], tmp);
|
|
@@ -195,7 +195,7 @@ function main() {
|
|
|
195
195
|
assert.equal(reportPath, plan.reportPath);
|
|
196
196
|
assert.ok(fs.existsSync(reportPath));
|
|
197
197
|
const report = fs.readFileSync(reportPath, "utf8");
|
|
198
|
-
assert.match(report, /Workflow App: end-to-end-golden-path@0\.1\.
|
|
198
|
+
assert.match(report, /Workflow App: end-to-end-golden-path@0\.1\.88/);
|
|
199
199
|
assert.match(report, /## Candidates/);
|
|
200
200
|
assert.match(report, /## Trust Audit/);
|
|
201
201
|
assert.match(report, /## Acceptance Rationale/);
|
package/scripts/parity-check.js
CHANGED
|
@@ -74,12 +74,13 @@ function cliDispatchSources() {
|
|
|
74
74
|
|
|
75
75
|
function cliHelpTokens() {
|
|
76
76
|
const lines = formatHelp().split(/\r?\n/);
|
|
77
|
-
const start = lines.indexOf("Commands:");
|
|
78
|
-
if (start < 0) return [];
|
|
79
77
|
const tokens = new Set();
|
|
80
|
-
for (const line of lines
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
for (const line of lines) {
|
|
79
|
+
// Command lines start with exactly 2 spaces; examples are 4-space.
|
|
80
|
+
if (!line.startsWith(" ") || line.startsWith(" ")) continue;
|
|
81
|
+
const trimmed = line.trim();
|
|
82
|
+
if (!trimmed || !/^[a-z]/.test(trimmed)) continue;
|
|
83
|
+
const first = trimmed.split(/\s+/)[0];
|
|
83
84
|
for (const token of first.split("|")) {
|
|
84
85
|
const clean = token.replace(/[<[].*$/, "");
|
|
85
86
|
if (clean) tokens.add(clean);
|
package/scripts/release-check.js
CHANGED
|
@@ -60,9 +60,9 @@ const checks = [
|
|
|
60
60
|
{ name: "type check", command: ["npm", "run", "check"] },
|
|
61
61
|
{ name: "onramp contract", command: ["npm", "run", "onramp:check"] },
|
|
62
62
|
{ name: "run-state schema consistency", command: ["node", "scripts/validate-run-state-schema.js"] },
|
|
63
|
-
//
|
|
64
|
-
// a private cwd + state roots
|
|
65
|
-
//
|
|
63
|
+
// Every test path uses --concurrency auto by default (parallel, race-free:
|
|
64
|
+
// each smoke runs in a private cwd + state roots). The release tag-gate
|
|
65
|
+
// (release-gate.sh) forces CW_TEST_CONCURRENCY=1 to stay sequential as the
|
|
66
66
|
// deterministic backstop.
|
|
67
67
|
{ name: "tests", command: ["npm", "run", "test:ci"] },
|
|
68
68
|
{ name: "canonical apps", command: ["npm", "run", "canonical-apps"] },
|
package/scripts/release-flow.js
CHANGED
|
@@ -25,6 +25,12 @@
|
|
|
25
25
|
// --soft for best-effort (skip-not-fail).
|
|
26
26
|
// Flags also accepted: --prev-tag <t>, --agent-command "...", --agent-model m, --dry-run
|
|
27
27
|
//
|
|
28
|
+
// VERDICT CONTRACT (auto-generated by --cut, never write by hand):
|
|
29
|
+
// Path: .cw-release/review-<FULLSHA>.verdict
|
|
30
|
+
// First line: "APPROVED <FULLSHA>"
|
|
31
|
+
// Both release-flow.js --cut and CI's release-gate.yml verify this exact path and
|
|
32
|
+
// format. The file MUST be committed to the tag's history.
|
|
33
|
+
//
|
|
28
34
|
// Test seams (smoke/operator only, never the delegated agent):
|
|
29
35
|
// CW_RELEASE_FLOW_GATE_CMD overrides the deterministic gate command
|
|
30
36
|
// (default: `bash <thisdir>/release-gate.sh`) so the smoke can exercise the
|
|
@@ -153,6 +159,32 @@ function substitute(arg, map) {
|
|
|
153
159
|
return arg.replace(/\{\{(\w+)\}\}/g, (m, k) => (k in map ? String(map[k]) : m));
|
|
154
160
|
}
|
|
155
161
|
|
|
162
|
+
/** Extract verdict lines from agent stdout. Returns the verdict text (first line
|
|
163
|
+
* must be APPROVED <sha> or REJECTED) or null if no valid verdict found.
|
|
164
|
+
* Also logs failures to stderr so the operator can inspect. */
|
|
165
|
+
function extractVerdictFromStdout(stdout, resultPath) {
|
|
166
|
+
const lines = stdout.split(/\r?\n/);
|
|
167
|
+
const approvedLine = lines.find((line) => /^APPROVED\s+\S+/.test(line.trim()));
|
|
168
|
+
if (approvedLine) {
|
|
169
|
+
const idx = lines.indexOf(approvedLine);
|
|
170
|
+
// Include the APPROVED line and any capability line that follows.
|
|
171
|
+
const capLine = (lines[idx + 1] || "").trim();
|
|
172
|
+
const verdict = capLine && !capLine.startsWith("#") && !/^(APPROVED|REJECTED)\s/.test(capLine)
|
|
173
|
+
? [approvedLine.trim(), capLine].join("\n")
|
|
174
|
+
: approvedLine.trim();
|
|
175
|
+
say(`reviewer verdict captured from stdout → ${resultPath}`);
|
|
176
|
+
return verdict;
|
|
177
|
+
}
|
|
178
|
+
const rejectedLine = lines.find((line) => /^REJECTED/i.test(line.trim()));
|
|
179
|
+
if (rejectedLine) {
|
|
180
|
+
process.stderr.write(`reviewer REJECTED via stdout — full output:\n${stdout.trim()}\n`);
|
|
181
|
+
return rejectedLine.trim();
|
|
182
|
+
}
|
|
183
|
+
// Partial: print the last 20 lines to help the operator diagnose.
|
|
184
|
+
process.stderr.write(`reviewer stdout had no APPROVED/REJECTED line. Last 20 lines:\n${lines.slice(-20).join("\n").trim()}\n`);
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
156
188
|
function delegateReview(resultPath, inputPath) {
|
|
157
189
|
// Reuse the canonical agent-config resolver (flags > env > file).
|
|
158
190
|
let resolveAgentConfig;
|
|
@@ -188,16 +220,31 @@ function delegateReview(resultPath, inputPath) {
|
|
|
188
220
|
say(`[2/3] reviewer — delegating to: ${cfg.command} ${(cfg.args || []).join(" ")} (model: ${cfg.model || "unreported"})`);
|
|
189
221
|
// RED LINE: argv-style, shell:false. The agent runs the model in its own
|
|
190
222
|
// process and inherits its own credentials; CW holds none.
|
|
223
|
+
// Capture stdout so agents that print verdicts (rather than writing the
|
|
224
|
+
// result file) are supported. Agents that DO write the file still work:
|
|
225
|
+
// the file takes precedence. stderr goes to the terminal for live output.
|
|
191
226
|
const r = spawnSync(cfg.command, args, {
|
|
192
227
|
cwd: repoRoot,
|
|
193
228
|
env: { ...process.env },
|
|
194
229
|
encoding: "utf8",
|
|
195
230
|
timeout: cfg.timeoutMs || 600000,
|
|
196
231
|
shell: false,
|
|
197
|
-
stdio: "inherit"
|
|
232
|
+
stdio: ["ignore", "pipe", "inherit"],
|
|
233
|
+
maxBuffer: 32 * 1024 * 1024
|
|
198
234
|
});
|
|
199
235
|
if (r.status !== 0) die(`reviewer agent exited ${r.status === null ? "(timeout/no-exit)" : r.status} — no verdict trusted.`);
|
|
200
|
-
|
|
236
|
+
// If the agent already wrote the verdict file (backward compat), use it.
|
|
237
|
+
// Otherwise extract the verdict from stdout (headless agent path).
|
|
238
|
+
if (fs.existsSync(resultPath)) return;
|
|
239
|
+
const stdout = String(r.stdout || "").trim();
|
|
240
|
+
if (stdout) {
|
|
241
|
+
const verdictLines = extractVerdictFromStdout(stdout, resultPath);
|
|
242
|
+
if (verdictLines) {
|
|
243
|
+
fs.writeFileSync(resultPath, `${verdictLines}\n`);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
die("reviewer agent produced no verdict — stdout had no APPROVED <sha> or REJECTED line, and no verdict file was written. Ensure the agent writes the verdict to {{result}} or prints the verdict lines to stdout.");
|
|
201
248
|
}
|
|
202
249
|
|
|
203
250
|
// Endpoint mode (e.g. DeepSeek HTTP): POST the prompt, write the response as
|
package/scripts/release-gate.sh
CHANGED
|
@@ -30,7 +30,7 @@ say "[1/6] build"
|
|
|
30
30
|
npm run --prefix plugins/cool-workflow build >/dev/null 2>&1 || fail "build failed"
|
|
31
31
|
|
|
32
32
|
say "[2/6] tests"
|
|
33
|
-
npm test --prefix plugins/cool-workflow >/dev/null 2>&1 || fail "tests failed"
|
|
33
|
+
CW_TEST_CONCURRENCY=1 npm test --prefix plugins/cool-workflow >/dev/null 2>&1 || fail "tests failed"
|
|
34
34
|
|
|
35
35
|
if [[ -n "$PREV_TAG" ]]; then
|
|
36
36
|
RANGE="$PREV_TAG..HEAD"
|
package/tsconfig.json
CHANGED