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.
Files changed (81) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +111 -68
  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/agent-config.js +42 -1
  11. package/dist/capability-core.js +9 -4
  12. package/dist/capability-registry.js +48 -0
  13. package/dist/cli/command-surface.js +182 -11
  14. package/dist/cli.js +2 -1
  15. package/dist/doctor.js +27 -5
  16. package/dist/drive.js +222 -16
  17. package/dist/execution-backend.js +12 -4
  18. package/dist/loop-expansion.js +60 -0
  19. package/dist/onramp.js +25 -0
  20. package/dist/operator-ux/format.js +21 -15
  21. package/dist/operator-ux.js +2 -1
  22. package/dist/orchestrator/lifecycle-operations.js +134 -3
  23. package/dist/orchestrator.js +122 -76
  24. package/dist/run-export.js +106 -2
  25. package/dist/state-node.js +13 -3
  26. package/dist/state.js +21 -0
  27. package/dist/telemetry-attestation.js +30 -6
  28. package/dist/telemetry-demo.js +29 -1
  29. package/dist/telemetry-ledger.js +6 -0
  30. package/dist/term.js +93 -0
  31. package/dist/version.js +1 -1
  32. package/dist/worker-accept/telemetry-ledger.js +12 -2
  33. package/dist/workflow-api.js +33 -0
  34. package/dist/workflow-app-framework.js +20 -0
  35. package/docs/agent-delegation-drive.7.md +28 -6
  36. package/docs/capability-topology-registry.7.md +69 -46
  37. package/docs/cli-mcp-parity.7.md +22 -2
  38. package/docs/contract-migration-tooling.7.md +8 -0
  39. package/docs/control-plane-scheduling.7.md +8 -0
  40. package/docs/durable-state-and-locking.7.md +8 -0
  41. package/docs/evidence-adoption-reasoning-chain.7.md +8 -0
  42. package/docs/execution-backends.7.md +8 -0
  43. package/docs/launch/launch-kit.md +9 -9
  44. package/docs/multi-agent-cli-mcp-surface.7.md +8 -0
  45. package/docs/multi-agent-eval-replay-harness.7.md +8 -0
  46. package/docs/multi-agent-operator-ux.7.md +8 -0
  47. package/docs/node-snapshot-diff-replay.7.md +8 -0
  48. package/docs/observability-cost-accounting.7.md +8 -0
  49. package/docs/project-index.md +26 -5
  50. package/docs/readme-v0.1.87-full.md +301 -0
  51. package/docs/real-execution-backends.7.md +8 -0
  52. package/docs/release-and-migration.7.md +8 -0
  53. package/docs/release-history.md +10 -0
  54. package/docs/release-tooling.7.md +16 -0
  55. package/docs/report-verifiable-bundle.7.md +34 -2
  56. package/docs/run-registry-control-plane.7.md +18 -0
  57. package/docs/run-retention-reclamation.7.md +8 -0
  58. package/docs/state-explosion-management.7.md +8 -0
  59. package/docs/team-collaboration.7.md +8 -0
  60. package/docs/trust-model.md +6 -4
  61. package/docs/web-desktop-workbench.7.md +8 -0
  62. package/manifest/plugin.manifest.json +1 -1
  63. package/manifest/source-context-profiles.json +1 -1
  64. package/package.json +8 -5
  65. package/scripts/agents/agent-adapter-core.js +180 -0
  66. package/scripts/agents/builtin-templates.json +5 -1
  67. package/scripts/agents/claude-p-agent.js +7 -33
  68. package/scripts/agents/codex-agent.js +134 -0
  69. package/scripts/agents/cw-attest-wrap.js +9 -1
  70. package/scripts/agents/gemini-agent.js +115 -0
  71. package/scripts/agents/opencode-agent.js +119 -0
  72. package/scripts/canonical-apps.js +4 -4
  73. package/scripts/coverage-gate.js +15 -1
  74. package/scripts/cw.js +0 -0
  75. package/scripts/dogfood-release.js +1 -1
  76. package/scripts/golden-path.js +4 -4
  77. package/scripts/parity-check.js +6 -5
  78. package/scripts/release-check.js +3 -3
  79. package/scripts/release-flow.js +49 -2
  80. package/scripts/release-gate.sh +1 -1
  81. 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",
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.86");
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.86");
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.86");
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`);
@@ -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.86";
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, "..", "..");
@@ -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.86");
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.86 end-to-end golden path."
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.86");
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\.86/);
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/);
@@ -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.slice(start + 1)) {
81
- if (!line.trim()) break;
82
- const first = line.trim().split(/\s+/)[0];
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);
@@ -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
- // Parallel suite (test:ci = run-all.js --concurrency auto). Each smoke runs in
64
- // a private cwd + state roots (CW_HOME/HOME/TMPDIR), so concurrency is race-free.
65
- // The bare `npm test` and the tag-gate (release-gate.sh) stay sequential as the
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"] },
@@ -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
- return;
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
@@ -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
@@ -8,7 +8,9 @@
8
8
  "strict": true,
9
9
  "esModuleInterop": true,
10
10
  "forceConsistentCasingInFileNames": true,
11
- "skipLibCheck": true
11
+ "skipLibCheck": true,
12
+ "incremental": true,
13
+ "tsBuildInfoFile": ".cache/tsconfig.tsbuildinfo"
12
14
  },
13
15
  "include": [
14
16
  "src/**/*.ts"