cool-workflow 0.1.94 → 0.1.96

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 (74) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +4 -0
  4. package/apps/architecture-review/app.json +1 -1
  5. package/apps/architecture-review/workflow.js +3 -3
  6. package/apps/architecture-review-fast/app.json +1 -1
  7. package/apps/end-to-end-golden-path/app.json +1 -1
  8. package/apps/pdca-blackboard-loop/app.json +45 -0
  9. package/apps/pdca-blackboard-loop/workflow.js +59 -0
  10. package/apps/pr-review-fix-ci/app.json +1 -1
  11. package/apps/release-cut/app.json +1 -1
  12. package/apps/research-synthesis/app.json +1 -1
  13. package/dist/agent-config.js +2 -1
  14. package/dist/cli/command-surface.js +3 -1
  15. package/dist/dispatch.js +12 -6
  16. package/dist/evidence-grounding.js +18 -13
  17. package/dist/execution-backend/probes.js +22 -6
  18. package/dist/execution-backend.js +37 -4
  19. package/dist/node-snapshot.js +3 -3
  20. package/dist/orchestrator/lifecycle-operations.js +13 -5
  21. package/dist/orchestrator.js +28 -2
  22. package/dist/reclamation.js +8 -2
  23. package/dist/run-registry/derive.js +4 -1
  24. package/dist/scheduler.js +14 -14
  25. package/dist/schema-validate.js +8 -2
  26. package/dist/state-explosion/helpers.js +4 -21
  27. package/dist/state-explosion/size.js +63 -0
  28. package/dist/state-explosion.js +18 -71
  29. package/dist/state.js +47 -9
  30. package/dist/trust-audit.js +27 -2
  31. package/dist/util/fingerprint.js +19 -0
  32. package/dist/util/fingerprint.test.js +27 -0
  33. package/dist/version.js +1 -1
  34. package/dist/workbench-host.js +11 -0
  35. package/dist/workbench.js +19 -17
  36. package/dist/worker-isolation.js +25 -1
  37. package/docs/agent-delegation-drive.7.md +66 -1
  38. package/docs/cli-mcp-parity.7.md +4 -0
  39. package/docs/contract-migration-tooling.7.md +4 -0
  40. package/docs/control-plane-scheduling.7.md +4 -0
  41. package/docs/durable-state-and-locking.7.md +4 -0
  42. package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
  43. package/docs/execution-backends.7.md +4 -0
  44. package/docs/multi-agent-cli-mcp-surface.7.md +12 -0
  45. package/docs/multi-agent-eval-replay-harness.7.md +4 -0
  46. package/docs/multi-agent-operator-ux.7.md +4 -0
  47. package/docs/node-snapshot-diff-replay.7.md +4 -0
  48. package/docs/observability-cost-accounting.7.md +4 -0
  49. package/docs/project-index.md +11 -4
  50. package/docs/real-execution-backends.7.md +4 -0
  51. package/docs/release-and-migration.7.md +4 -0
  52. package/docs/release-tooling.7.md +20 -0
  53. package/docs/routines.md +30 -0
  54. package/docs/run-registry-control-plane.7.md +4 -0
  55. package/docs/run-retention-reclamation.7.md +4 -0
  56. package/docs/sandbox-profiles.7.md +15 -0
  57. package/docs/state-explosion-management.7.md +4 -0
  58. package/docs/team-collaboration.7.md +4 -0
  59. package/docs/web-desktop-workbench.7.md +4 -0
  60. package/manifest/plugin.manifest.json +1 -1
  61. package/package.json +8 -5
  62. package/scripts/agents/agent-adapter-core.js +22 -1
  63. package/scripts/agents/claude-p-agent.js +10 -2
  64. package/scripts/agents/codex-agent.js +22 -2
  65. package/scripts/agents/gemini-agent.js +10 -2
  66. package/scripts/agents/opencode-agent.js +10 -2
  67. package/scripts/bump-version.js +10 -0
  68. package/scripts/canonical-apps.js +4 -4
  69. package/scripts/dogfood-release.js +1 -1
  70. package/scripts/golden-path.js +4 -4
  71. package/scripts/release-check.js +7 -1
  72. package/scripts/release-flow.js +44 -3
  73. package/scripts/release-gate.sh +1 -1
  74. package/scripts/version-sync-check.js +2 -0
