cool-workflow 0.2.5 → 0.2.6

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 (80) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/.gemini-plugin/plugin.json +1 -1
  4. package/.opencode-plugin/plugin.json +1 -1
  5. package/README.md +5 -3
  6. package/apps/architecture-review/app.json +1 -1
  7. package/apps/architecture-review-fast/app.json +1 -1
  8. package/apps/end-to-end-golden-path/app.json +1 -1
  9. package/apps/pr-review-fix-ci/app.json +1 -1
  10. package/apps/release-cut/app.json +1 -1
  11. package/apps/research-synthesis/app.json +1 -1
  12. package/dist/core/state/run-paths.js +3 -30
  13. package/dist/core/version.js +1 -1
  14. package/dist/mcp/server.js +95 -27
  15. package/dist/mcp/tool-process.js +181 -0
  16. package/dist/mcp-server.js +8 -1
  17. package/dist/shell/agent-config.js +11 -2
  18. package/dist/shell/audit-cli.js +1 -1
  19. package/dist/shell/drive.js +99 -89
  20. package/dist/shell/execution-backend/agent.js +18 -15
  21. package/dist/shell/execution-backend/local.js +1 -0
  22. package/dist/shell/fs-atomic.js +3 -0
  23. package/dist/shell/metrics-cli.js +1 -1
  24. package/dist/shell/multi-agent-cli.js +4 -1
  25. package/dist/shell/multi-agent-operator-ux.js +1 -1
  26. package/dist/shell/observability.js +7 -4
  27. package/dist/shell/operator-ux.js +1 -1
  28. package/dist/shell/perf-trace.js +136 -0
  29. package/dist/shell/pipeline.js +1 -1
  30. package/dist/shell/report-view-cli.js +2 -0
  31. package/dist/shell/run-export-cli.js +5 -2
  32. package/dist/shell/run-export.js +241 -27
  33. package/dist/shell/run-registry-io.js +8 -0
  34. package/dist/shell/run-store.js +27 -3
  35. package/dist/shell/state-explosion-cli.js +4 -1
  36. package/dist/shell/telemetry-demo.js +1 -1
  37. package/dist/shell/trust-audit.js +91 -43
  38. package/dist/shell/workbench.js +4 -1
  39. package/dist/wiring/capability-table/parity.js +10 -5
  40. package/docs/agent-delegation-drive.7.md +2 -0
  41. package/docs/cli-mcp-parity.7.md +21 -5
  42. package/docs/contract-migration-tooling.7.md +2 -0
  43. package/docs/control-plane-scheduling.7.md +2 -0
  44. package/docs/durable-state-and-locking.7.md +2 -0
  45. package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
  46. package/docs/execution-backends.7.md +2 -0
  47. package/docs/mcp-app-surface.7.md +21 -0
  48. package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
  49. package/docs/multi-agent-eval-replay-harness.7.md +2 -0
  50. package/docs/multi-agent-operator-ux.7.md +2 -0
  51. package/docs/node-snapshot-diff-replay.7.md +2 -0
  52. package/docs/observability-cost-accounting.7.md +2 -0
  53. package/docs/project-index.md +18 -4
  54. package/docs/real-execution-backends.7.md +2 -0
  55. package/docs/release-and-migration.7.md +2 -0
  56. package/docs/release-tooling.7.md +10 -0
  57. package/docs/run-registry-control-plane.7.md +35 -9
  58. package/docs/run-retention-reclamation.7.md +2 -0
  59. package/docs/state-explosion-management.7.md +2 -0
  60. package/docs/team-collaboration.7.md +2 -0
  61. package/docs/trust-audit-anchor.7.md +2 -0
  62. package/docs/web-desktop-workbench.7.md +48 -0
  63. package/manifest/plugin.manifest.json +1 -1
  64. package/package.json +3 -2
  65. package/scripts/canonical-apps.js +4 -4
  66. package/scripts/dogfood-release.js +1 -1
  67. package/scripts/gen-manifests.js +32 -61
  68. package/scripts/golden-path.js +4 -4
  69. package/scripts/parity-check.js +42 -23
  70. package/scripts/purity-baseline.json +0 -3
  71. package/scripts/release-flow.js +43 -13
  72. package/scripts/release-gate.js +30 -6
  73. package/scripts/schema-version-inventory.json +31 -0
  74. package/scripts/validate-run-state-schema.js +95 -4
  75. package/scripts/verify-release-verdict.js +139 -0
  76. package/ui/workbench/app.css +15 -7
  77. package/ui/workbench/app.js +118 -16
  78. package/ui/workbench/index.html +2 -0
  79. package/ui/workbench/inspection.js +51 -0
  80. package/ui/workbench/navigation.js +44 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cool-workflow",