@@ -34,7 +34,7 @@ const { spawn, spawnSync } = require("node:child_process");
34
34
  // wrappers instead of carrying a private copy. A drifted inline copy (ASCII
35
35
  // hyphens silently became em-dashes here) meant claude was sent a different
36
36
  // instruction text than the other providers for the same contract.
37
- const { buildPrompt, createRenderer, toolLabel, summarizeToolResult } = require("./agent-adapter-core");
37
+ const { buildPrompt, createRenderer, persistStderr, toolLabel, summarizeToolResult } = require("./agent-adapter-core");
38
38
 
39
39
  const inputPath = process.argv[2];
40
40
  const resultPath = process.argv[3];
@@ -56,11 +56,14 @@ if (!streamEnabled) {
56
56
  shell: false
57
57
  });
58
58
  if (child.error) {
59
+ persistStderr(resultPath, `claude spawn failed: ${child.error.message}`);
59
60
  process.stderr.write(`claude spawn failed: ${child.error.message}\n`);
60
61
  process.exit(1);
61
62
  }
62
63
  if (child.status !== 0) {
63
- process.stderr.write(String(child.stderr || `claude exited ${child.status}`));
64
+ const detail = String(child.stderr || `claude exited ${child.status}`);
65
+ persistStderr(resultPath, detail);
66
+ process.stderr.write(detail);
64
67
  process.exit(child.status === null ? 1 : child.status);
65
68
  }
66
69
 
@@ -69,6 +72,7 @@ if (!streamEnabled) {
69
72
  try {
70
73
  parsed = JSON.parse(out);
71
74
  } catch (error) {
75
+ persistStderr(resultPath, `claude output was not JSON: ${error.message}`);
72
76
  process.stderr.write(`claude output was not JSON: ${error.message}\n`);
73
77
  process.exit(1);
74
78
  }
@@ -166,6 +170,7 @@ function renderEvent(ev) {
166
170
 
167
171
  child.on("error", (err) => {
168
172
  render.finishLive(); // restore the terminal before exiting
173
+ persistStderr(resultPath, `claude spawn failed: ${err.message}`);
169
174
  process.stderr.write(`claude spawn failed: ${err.message}\n`);
170
175
  process.exit(1);
171
176
  });
@@ -174,12 +179,15 @@ child.on("close", (code) => {
174
179
  render.finishLive(); // stop the spinner + restore the cursor BEFORE any further output
175
180
  render.writeTranscript(transcriptPath); // full narration + tool I/O always saved
176
181
  if (code !== 0) {
182
+ const detail = childStderr.trim() || `claude exited ${code === null ? "(timeout/killed)" : code}`;
183
+ persistStderr(resultPath, detail);
177
184
  if (childStderr.trim()) process.stderr.write(`${childStderr.trim()}\n`);
178
185
  process.stderr.write(`claude exited ${code === null ? "(timeout/killed)" : code}\n`);
179
186
  process.exit(code === null ? 1 : code);
180
187
  }
181
188
  if (typeof resultText !== "string") {
182
189
  // Fail closed: no result event ⇒ no result.md ⇒ CW records a failed hop.
190
+ persistStderr(resultPath, childStderr.trim() || "claude produced no result event — refusing to fabricate a result");
183
191
  process.stderr.write("claude produced no result event — refusing to fabricate a result\n");
184
192
  process.exit(1);
185
193
  }
@@ -14,6 +14,13 @@
14
14
  //
15
15
  // stdout: one JSON object { model, usage, result } for CW provenance.
16
16
  // stderr: optional live trace when CW_AGENT_STREAM=1 and attached to a TTY.
17
+ //
18
+ // SPEED: codex `exec` inherits the user's ~/.codex/config.toml, including a heavy
19
+ // `model_reasoning_effort` (e.g. "high"), which makes every read/grep turn slow.
20
+ // CW caps it for ITS runs only via `-c model_reasoning_effort=<effort>` — a
21
+ // per-run override that does NOT touch the user's interactive codex. Tune with
22
+ // CW_CODEX_REASONING_EFFORT (default "low"); set it to "medium"/"high" to opt back
23
+ // into more thinking.
17
24
 
18
25
  const fs = require("node:fs");
19
26
  const os = require("node:os");
@@ -25,6 +32,7 @@ const {
25
32
  emitReport,
26
33
  flushJsonLines,
27
34
  parseJsonLines,
35
+ persistStderr,
28
36
  writeResult
29
37
  } = require("./agent-adapter-core");
30
38
 
@@ -74,9 +82,14 @@ function recordJsonLine(line) {
74
82
 
75
83
  render.action("codex: reading the repo (read-only)…");
76
84
 
85
+ // Cap codex's reasoning effort for CW runs (speed) — overrides config.toml for
86
+ // THIS invocation only. Default "low"; CW_CODEX_REASONING_EFFORT opts back up.
87
+ const effort = process.env.CW_CODEX_REASONING_EFFORT || "low";
77
88
  const args = [
78
89
  "exec",
79
90
  "--json",
91
+ "-c",
92
+ `model_reasoning_effort=${effort}`,
80
93
  "--output-last-message",
81
94
  finalPath,
82
95
  "--sandbox",
@@ -108,6 +121,7 @@ child.stderr.on("data", (chunk) => {
108
121
 
109
122
  child.on("error", (error) => {
110
123
  render.finishLive();
124
+ persistStderr(resultPath, `codex spawn failed: ${error.message}`);
111
125
  process.stderr.write(`codex spawn failed: ${error.message}\n`);
112
126
  process.exit(1);
113
127
  });
@@ -118,11 +132,14 @@ child.on("close", (code) => {
118
132
  render.writeTranscript(transcriptPath);
119
133
  if (code !== 0) {
120
134
  const detail = childStderr.trim() || `codex exited ${code === null ? "(timeout/killed)" : code}`;
135
+ persistStderr(resultPath, detail);
121
136
  process.stderr.write(`${detail}\n`);
122
137
  process.exit(code === null ? 1 : code);
123
138
  }
124
139
  if (state.invalidJson) {
125
- process.stderr.write("codex --json produced a non-JSONL stdout line - refusing to trust the result\n");
140
+ const detail = "codex --json produced a non-JSONL stdout line - refusing to trust the result";
141
+ persistStderr(resultPath, childStderr.trim() || detail);
142
+ process.stderr.write(`${detail}\n`);
126
143
  process.exit(1);
127
144
  }
128
145
 
@@ -130,7 +147,9 @@ child.on("close", (code) => {
130
147
  try {
131
148
  resultText = fs.readFileSync(finalPath, "utf8");
132
149
  } catch {
133
- process.stderr.write("codex produced no final output file - refusing to fabricate a result\n");
150
+ const detail = "codex produced no final output file - refusing to fabricate a result";
151
+ persistStderr(resultPath, childStderr.trim() || detail);
152
+ process.stderr.write(`${detail}\n`);
134
153
  process.exit(1);
135
154
  } finally {
136
155
  try {
@@ -143,6 +162,7 @@ child.on("close", (code) => {
143
162
  try {
144
163
  writeResult(resultPath, resultText);
145
164
  } catch (error) {
165
+ persistStderr(resultPath, `codex produced no final result: ${error.message}`);
146
166
  process.stderr.write(`codex produced no final result: ${error.message}\n`);
147
167
  process.exit(1);
148
168
  }
@@ -22,6 +22,7 @@ const {
22
22
  emitReport,
23
23
  flushJsonLines,
24
24
  parseJsonLines,
25
+ persistStderr,
25
26
  writeResult
26
27
  } = require("./agent-adapter-core");
27
28
 
@@ -83,6 +84,7 @@ child.stderr.on("data", (chunk) => {
83
84
 
84
85
  child.on("error", (error) => {
85
86
  render.finishLive();
87
+ persistStderr(resultPath, `gemini spawn failed: ${error.message}`);
86
88
  process.stderr.write(`gemini spawn failed: ${error.message}\n`);
87
89
  process.exit(1);
88
90
  });
@@ -93,23 +95,29 @@ child.on("close", (code) => {
93
95
  render.writeTranscript(transcriptPath);
94
96
  if (code !== 0) {
95
97
  const detail = childStderr.trim() || `gemini exited ${code === null ? "(timeout/killed)" : code}`;
98
+ persistStderr(resultPath, detail);
96
99
  process.stderr.write(`${detail}\n`);
97
100
  process.exit(code === null ? 1 : code);
98
101
  }
99
102
  if (state.invalidJson) {
100
- process.stderr.write("gemini --output-format stream-json produced a non-JSONL stdout line - refusing to trust the result\n");
103
+ const detail = "gemini --output-format stream-json produced a non-JSONL stdout line - refusing to trust the result";
104
+ persistStderr(resultPath, childStderr.trim() || detail);
105
+ process.stderr.write(`${detail}\n`);
101
106
  process.exit(1);
102
107
  }
103
108
 
104
109
  const resultText = state.finalResult || state.textFragments.join("\n\n");
105
110
  if (!resultText.trim()) {
106
- process.stderr.write("gemini produced no result text - refusing to fabricate a result\n");
111
+ const detail = "gemini produced no result text - refusing to fabricate a result";
112
+ persistStderr(resultPath, childStderr.trim() || detail);
113
+ process.stderr.write(`${detail}\n`);
107
114
  process.exit(1);
108
115
  }
109
116
 
110
117
  try {
111
118
  writeResult(resultPath, resultText);
112
119
  } catch (error) {
120
+ persistStderr(resultPath, `gemini produced no final result: ${error.message}`);
113
121
  process.stderr.write(`gemini produced no final result: ${error.message}\n`);
114
122
  process.exit(1);
115
123
  }
@@ -27,6 +27,7 @@ const {
27
27
  emitReport,
28
28
  flushJsonLines,
29
29
  parseJsonLines,
30
+ persistStderr,
30
31
  writeResult
31
32
  } = require("./agent-adapter-core");
32
33
 
@@ -136,6 +137,7 @@ child.stderr.on("data", (chunk) => {
136
137
 
137
138
  child.on("error", (error) => {
138
139
  render.finishLive();
140
+ persistStderr(resultPath, `opencode spawn failed: ${error.message}`);
139
141
  process.stderr.write(`opencode spawn failed: ${error.message}\n`);
140
142
  process.exit(1);
141
143
  });
@@ -146,23 +148,29 @@ child.on("close", (code) => {
146
148
  render.writeTranscript(transcriptPath);
147
149
  if (code !== 0) {
148
150
  const detail = childStderr.trim() || `opencode exited ${code === null ? "(timeout/killed)" : code}`;
151
+ persistStderr(resultPath, detail);
149
152
  process.stderr.write(`${detail}\n`);
150
153
  process.exit(code === null ? 1 : code);
151
154
  }
152
155
  if (state.invalidJson) {
153
- process.stderr.write("opencode --format json produced a non-JSONL stdout line - refusing to trust the result\n");
156
+ const detail = "opencode --format json produced a non-JSONL stdout line - refusing to trust the result";
157
+ persistStderr(resultPath, childStderr.trim() || detail);
158
+ process.stderr.write(`${detail}\n`);
154
159
  process.exit(1);
155
160
  }
156
161
 
157
162
  const resultText = state.finalResult || state.lastMessageText || state.textFragments.join("\n\n");
158
163
  if (!resultText.trim()) {
159
- process.stderr.write("opencode produced no result text - refusing to fabricate a result\n");
164
+ const detail = "opencode produced no result text - refusing to fabricate a result";
165
+ persistStderr(resultPath, childStderr.trim() || detail);
166
+ process.stderr.write(`${detail}\n`);
160
167
  process.exit(1);
161
168
  }
162
169
 
163
170
  try {
164
171
  writeResult(resultPath, resultText);
165
172
  } catch (error) {
173
+ persistStderr(resultPath, `opencode produced no final result: ${error.message}`);
166
174
  process.stderr.write(`opencode produced no final result: ${error.message}\n`);
167
175
  process.exit(1);
168
176
  }
@@ -79,6 +79,16 @@ function main() {
79
79
  const lock = path.join(pluginRoot, "package-lock.json");
80
80
  if (fs.existsSync(lock) && replaceFirstVersionField(lock, next)) note("package-lock.json");
81
81
 
82
+ // 2b. Official MCP Registry server metadata (top-level server version + npm package version).
83
+ const serverJson = path.join(pluginRoot, "server.json");
84
+ if (fs.existsSync(serverJson)) {
85
+ const json = JSON.parse(fs.readFileSync(serverJson, "utf8"));
86
+ json.version = next;
87
+ if (json.packages && json.packages[0]) json.packages[0].version = next;
88
+ fs.writeFileSync(serverJson, `${JSON.stringify(json, null, 2)}\n`);
89
+ note("server.json");
90
+ }
91
+
82
92
  // 3. src/version.ts runtime constant
83
93
  const versionTs = path.join(pluginRoot, "src", "version.ts");
84
94
  const vtsText = fs.readFileSync(versionTs, "utf8");
@@ -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.94",
86
+ "Cool Workflow v0.1.96",
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.94");
120
+ assert.equal(summary.version, "0.1.96");
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.94");
127
+ assert.equal(shown.app.version, "0.1.96");
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.94");
138
+ assert.equal(state.workflow.app.version, "0.1.96");
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`);
@@ -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.94";
9
+ const TARGET_VERSION = "0.1.96";
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.94");
36
+ assert.equal(appValidation.summary.version, "0.1.96");
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.94 end-to-end golden path."
45
+ "Prove the deterministic v0.1.96 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.94");
55
+ assert.equal(state.workflow.app.version, "0.1.96");
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\.94/);
198
+ assert.match(report, /Workflow App: end-to-end-golden-path@0\.1\.96/);
199
199
  assert.match(report, /## Candidates/);
200
200
  assert.match(report, /## Trust Audit/);
201
201
  assert.match(report, /## Acceptance Rationale/);
@@ -7,6 +7,7 @@ const path = require("node:path");
7
7
 
8
8
  const pluginRoot = path.resolve(__dirname, "..");
9
9
  const repoRoot = path.resolve(pluginRoot, "..", "..");
10
+ const skipTests = process.argv.includes("--skip-tests") || process.env.CW_RELEASE_CHECK_SKIP_TESTS === "1";
10
11
  const checks = [
11
12
  {
12
13
  name: "docs presence",
@@ -87,6 +88,11 @@ function main() {
87
88
  process.stdout.write(`release:check ${check.name} ... `);
88
89
  const started = Date.now();
89
90
  try {
91
+ if (skipTests && check.name === "tests") {
92
+ results.push({ name: check.name, ok: true, skipped: true, elapsedMs: 0 });
93
+ process.stdout.write("skipped\n");
94
+ continue;
95
+ }
90
96
  if (check.run) check.run();
91
97
  else runCommand(check.command);
92
98
  const elapsedMs = Date.now() - started;
@@ -103,7 +109,7 @@ function main() {
103
109
  const failed = results.filter((entry) => !entry.ok);
104
110
  process.stdout.write("\nRelease Check Summary\n");
105
111
  for (const result of results) {
106
- process.stdout.write(`- ${result.ok ? "PASS" : "FAIL"} ${result.name}\n`);
112
+ process.stdout.write(`- ${result.skipped ? "SKIP" : result.ok ? "PASS" : "FAIL"} ${result.name}\n`);
107
113
  }
108
114
  process.stdout.write(`\nDry-run only: no tag, push, publish, or fixture mutation was requested.\n`);
109
115
  if (failed.length > 0) {
@@ -185,6 +185,11 @@ function buildReviewerInput(resultPath) {
185
185
  "or:",
186
186
  " REJECTED",
187
187
  " <numbered gate failures with file:line references>",
188
+ "",
189
+ "If your host wrapper asks for Markdown, JSON, or a fenced result block, still put",
190
+ "the exact APPROVED/REJECTED verdict line above as a standalone line outside any",
191
+ "prose or code fence. The release tool extracts only strict standalone verdict",
192
+ "lines; prose such as 'Verdict: APPROVED' is not trusted.",
188
193
  ""
189
194
  ].join("\n");
190
195
  }
@@ -250,14 +255,28 @@ function delegateReview(resultPath, inputPath) {
250
255
  };
251
256
 
252
257
  if (cfg.command) {
253
- const args = (cfg.args || []).map((a) => substitute(a, subMap));
254
- say(`[2/3] reviewer delegating to: ${cfg.command} ${(cfg.args || []).join(" ")} (model: ${cfg.model || "unreported"})`);
258
+ // cfg.command can arrive as a SINGLE unsplit multi-token string. A
259
+ // builtin:<vendor> template (CW_AGENT_COMMAND=builtin:claude) expands to
260
+ // "node /abs/claude-p-agent.js {{input}} {{result}}" with cfg.args undefined,
261
+ // and a durable agent-config.json command field is read verbatim (no split).
262
+ // Split it on whitespace into binary + inline args — the SAME way the
263
+ // orchestrator does (src/execution-backend/agent.ts resolveAgentInvocation) —
264
+ // so we spawn the real binary, never one literally named
265
+ // "node /abs/... {{input}}" (an instant ENOENT the old code mislabeled as a
266
+ // timeout). An explicit cfg.args array still follows the inline args. A plain
267
+ // CW_AGENT_COMMAND="claude -p {{input}}" is already pre-split by
268
+ // resolveAgentConfig, so this leaves it unchanged (no inline args).
269
+ const parts = cfg.command.split(/\s+/).filter(Boolean);
270
+ const bin = parts[0];
271
+ const argTemplate = [...parts.slice(1), ...(cfg.args || [])];
272
+ const args = argTemplate.map((a) => substitute(a, subMap));
273
+ say(`[2/3] reviewer — delegating to: ${bin} ${argTemplate.join(" ")} (model: ${cfg.model || "unreported"})`);
255
274
  // RED LINE: argv-style, shell:false. The agent runs the model in its own
256
275
  // process and inherits its own credentials; CW holds none.
257
276
  // Capture stdout so agents that print verdicts (rather than writing the
258
277
  // result file) are supported. Agents that DO write the file still work:
259
278
  // the file takes precedence. stderr goes to the terminal for live output.
260
- const r = spawnSync(cfg.command, args, {
279
+ const r = spawnSync(bin, args, {
261
280
  cwd: repoRoot,
262
281
  env: { ...process.env },
263
282
  encoding: "utf8",
@@ -266,6 +285,10 @@ function delegateReview(resultPath, inputPath) {
266
285
  stdio: ["ignore", "pipe", "inherit"],
267
286
  maxBuffer: 32 * 1024 * 1024
268
287
  });
288
+ // A spawn that never started (ENOENT for a missing binary, ETIMEDOUT on the
289
+ // deadline) sets r.error — surface its real message instead of the old
290
+ // catch-all "(timeout/no-exit)" that hid the builtin: ENOENT for so long.
291
+ if (r.error) die(`reviewer agent could not be spawned (${bin}): ${r.error.message} — no verdict trusted.`);
269
292
  if (r.status !== 0) die(`reviewer agent exited ${r.status === null ? "(timeout/no-exit)" : r.status} — no verdict trusted.`);
270
293
  // If the agent already wrote the verdict file (backward compat), use it.
271
294
  // Otherwise extract the verdict from stdout (headless agent path).
@@ -316,6 +339,14 @@ function verifyVerdict(resultPath) {
316
339
  const lines = text.split(/\r?\n/);
317
340
  const firstLine = lines[0] || "";
318
341
  if (firstLine !== `APPROVED ${HEAD}`) {
342
+ if (firstLine.toUpperCase() !== "REJECTED") {
343
+ const normalized = extractVerdictFromStdout(text, resultPath);
344
+ if (normalized && normalized.split(/\r?\n/)[0] === `APPROVED ${HEAD}`) {
345
+ fs.writeFileSync(resultPath, `${normalized}\n`);
346
+ const normalizedLines = normalized.split(/\r?\n/);
347
+ return (normalizedLines[1] || "").trim();
348
+ }
349
+ }
319
350
  die(`verdict first line must be exactly "APPROVED ${HEAD}" — release blocked.`, text.trim());
320
351
  }
321
352
  const cap = (lines[1] || "").trim();
@@ -472,6 +503,16 @@ function cut(resultPath, capability) {
472
503
  if (bump.status !== 0) die("bump:version failed");
473
504
  // Regenerate the gated project index after the version bump (PR #87 gate).
474
505
  spawnSync("npm", ["run", "sync:project-index", "--", "--repo-only"], { cwd: pluginRoot, stdio: "inherit" });
506
+ // Belt-and-suspenders: the reviewer agent writes a narration transcript into
507
+ // .cw-release/ that may carry local paths (the operator's home dir). It is
508
+ // .gitignored, but `git add -A` would still stage it if it were ever tracked,
509
+ // and a tracked transcript leaks PII into the immutable tag commit (it tripped
510
+ // pii-redaction-smoke and red-failed release-gate for v0.1.96). Remove any
511
+ // transcript before staging so it can never ride into the tag.
512
+ const releaseDir = path.join(repoRoot, ".cw-release");
513
+ for (const f of fs.existsSync(releaseDir) ? fs.readdirSync(releaseDir) : []) {
514
+ if (/^transcript.*\.md$/.test(f)) fs.rmSync(path.join(releaseDir, f), { force: true });
515
+ }
475
516
  git(["add", "-A"]);
476
517
  const commit = git(["commit", "-m", `chore(release): record APPROVED reviewer verdict for v${cutVersion}`]);
477
518
  if (commit.code !== 0) die("verdict commit failed", commit.err);
@@ -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
- CW_TEST_CONCURRENCY=1 npm test --prefix plugins/cool-workflow >/dev/null 2>&1 || fail "tests failed"
33
+ CW_TEST_CONCURRENCY=1 npm run test:gate --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"
@@ -60,6 +60,8 @@ function main() {
60
60
  // uses `npm install --no-package-lock`), so only validate it when present.
61
61
  checkJsonIfPresent("plugins/cool-workflow/package-lock.json", "version", VERSION, checks);
62
62
  checkJson("plugins/cool-workflow/.codex-plugin/plugin.json", "version", VERSION, checks);
63
+ checkJson("plugins/cool-workflow/server.json", "version", VERSION, checks);
64
+ checkNestedJson("plugins/cool-workflow/server.json", ["packages", 0, "version"], VERSION, checks);
63
65
  checkNestedJson("plugins/cool-workflow/manifest/plugin.manifest.json", ["identity", "version"], VERSION, checks);
64
66
  checkJson("plugins/cool-workflow/.claude-plugin/plugin.json", "version", VERSION, checks);
65
67
  checkJson("plugins/cool-workflow/.gemini-plugin/plugin.json", "version", VERSION, checks);