3
3
  "description": "A workflow control plane and run-time you are able to check: it sends out jobs in TypeScript, makes certain of work against facts before it goes through, puts state into fixed records, orders jobs by time, runs jobs again and again, gets a group of agents to do their parts together, and talks MCP. It gives the doing of the work to outside agents — it never runs the models itself.",
4
- "version": "0.2.5",
4
+ "version": "0.2.6",
5
5
  "author": {
6
6
  "name": "COOLWHITE LLC"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cool-workflow",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "A workflow control plane and run-time you are able to check: it sends out jobs in TypeScript, makes certain of work against facts before it goes through, puts state into fixed records, orders jobs by time, runs jobs again and again, gets a group of agents to do their parts together, and talks MCP. It gives the doing of the work to outside agents — it never runs the models itself.",
5
5
  "author": {
6
6
  "name": "COOLWHITE LLC"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cool-workflow",
3
3
  "description": "A workflow control plane and run-time you are able to check: it sends out jobs in TypeScript, makes certain of work against facts before it goes through, puts state into fixed records, orders jobs by time, runs jobs again and again, gets a group of agents to do their parts together, and talks MCP. It gives the doing of the work to outside agents — it never runs the models itself.",
4
- "version": "0.2.5",
4
+ "version": "0.2.6",
5
5
  "author": {
6
6
  "name": "COOLWHITE LLC"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cool-workflow",
3
3
  "description": "A workflow control plane and run-time you are able to check: it sends out jobs in TypeScript, makes certain of work against facts before it goes through, puts state into fixed records, orders jobs by time, runs jobs again and again, gets a group of agents to do their parts together, and talks MCP. It gives the doing of the work to outside agents — it never runs the models itself.",
4
- "version": "0.2.5",
4
+ "version": "0.2.6",
5
5
  "author": {
6
6
  "name": "COOLWHITE LLC"
7
7
  },
package/README.md CHANGED
@@ -95,9 +95,11 @@ cw -q "How does auth work end-to-end here?"
95
95
  Any question works — "how does X work", "is it safe to change Y", "what would
96
96
  break if Z rotated". CW uses the current repo and the first agent it finds on
97
97
  your `PATH`. Want a specific agent? Add a flag (`-claude`, `-codex`, `-gemini`,
98
- `-opencode`, `-deepseek`). DeepSeek is the one with no auto-detect — it runs
99
- through `opencode` so `-deepseek` is the only way to get it. You can
100
- point CW at any folder, or at a **repo on the web by
98
+ `-opencode`, `-deepseek`). Note: the `-gemini` and `-deepseek` flags reach
99
+ their models through `opencode`, so those two need `opencode` installed and
100
+ its provider signed in. DeepSeek also has no auto-detect, so `-deepseek` is
101
+ the only way to get it. (A native `gemini` CLI found on your `PATH` is used
102
+ directly.) You can point CW at any folder, or at a **repo on the web by
101
103
  URL** — CW clones it and reviews the copy:
102
104
 
103
105
  ```bash
@@ -3,7 +3,7 @@
3
3
  "id": "architecture-review",
4
4
  "title": "Architecture Review",
5
5
  "summary": "Map a repository architecture, assess risks, verify important findings, and synthesize an evidence-backed verdict.",
6
- "version": "0.2.5",
6
+ "version": "0.2.6",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "architecture-review-fast",
4
4
  "title": "Architecture Review Fast",
5
5
  "summary": "Run a shorter architecture review with parallel map and assess phases for faster first results.",
6
- "version": "0.2.5",
6
+ "version": "0.2.6",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "end-to-end-golden-path",
4
4
  "title": "End-to-End Golden Path",
5
5
  "summary": "Deterministic one-worker workflow app for proving the CW integration chain.",
6
- "version": "0.2.5",
6
+ "version": "0.2.6",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "pr-review-fix-ci",
4
4
  "title": "PR Review Fix CI",
5
5
  "summary": "Review a pull request or branch, inspect CI failures, diagnose actionable issues, optionally patch, verify, and summarize with evidence.",
6
- "version": "0.2.5",
6
+ "version": "0.2.6",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "release-cut",
4
4
  "title": "Release Cut",
5
5
  "summary": "Prepare a release with checklist discipline: version checks, changelog, tests, packaging, release notes, and final verification.",
6
- "version": "0.2.5",
6
+ "version": "0.2.6",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "research-synthesis",
4
4
  "title": "Research Synthesis",
5
5
  "summary": "Split a research question into claims, investigate sources, cross-check evidence, verify claims, and synthesize a concise answer.",
6
- "version": "0.2.5",
6
+ "version": "0.2.6",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -1,10 +1,9 @@
1
1
  "use strict";
2
- // core/state/run-paths.ts — createRunPaths, ensureRunDirs.
2
+ // core/state/run-paths.ts — pure RunPaths construction.
3
3
  //
4
4
  // MILESTONE 3. Byte-exact port of the old build's src/state.ts:10-50.
5
- // `createRunPaths` is pure path math (core/); `ensureRunDirs` touches disk
6
- // (mkdirSync) so it lives here but is called from shell/run-store.ts, the
7
- // only place that actually creates a run directory tree.
5
+ // `createRunPaths` is pure path math. The directory write mechanism lives in
6
+ // shell/run-store.ts.
8
7
  //
9
8
  // Evidence: SPEC/state-core.md "src/state.ts — persistence kernel" and "The
10
9
  // full .cw/runs/<run-id>/ layout".
@@ -43,8 +42,6 @@ var __importStar = (this && this.__importStar) || (function () {
43
42
  })();
44
43
  Object.defineProperty(exports, "__esModule", { value: true });
45
44
  exports.createRunPaths = createRunPaths;
46
- exports.ensureRunDirs = ensureRunDirs;
47
- const fs = __importStar(require("node:fs"));
48
45
  const path = __importStar(require("node:path"));
49
46
  /** The 16-key RunPaths object, all joined under `runDir`. */
50
47
  function createRunPaths(runDir) {
@@ -67,27 +64,3 @@ function createRunPaths(runDir) {
67
64
  topologiesDir: path.join(runDir, "topologies"),
68
65
  };
69
66
  }
70
- /** `mkdirSync` (recursive) every dir this run needs. Missing optional dir
71
- * fields fall back to `path.join(runDir, "<name>")`, matching the old
72
- * build's defensive default (a RunPaths loaded from an old/partial
73
- * state.json may be missing an optional key). */
74
- function ensureRunDirs(paths) {
75
- const dirs = [
76
- paths.runDir,
77
- paths.tasksDir,
78
- paths.resultsDir,
79
- paths.dispatchesDir,
80
- paths.artifactsDir,
81
- paths.commitsDir,
82
- paths.stateNodesDir,
83
- paths.feedbackDir,
84
- paths.auditDir || path.join(paths.runDir, "audit"),
85
- paths.workersDir || path.join(paths.runDir, "workers"),
86
- paths.candidatesDir || path.join(paths.runDir, "candidates"),
87
- paths.multiAgentDir || path.join(paths.runDir, "multi-agent"),
88
- paths.blackboardDir || path.join(paths.runDir, "blackboard"),
89
- paths.topologiesDir || path.join(paths.runDir, "topologies"),
90
- ];
91
- for (const dir of dirs)
92
- fs.mkdirSync(dir, { recursive: true });
93
- }
@@ -7,7 +7,7 @@ exports.MIN_SUPPORTED_RUN_STATE_SCHEMA_VERSION = exports.LEGACY_RUN_STATE_SCHEMA
7
7
  // `cw version` output does not change bytes just because the code under it
8
8
  // was rebuilt. See SPEC/cli-surface.md "Exact outputs > Version" and
9
9
  // conformance/cases/version-basic.case.js (regex `/^\d+\.\d+\.\d+\n$/`).
10
- exports.CURRENT_COOL_WORKFLOW_VERSION = "0.2.5";
10
+ exports.CURRENT_COOL_WORKFLOW_VERSION = "0.2.6";
11
11
  // State-kernel schema version constants (SPEC/state-core.md "Version
12
12
  // constants"). Pinned to the old build's src/version.ts byte-for-byte.
13
13
  exports.WORKFLOW_APP_SCHEMA_VERSION = 1;
@@ -63,12 +63,13 @@
63
63
  // path. The envelope-level "missing field: name" check (right above
64
64
  // this bullet) is unchanged — it still answers -32000.
65
65
  Object.defineProperty(exports, "__esModule", { value: true });
66
+ exports.mcpToolAuthority = mcpToolAuthority;
66
67
  exports.negotiateProtocolVersion = negotiateProtocolVersion;
67
68
  exports.startServer = startServer;
68
69
  const version_1 = require("../core/version");
69
- const safe_json_1 = require("../core/format/safe-json");
70
70
  const recovery_hint_1 = require("../core/format/recovery-hint");
71
71
  const dispatch_1 = require("./dispatch");
72
+ const tool_process_1 = require("./tool-process");
72
73
  const MAX_LINE_BYTES = 16 * 1024 * 1024;
73
74
  /** Protocol versions this server can speak, oldest first. `initialize`
74
75
  * echoes the client's `params.protocolVersion` when it is in this list,
@@ -77,6 +78,41 @@ const MAX_LINE_BYTES = 16 * 1024 * 1024;
77
78
  * behavior-identical to the old hard-coded reply (mechanism first; a
78
79
  * second version is a one-line append here). */
79
80
  const SUPPORTED_PROTOCOL_VERSIONS = ["2024-11-05"];
81
+ /** Read the optional server-side tool policy. When both values are unset, the
82
+ * full present tool list and access stay unchanged. An enabled list is an
83
+ * allowlist; disabled names are then removed. */
84
+ function mcpToolAuthority(definitions = (0, dispatch_1.toolDefinitions)(), environment = process.env) {
85
+ const known = new Set(definitions.map((definition) => definition.name));
86
+ const enabled = configuredToolNames("CW_MCP_ENABLED_TOOLS", environment, known);
87
+ const disabled = configuredToolNames("CW_MCP_DISABLED_TOOLS", environment, known);
88
+ if (!enabled && !disabled)
89
+ return {};
90
+ const allowed = enabled ? new Set(enabled) : new Set(known);
91
+ for (const name of disabled ?? [])
92
+ allowed.delete(name);
93
+ return { allowed };
94
+ }
95
+ function configuredToolNames(name, environment, known) {
96
+ const value = environment[name];
97
+ if (value === undefined)
98
+ return undefined;
99
+ const names = value.split(",").map((part) => part.trim());
100
+ if (names.some((tool) => tool.length === 0))
101
+ throw new Error(`MCP tool policy ${name} contains an empty tool name`);
102
+ const selected = new Set(names);
103
+ for (const tool of selected) {
104
+ if (!known.has(tool))
105
+ throw new Error(`MCP tool policy ${name} names an unknown tool: ${tool}`);
106
+ }
107
+ return selected;
108
+ }
109
+ function permittedToolDefinitions(authority) {
110
+ const definitions = (0, dispatch_1.toolDefinitions)();
111
+ return authority.allowed ? definitions.filter((definition) => authority.allowed?.has(definition.name)) : definitions;
112
+ }
113
+ function toolPermitted(name, authority) {
114
+ return authority.allowed === undefined || authority.allowed.has(name);
115
+ }
80
116
  /** Picks the `initialize` reply's protocolVersion from the client's
81
117
  * requested one (see SUPPORTED_PROTOCOL_VERSIONS). Exported for the
82
118
  * protocol-version smoke; pure. */
@@ -154,18 +190,9 @@ function resultMessage(id, result) {
154
190
  // MUST NOT be answered) pinned by SPEC/mcp.md's edge-cases. Revisit only
155
191
  // as a deliberate spec change, not as a drive-by fix.
156
192
  }
157
- /** Handles one already-parsed JSON-RPC request object. May write zero or
158
- * one reply line to stdout. `await`ing callTool's result is a no-op for
159
- * the ~197 tools whose handler returns a plain value already (an `await`
160
- * on a non-Promise resolves on the next microtask, invisible to a
161
- * caller that already awaits handleRequest) -- it only matters for
162
- * `cw_run`, whose live drive loop returns a real Promise so it can
163
- * actually stay interruptible (see shell/drive.ts's driveAsync). This
164
- * must stay inside the existing try/catch: an async tool handler throws
165
- * by REJECTING its returned Promise rather than throwing synchronously,
166
- * and an unawaited rejection here would be an unhandled rejection
167
- * instead of the normal `-32000` JSON-RPC error reply. */
168
- async function handleRequest(message) {
193
+ /** Handles one already-parsed JSON-RPC request object. The parent owns the
194
+ * protocol reply while the durable tool process owns the blocking tool work. */
195
+ async function handleRequest(message, tools, authority) {
169
196
  const hasId = Object.prototype.hasOwnProperty.call(message, "id");
170
197
  const id = message.id;
171
198
  if (typeof message.method !== "string") {
@@ -187,7 +214,7 @@ async function handleRequest(message) {
187
214
  return;
188
215
  }
189
216
  case "tools/list": {
190
- writeMessage(resultMessage(id, { tools: (0, dispatch_1.toolDefinitions)() }));
217
+ writeMessage(resultMessage(id, { tools: permittedToolDefinitions(authority) }));
191
218
  return;
192
219
  }
193
220
  case "ping": {
@@ -221,8 +248,10 @@ async function handleRequest(message) {
221
248
  // a tool-call outcome, and keeps going through the outer
222
249
  // try/catch as a -32000 error, unchanged.
223
250
  try {
224
- const coreResult = await (0, dispatch_1.callTool)(name, args ?? {});
225
- const content = [{ type: "text", text: (0, safe_json_1.safeJsonStringify)(coreResult) }];
251
+ if (!toolPermitted(name, authority))
252
+ throw new Error(`MCP tool disabled by policy: ${name}`);
253
+ const text = await tools.execute(name, args ?? {});
254
+ const content = [{ type: "text", text }];
226
255
  const advisory = untrustedContentAdvisory(name);
227
256
  if (advisory)
228
257
  content.push({ type: "text", text: advisory });
@@ -248,22 +277,28 @@ async function handleRequest(message) {
248
277
  writeMessage(errorMessage(id, -32000, text));
249
278
  }
250
279
  }
251
- /** Handles one raw (already-trimmed, non-empty) stdin line. */
252
- async function handleLine(line) {
280
+ /** Parses a raw stdin line without writing. This lets a valid ping use the
281
+ * control path while parse errors keep their old place in the work queue. */
282
+ function parseLine(line) {
253
283
  let parsed;
254
284
  try {
255
285
  parsed = JSON.parse(line);
256
286
  }
257
287
  catch (error) {
258
288
  const detail = error instanceof Error ? error.message : String(error);
259
- writeMessage(errorMessage(null, -32700, `Parse error: ${detail}`));
260
- return;
289
+ return { error: { code: -32700, message: `Parse error: ${detail}` } };
261
290
  }
262
291
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
263
- writeMessage(errorMessage(null, -32600, "Invalid Request: not a JSON-RPC object"));
292
+ return { error: { code: -32600, message: "Invalid Request: not a JSON-RPC object" } };
293
+ }
294
+ return { message: parsed };
295
+ }
296
+ async function handleLine(parsed, tools, authority) {
297
+ if ("error" in parsed) {
298
+ writeMessage(errorMessage(null, parsed.error.code, parsed.error.message));
264
299
  return;
265
300
  }
266
- await handleRequest(parsed);
301
+ await handleRequest(parsed.message, tools, authority);
267
302
  }
268
303
  /** Starts the stdio read loop. Never resolves — the server is long-lived
269
304
  * and stops only when its stdin closes / the process exits.
@@ -277,7 +312,24 @@ async function handleLine(line) {
277
312
  * answered in handleRequest's fast path before any tool work, so a
278
313
  * keep-alive ping still gets a reply while a long drive holds the queue. */
279
314
  function startServer() {
315
+ const authority = mcpToolAuthority();
280
316
  process.stdin.setEncoding("utf8");
317
+ const tools = new tool_process_1.ToolProcessExecutor();
318
+ // EOF is a normal protocol close: keep the queue alive so a client which
319
+ // sent a batch and then closed stdin still gets its ordered replies. A
320
+ // process stop is different. Stop the child before giving the same signal
321
+ // back to the parent, so a child in a synchronous lock wait cannot wake up
322
+ // later and write state after its MCP parent is gone.
323
+ const stopForSignal = (signal) => {
324
+ const stop = () => {
325
+ tools.close();
326
+ process.removeListener(signal, stop);
327
+ process.kill(process.pid, signal);
328
+ };
329
+ process.once(signal, stop);
330
+ };
331
+ stopForSignal("SIGINT");
332
+ stopForSignal("SIGTERM");
281
333
  let buffer = "";
282
334
  // True while the REST of an oversize line is still streaming in: emit one
283
335
  // -32700 for the whole line and skip everything up to its terminating
@@ -325,8 +377,20 @@ function startServer() {
325
377
  const line = buffer.slice(0, newlineIndex);
326
378
  buffer = buffer.slice(newlineIndex + 1);
327
379
  const trimmed = line.trim();
328
- if (trimmed)
329
- enqueue(() => handleLine(trimmed));
380
+ if (!trimmed)
381
+ continue;
382
+ const parsed = parseLine(trimmed);
383
+ // Ping is a control-plane keep-alive. It must not wait behind a tool
384
+ // process that is blocked on a file lock or an outside agent.
385
+ if ("message" in parsed && parsed.message.method === "ping") {
386
+ void handleLine(parsed, tools, authority).catch((error) => {
387
+ const detail = error instanceof Error ? (error.stack ?? error.message) : String(error);
388
+ process.stderr.write(`cool-workflow mcp: a ping reply failed: ${detail}\n`);
389
+ });
390
+ }
391
+ else {
392
+ enqueue(() => handleLine(parsed, tools, authority));
393
+ }
330
394
  }
331
395
  // No newline yet and the pending (unterminated) line already exceeds the
332
396
  // cap: report ONCE, drop the head, and discard the rest of this line
@@ -335,9 +399,13 @@ function startServer() {
335
399
  if (!discarding && buffer.length > MAX_LINE_BYTES) {
336
400
  buffer = "";
337
401
  discarding = true;
338
- enqueue(() => {
339
- writeMessage(errorMessage(null, -32700, `Parse error: request line exceeds ${MAX_LINE_BYTES} bytes`));
340
- });
402
+ enqueue(() => writeMessage(errorMessage(null, -32700, `Parse error: request line exceeds ${MAX_LINE_BYTES} bytes`)));
341
403
  }
342
404
  });
405
+ // A client may send a batch and close stdin at once. Keep the tool process
406
+ // alive until the serial queue has written those replies, then close it so
407
+ // it cannot outlive the MCP parent.
408
+ process.stdin.on("end", () => {
409
+ void queue.finally(() => tools.close());
410
+ });
343
411
  }
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+ // mcp/tool-process.ts — the private execution lane for MCP tools.
3
+ //
4
+ // The MCP server keeps its stdio protocol work in the parent process. This
5
+ // child process runs one tools/call at a time, so a synchronous file-lock wait
6
+ // or a long agent call cannot stop the parent from reading and answering ping.
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.ToolProcessExecutor = void 0;
42
+ exports.startToolProcessWorker = startToolProcessWorker;
43
+ const node_child_process_1 = require("node:child_process");
44
+ const path = __importStar(require("node:path"));
45
+ const safe_json_1 = require("../core/format/safe-json");
46
+ const dispatch_1 = require("./dispatch");
47
+ function isResponse(value) {
48
+ if (!value || typeof value !== "object")
49
+ return false;
50
+ const record = value;
51
+ return record.schemaVersion === 1 && typeof record.id === "number" && typeof record.ok === "boolean" &&
52
+ (record.ok ? typeof record.text === "string" : typeof record.error === "string");
53
+ }
54
+ function diagnostic(kind, chunk) {
55
+ const text = String(chunk);
56
+ if (text)
57
+ process.stderr.write(`cool-workflow mcp tool process ${kind}: ${text}`);
58
+ }
59
+ /** One durable, serial child process for tools/call. The public MCP server
60
+ * owns response shapes; this class moves only a tool request and its already
61
+ * rendered JSON text across IPC. A stopped child never causes an automatic
62
+ * retry because a tool may already have written part of its state. */
63
+ class ToolProcessExecutor {
64
+ workerPath;
65
+ child;
66
+ nextId = 1;
67
+ pending;
68
+ constructor(options = {}) {
69
+ this.workerPath = options.workerPath ?? path.join(__dirname, "tool-process.js");
70
+ }
71
+ execute(name, args) {
72
+ if (this.pending)
73
+ throw new Error("MCP tool process received concurrent work");
74
+ const child = this.ensureChild();
75
+ const id = this.nextId++;
76
+ const request = { schemaVersion: 1, id, name, args };
77
+ return new Promise((resolve, reject) => {
78
+ this.pending = { id, name, resolve, reject };
79
+ try {
80
+ child.send(request, (error) => {
81
+ if (!error || this.pending?.id !== id)
82
+ return;
83
+ this.pending = undefined;
84
+ reject(error);
85
+ });
86
+ }
87
+ catch (error) {
88
+ if (this.pending?.id === id)
89
+ this.pending = undefined;
90
+ reject(error instanceof Error ? error : new Error(String(error)));
91
+ }
92
+ });
93
+ }
94
+ close() {
95
+ const child = this.child;
96
+ this.child = undefined;
97
+ const pending = this.pending;
98
+ this.pending = undefined;
99
+ if (pending)
100
+ pending.reject(new Error(`MCP tool process stopped before ${pending.name} ended`));
101
+ if (!child)
102
+ return;
103
+ if (child.connected)
104
+ child.disconnect();
105
+ child.kill();
106
+ }
107
+ ensureChild() {
108
+ if (this.child && !this.child.killed && this.child.connected)
109
+ return this.child;
110
+ const child = (0, node_child_process_1.fork)(this.workerPath, [], {
111
+ cwd: process.cwd(),
112
+ env: process.env,
113
+ stdio: ["ignore", "pipe", "pipe", "ipc"],
114
+ serialization: "json",
115
+ });
116
+ child.stdout?.on("data", (chunk) => diagnostic("stdout", chunk));
117
+ child.stderr?.on("data", (chunk) => diagnostic("stderr", chunk));
118
+ child.on("message", (message) => this.handleMessage(child, message));
119
+ child.on("error", (error) => this.handleStopped(child, error));
120
+ child.on("exit", () => this.handleStopped(child));
121
+ this.child = child;
122
+ return child;
123
+ }
124
+ handleMessage(child, message) {
125
+ if (child !== this.child || !isResponse(message)) {
126
+ this.handleStopped(child, new Error("MCP tool process sent an invalid response"));
127
+ child.kill();
128
+ return;
129
+ }
130
+ const pending = this.pending;
131
+ if (!pending || pending.id !== message.id) {
132
+ this.handleStopped(child, new Error("MCP tool process sent an unexpected response"));
133
+ child.kill();
134
+ return;
135
+ }
136
+ this.pending = undefined;
137
+ if (message.ok)
138
+ pending.resolve(message.text);
139
+ else
140
+ pending.reject(new Error(message.error));
141
+ }
142
+ handleStopped(child, error) {
143
+ if (child !== this.child)
144
+ return;
145
+ this.child = undefined;
146
+ const pending = this.pending;
147
+ this.pending = undefined;
148
+ if (pending) {
149
+ pending.reject(error ?? new Error(`MCP tool process stopped before ${pending.name} ended`));
150
+ }
151
+ }
152
+ }
153
+ exports.ToolProcessExecutor = ToolProcessExecutor;
154
+ function isRequest(value) {
155
+ if (!value || typeof value !== "object")
156
+ return false;
157
+ const record = value;
158
+ return record.schemaVersion === 1 && typeof record.id === "number" && typeof record.name === "string";
159
+ }
160
+ function send(message) {
161
+ if (process.send)
162
+ process.send(message);
163
+ }
164
+ /** Child entry point. Kept in this module so the parent and child share one
165
+ * small, checked IPC shape. */
166
+ function startToolProcessWorker() {
167
+ process.on("disconnect", () => process.exit(0));
168
+ process.on("message", async (message) => {
169
+ if (!isRequest(message))
170
+ return;
171
+ try {
172
+ const result = await (0, dispatch_1.callTool)(message.name, message.args);
173
+ send({ schemaVersion: 1, id: message.id, ok: true, text: (0, safe_json_1.safeJsonStringify)(result) });
174
+ }
175
+ catch (error) {
176
+ send({ schemaVersion: 1, id: message.id, ok: false, error: error instanceof Error ? error.message : String(error) });
177
+ }
178
+ });
179
+ }
180
+ if (require.main === module)
181
+ startToolProcessWorker();
@@ -26,4 +26,11 @@ function exitQuietOnEpipe(stream) {
26
26
  }
27
27
  exitQuietOnEpipe(process.stdout);
28
28
  exitQuietOnEpipe(process.stderr);
29
- (0, server_1.startServer)();
29
+ try {
30
+ (0, server_1.startServer)();
31
+ }
32
+ catch (error) {
33
+ const text = error instanceof Error ? error.message : String(error);
34
+ process.stderr.write(`cool-workflow mcp: ${text}\n`);
35
+ process.exitCode = 1;
36
+ }
@@ -246,10 +246,19 @@ function resolveAgentConfig(args = {}, env = process.env) {
246
246
  if (!finalCommand && !endpoint) {
247
247
  const detected = detectAgentFromPath(env);
248
248
  if (detected) {
249
- finalCommand = expandBuiltinAgentCommand(`builtin:${detected}`);
249
+ // Detect = use what was found. Auto-detect saw a BINARY on PATH, so it
250
+ // must resolve to the wrapper that runs THAT binary. For "gemini" the
251
+ // plain builtin:gemini template goes through opencode instead (a user
252
+ // choice kept for the explicit -gemini flag, where --help says "via
253
+ // opencode") — a machine with only the native gemini CLI would be
254
+ // detected as ready and then fail when the wrapper spawns a missing
255
+ // opencode. So the auto path maps gemini to builtin:gemini-cli, the
256
+ // wrapper that spawns the found binary itself.
257
+ const template = detected === "gemini" ? "gemini-cli" : detected;
258
+ finalCommand = expandBuiltinAgentCommand(`builtin:${template}`);
250
259
  finalSource = "auto";
251
260
  if (!finalModel)
252
- finalModel = `builtin:${detected}`;
261
+ finalModel = `builtin:${template}`;
253
262
  }
254
263
  }
255
264
  return {
@@ -160,7 +160,7 @@ function auditRepairCli(runId, args) {
160
160
  * data can never drift from the standalone commands' output. */
161
161
  function auditSummaryCli(runId, args) {
162
162
  const run = (0, run_store_1.loadRunFromCwd)(runId, invocationCwd(args));
163
- return (0, trust_audit_1.summarizeTrustAudit)(run);
163
+ return (0, trust_audit_1.summarizeTrustAudit)(run, { persist: args.__cwWorkbenchReadOnlyProjection !== true });
164
164
  }
165
165
  function auditMultiAgentCli(runId, args) {
166
166
  const run = (0, run_store_1.loadRunFromCwd)(runId, invocationCwd(args));