cool-workflow 0.1.81 → 0.1.83

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 (102) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/.codex-plugin/plugin.json +4 -4
  3. package/README.md +125 -121
  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/candidate-scoring.js +20 -26
  11. package/dist/capability-core.js +80 -93
  12. package/dist/capability-registry.js +30 -3
  13. package/dist/cli.js +12 -1
  14. package/dist/commit.js +217 -204
  15. package/dist/coordinator/util.js +6 -9
  16. package/dist/dispatch.js +11 -3
  17. package/dist/doctor.js +153 -0
  18. package/dist/evidence-reasoning.js +4 -1
  19. package/dist/execution-backend/agent.js +11 -48
  20. package/dist/execution-backend.js +11 -31
  21. package/dist/gates.js +48 -0
  22. package/dist/mcp-server.js +11 -0
  23. package/dist/multi-agent/helpers.js +6 -10
  24. package/dist/multi-agent/ids.js +20 -0
  25. package/dist/multi-agent-eval.js +27 -1
  26. package/dist/multi-agent-host.js +53 -21
  27. package/dist/multi-agent-operator-ux.js +2 -1
  28. package/dist/multi-agent-trust.js +5 -5
  29. package/dist/node-projection.js +59 -0
  30. package/dist/node-snapshot.js +8 -18
  31. package/dist/orchestrator/lifecycle-operations.js +22 -1
  32. package/dist/orchestrator.js +29 -2
  33. package/dist/reclamation/hash.js +72 -0
  34. package/dist/reclamation.js +31 -112
  35. package/dist/run-registry/queue.js +6 -7
  36. package/dist/run-registry.js +35 -24
  37. package/dist/scheduler.js +112 -57
  38. package/dist/topology.js +25 -4
  39. package/dist/trust-audit.js +70 -38
  40. package/dist/validation.js +328 -0
  41. package/dist/version.js +1 -1
  42. package/dist/worker-isolation.js +163 -58
  43. package/docs/agent-delegation-drive.7.md +90 -85
  44. package/docs/agent-framework.md +33 -32
  45. package/docs/candidate-scoring.7.md +26 -24
  46. package/docs/canonical-workflow-apps.7.md +40 -40
  47. package/docs/capability-topology-registry.7.md +24 -24
  48. package/docs/cli-mcp-parity.7.md +227 -154
  49. package/docs/contract-migration-tooling.7.md +49 -41
  50. package/docs/control-plane-scheduling.7.md +45 -40
  51. package/docs/coordinator-blackboard.7.md +30 -30
  52. package/docs/dogfood-one-real-repo.7.md +44 -44
  53. package/docs/durable-state-and-locking.7.md +35 -30
  54. package/docs/end-to-end-golden-path.7.md +29 -29
  55. package/docs/error-feedback.7.md +27 -27
  56. package/docs/evidence-adoption-reasoning-chain.7.md +63 -58
  57. package/docs/execution-backends.7.md +84 -79
  58. package/docs/getting-started.md +35 -18
  59. package/docs/index.md +3 -3
  60. package/docs/mcp-app-surface.7.md +64 -64
  61. package/docs/multi-agent-cli-mcp-surface.7.md +83 -77
  62. package/docs/multi-agent-eval-replay-harness.7.md +59 -54
  63. package/docs/multi-agent-operator-ux.7.md +70 -65
  64. package/docs/multi-agent-runtime-core.7.md +39 -39
  65. package/docs/multi-agent-topologies.7.md +24 -24
  66. package/docs/multi-agent-trust-policy-audit.7.md +38 -38
  67. package/docs/node-snapshot-diff-replay.7.md +26 -21
  68. package/docs/observability-cost-accounting.7.md +50 -45
  69. package/docs/operator-ux.7.md +30 -30
  70. package/docs/pipeline-runner.7.md +31 -31
  71. package/docs/project-index.md +15 -6
  72. package/docs/real-execution-backends.7.md +47 -42
  73. package/docs/release-and-migration.7.md +43 -38
  74. package/docs/release-tooling.7.md +74 -39
  75. package/docs/routines.md +16 -16
  76. package/docs/run-registry-control-plane.7.md +120 -115
  77. package/docs/run-retention-reclamation.7.md +46 -41
  78. package/docs/sandbox-profiles.7.md +32 -32
  79. package/docs/scheduled-tasks.md +14 -14
  80. package/docs/security-trust-hardening.7.md +29 -29
  81. package/docs/source-context-profiles.7.md +28 -28
  82. package/docs/state-explosion-management.7.md +64 -59
  83. package/docs/state-node.7.md +8 -8
  84. package/docs/team-collaboration.7.md +63 -58
  85. package/docs/trust-model.md +126 -126
  86. package/docs/unix-principles.md +80 -80
  87. package/docs/vendor-manifest-loadability.7.md +20 -20
  88. package/docs/verifier-gated-commit.7.md +16 -16
  89. package/docs/web-desktop-workbench.7.md +70 -65
  90. package/docs/worker-isolation.7.md +34 -37
  91. package/docs/workflow-app-framework.7.md +38 -38
  92. package/manifest/plugin.manifest.json +4 -4
  93. package/manifest/source-context-profiles.json +1 -1
  94. package/package.json +3 -2
  95. package/scripts/canonical-apps.js +4 -4
  96. package/scripts/children/batch-delegate-child.js +58 -0
  97. package/scripts/children/http-delegate-child.js +39 -0
  98. package/scripts/dogfood-release.js +1 -1
  99. package/scripts/gen-parity-doc.js +106 -0
  100. package/scripts/golden-path.js +4 -4
  101. package/scripts/release-flow.js +181 -5
  102. package/dist/verifier-registry.js +0 -46
package/dist/doctor.js ADDED
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runDoctor = runDoctor;
7
+ exports.formatDoctorReport = formatDoctorReport;
8
+ // `cw doctor` — environment diagnostics, in the spirit of `brew doctor`.
9
+ //
10
+ // Homebrew's `doctor` turned "something is subtly wrong with your setup" into a
11
+ // proactive, named list of problems each paired with a concrete fix. We borrow
12
+ // that idea: instead of letting a missing agent / old Node / unwritable state
13
+ // surface as a confusing mid-run failure, `cw doctor` probes the host up front
14
+ // and prints WHAT is wrong and WHAT TO DO about it.
15
+ //
16
+ // Discipline:
17
+ // - READ-ONLY. Probes versions, $PATH, and the WRITABILITY of the nearest
18
+ // existing ancestor dir (via access(2)) — it never creates `.cw/` or $CW_HOME
19
+ // as a side effect, so running `doctor` changes nothing on disk.
20
+ // - FAIL CLOSED. Any `fail` check ⇒ `ok:false` ⇒ the CLI exits non-zero. A
21
+ // `warn` (e.g. no agent yet — demo/preview still work) does not fail.
22
+ // - TWO RENDERINGS. Human text by default; a stable `--json` payload for scripts.
23
+ const node_fs_1 = __importDefault(require("node:fs"));
24
+ const node_os_1 = __importDefault(require("node:os"));
25
+ const node_path_1 = __importDefault(require("node:path"));
26
+ const node_child_process_1 = require("node:child_process");
27
+ const agent_config_1 = require("./agent-config");
28
+ /** Resolve a bare binary name against $PATH (or accept an explicit path). Returns
29
+ * the resolved path, or undefined when not found. No spawning. */
30
+ function whichBinary(bin, env) {
31
+ if (bin.includes("/") || bin.includes("\\")) {
32
+ try {
33
+ return node_fs_1.default.statSync(bin).isFile() ? bin : undefined;
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ }
39
+ const dirs = (env.PATH || "").split(node_path_1.default.delimiter).filter(Boolean);
40
+ const exts = process.platform === "win32" ? (env.PATHEXT || ".EXE;.CMD;.BAT").split(";") : [""];
41
+ for (const dir of dirs) {
42
+ for (const ext of exts) {
43
+ const candidate = node_path_1.default.join(dir, bin + ext);
44
+ try {
45
+ if (node_fs_1.default.statSync(candidate).isFile())
46
+ return candidate;
47
+ }
48
+ catch { /* keep looking */ }
49
+ }
50
+ }
51
+ return undefined;
52
+ }
53
+ /** True when `target` could be created/written: walk up to the nearest EXISTING
54
+ * ancestor and require it to be a writable DIRECTORY. (A file in the path is not
55
+ * writable-as-a-dir even though access(2) W_OK on the file itself would pass.)
56
+ * Does NOT create anything — a diagnostic must not have side effects. */
57
+ function dirWritable(target) {
58
+ let dir = node_path_1.default.resolve(target);
59
+ for (;;) {
60
+ let stat;
61
+ try {
62
+ stat = node_fs_1.default.statSync(dir);
63
+ }
64
+ catch {
65
+ const parent = node_path_1.default.dirname(dir);
66
+ if (parent === dir)
67
+ return false;
68
+ dir = parent;
69
+ continue;
70
+ }
71
+ if (!stat.isDirectory())
72
+ return false; // an existing file blocks mkdir beneath it
73
+ try {
74
+ node_fs_1.default.accessSync(dir, node_fs_1.default.constants.W_OK);
75
+ return true;
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
81
+ }
82
+ function runDoctor(args = {}, env = process.env, cwd = process.cwd()) {
83
+ const checks = [];
84
+ // 1. Node runtime — the one hard prerequisite (README: v18+).
85
+ const major = Number((process.version.match(/^v(\d+)/) || [])[1]);
86
+ checks.push(Number.isFinite(major) && major >= 18
87
+ ? { name: "node", status: "ok", detail: `Node ${process.version} (>= 18).` }
88
+ : { name: "node", status: "fail", detail: `Node ${process.version} is below the required v18.`, fix: "Install Node.js 18+ (e.g. `brew install node`, or https://nodejs.org)." });
89
+ // 2. Agent backend — CW delegates execution; without one, real runs park.
90
+ const cfg = (0, agent_config_1.resolveAgentConfig)(args, env);
91
+ if (cfg.source === "none") {
92
+ checks.push({
93
+ name: "agent",
94
+ status: "warn",
95
+ detail: "No agent backend configured — `demo` and `--preview` work, but a real run reports status: blocked.",
96
+ fix: 'Pass --agent-command "claude -p", set $CW_AGENT_COMMAND, or use --agent-command builtin:claude.'
97
+ });
98
+ }
99
+ else {
100
+ const binToken = cfg.command ? String(cfg.command).split(/\s+/)[0] : undefined;
101
+ checks.push({
102
+ name: "agent",
103
+ status: "ok",
104
+ detail: `Agent configured from ${cfg.source}${binToken ? `: ${binToken}` : cfg.endpoint ? " (HTTP endpoint)" : ""}.`
105
+ });
106
+ // 2b. If it is a command agent, is the binary actually on PATH?
107
+ if (binToken) {
108
+ const resolved = whichBinary(binToken, env);
109
+ checks.push(resolved
110
+ ? { name: "agent-binary", status: "ok", detail: `Agent binary "${binToken}" found at ${resolved}.` }
111
+ : { name: "agent-binary", status: "warn", detail: `Configured agent binary "${binToken}" is not on $PATH.`, fix: `Install "${binToken}", or correct --agent-command / $CW_AGENT_COMMAND.` });
112
+ }
113
+ }
114
+ // 3. git — only needed for commit provenance (git HEAD); a warn, not a hard fail.
115
+ const git = (0, node_child_process_1.spawnSync)("git", ["--version"], { encoding: "utf8", timeout: 5000 });
116
+ checks.push(!git.error && git.status === 0
117
+ ? { name: "git", status: "ok", detail: (String(git.stdout || "git").trim()) + "." }
118
+ : { name: "git", status: "warn", detail: "git is not available — commit provenance (git HEAD) is recorded as absent.", fix: "Install git (e.g. `brew install git`) if you want commit provenance." });
119
+ // 4. Home registry — the cross-repo run index lives here; must be writable.
120
+ const home = env.CW_HOME && String(env.CW_HOME).trim()
121
+ ? node_path_1.default.resolve(String(env.CW_HOME))
122
+ : node_path_1.default.join(node_os_1.default.homedir(), ".local", "state", "cool-workflow");
123
+ checks.push(dirWritable(home)
124
+ ? { name: "home-registry", status: "ok", detail: `Home registry location is writable (${home}).` }
125
+ : { name: "home-registry", status: "fail", detail: `Home registry location is not writable: ${home}`, fix: "Set $CW_HOME to a writable directory, or fix the permissions." });
126
+ // 5. Working-dir state — per-repo runs land under <cwd>/.cw.
127
+ const cwState = node_path_1.default.join(node_path_1.default.resolve(cwd), ".cw");
128
+ checks.push(dirWritable(cwState)
129
+ ? { name: "repo-state", status: "ok", detail: `Run state location is writable (${cwState}).` }
130
+ : { name: "repo-state", status: "warn", detail: `Cannot write run state under ${cwState}.`, fix: "Run from a writable working directory, or pass --cwd PATH." });
131
+ const fails = checks.filter((c) => c.status === "fail").length;
132
+ const warns = checks.filter((c) => c.status === "warn").length;
133
+ const ok = fails === 0;
134
+ const summary = ok
135
+ ? warns === 0
136
+ ? "ready — all checks passed"
137
+ : `ready, with ${warns} warning${warns === 1 ? "" : "s"}`
138
+ : `${fails} blocking problem${fails === 1 ? "" : "s"} found`;
139
+ return { schemaVersion: 1, ok, checks, summary };
140
+ }
141
+ /** Human rendering (TTY/default). `--json` callers use the report object directly. */
142
+ function formatDoctorReport(report) {
143
+ const glyph = { ok: "✓", warn: "!", fail: "✗" };
144
+ const lines = ["cw doctor"];
145
+ for (const check of report.checks) {
146
+ lines.push(` ${glyph[check.status]} ${check.name}: ${check.detail}`);
147
+ if (check.fix && check.status !== "ok")
148
+ lines.push(` fix: ${check.fix}`);
149
+ }
150
+ lines.push("");
151
+ lines.push(`${report.ok ? "✓" : "✗"} ${report.summary}`);
152
+ return lines.join("\n");
153
+ }
@@ -19,6 +19,7 @@ const state_1 = require("./state");
19
19
  const multi_agent_operator_ux_1 = require("./multi-agent-operator-ux");
20
20
  const trust_audit_1 = require("./trust-audit");
21
21
  const multi_agent_trust_1 = require("./multi-agent-trust");
22
+ const validation_1 = require("./validation");
22
23
  // ---------------------------------------------------------------------------
23
24
  // Evidence Adoption Reasoning Chain (v0.1.26)
24
25
  //
@@ -616,7 +617,9 @@ function readAllScores(run) {
616
617
  continue;
617
618
  for (const file of node_fs_1.default.readdirSync(dir).filter((entry) => entry.endsWith(".json")).sort()) {
618
619
  try {
619
- const score = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(dir, file), "utf8"));
620
+ const score = (0, validation_1.tryValidateCandidateScore)(JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(dir, file), "utf8")));
621
+ if (!score)
622
+ continue; // Malformed/forged score shape: skip; the score gate fails closed.
620
623
  scores.set(score.id, score);
621
624
  }
622
625
  catch {
@@ -255,53 +255,16 @@ function prepareAgentSpawn(request) {
255
255
  timeoutMs: resolved.timeoutMs || 600000
256
256
  };
257
257
  }
258
- // Reads jobs JSON on stdin, spawns ALL concurrently (shell:false, inherited env —
259
- // the agent's own credentials resolve; CW never reads them), per-job SIGTERM at
260
- // timeoutMs + SIGKILL at +5s, caps each captured stdout at 32MB, and prints the
261
- // outcome array when every job has settled. stderr is drained (a full pipe must
262
- // never wedge a child). A kill yields exitCode null the no-exit-code refusal.
263
- const BATCH_DELEGATE_CHILD = `
264
- const { spawn } = require("node:child_process");
265
- let raw = "";
266
- process.stdin.setEncoding("utf8");
267
- process.stdin.on("data", (d) => (raw += d));
268
- process.stdin.on("end", () => {
269
- const jobs = JSON.parse(raw);
270
- if (!jobs.length) { process.stdout.write("[]"); return; }
271
- const out = new Array(jobs.length);
272
- let pending = jobs.length;
273
- const CAP = 32 * 1024 * 1024;
274
- jobs.forEach((job, i) => {
275
- let stdout = "";
276
- let settled = false;
277
- const settle = (o) => {
278
- if (settled) return;
279
- settled = true;
280
- out[i] = o;
281
- if (--pending === 0) process.stdout.write(JSON.stringify(out));
282
- };
283
- let child;
284
- try {
285
- child = spawn(job.binary, job.args, { cwd: job.cwd, env: process.env, shell: false });
286
- } catch (error) {
287
- settle({ spawnError: String((error && error.message) || error), exitCode: null, stdout: "" });
288
- return;
289
- }
290
- const term = setTimeout(() => { try { child.kill("SIGTERM"); } catch {} }, job.timeoutMs);
291
- const kill = setTimeout(() => { try { child.kill("SIGKILL"); } catch {} }, job.timeoutMs + 5000);
292
- child.stdout.on("data", (d) => { if (stdout.length < CAP) stdout += d; });
293
- child.stderr.on("data", () => {});
294
- child.on("error", (error) => {
295
- clearTimeout(term); clearTimeout(kill);
296
- settle({ spawnError: String((error && error.message) || error), exitCode: null, stdout });
297
- });
298
- child.on("close", (code) => {
299
- clearTimeout(term); clearTimeout(kill);
300
- settle({ exitCode: typeof code === "number" ? code : null, stdout });
301
- });
302
- });
303
- });
304
- `;
258
+ // The batch delegate child is a real, packaged Node script (not an embedded
259
+ // `node -e` string F11). It reads jobs JSON on stdin, spawns ALL concurrently
260
+ // (shell:false, inherited env the agent's own credentials resolve; CW never
261
+ // reads them), per-job SIGTERM at timeoutMs + SIGKILL at +5s, caps each captured
262
+ // stdout at 32MB, and prints the outcome array when every job has settled. stderr
263
+ // is drained (a full pipe must never wedge a child). A kill yields exitCode null —
264
+ // the no-exit-code refusal. We spawn it BY PATH (shell:false); the path is
265
+ // resolved from this compiled module (dist/execution-backend/agent.js) up to the
266
+ // package's `scripts/children/` dir, which package.json ships in "files".
267
+ const BATCH_DELEGATE_CHILD_SCRIPT = node_path_1.default.resolve(__dirname, "..", "..", "scripts", "children", "batch-delegate-child.js");
305
268
  /** Run a batch of agent spawns concurrently; outcomes index-align with jobs. The
306
269
  * parent backstop timeout (max job timeout + 30s) means even a wedged delegate
307
270
  * child cannot deadlock the drive: on any batch-level failure EVERY job settles
@@ -310,7 +273,7 @@ function runAgentBatchOutcomes(jobs) {
310
273
  if (!jobs.length)
311
274
  return [];
312
275
  const maxTimeout = Math.max(...jobs.map((job) => job.timeoutMs));
313
- const child = (0, node_child_process_1.spawnSync)(process.execPath, ["-e", BATCH_DELEGATE_CHILD], {
276
+ const child = (0, node_child_process_1.spawnSync)(process.execPath, [BATCH_DELEGATE_CHILD_SCRIPT], {
314
277
  input: JSON.stringify(jobs),
315
278
  encoding: "utf8",
316
279
  maxBuffer: 33 * 1024 * 1024 * jobs.length,
@@ -47,6 +47,7 @@ exports.backendProbePayload = backendProbePayload;
47
47
  exports.buildChildEnv = buildChildEnv;
48
48
  exports.clearProbeCache = clearProbeCache;
49
49
  const node_fs_1 = __importDefault(require("node:fs"));
50
+ const node_path_1 = __importDefault(require("node:path"));
50
51
  const node_child_process_1 = require("node:child_process");
51
52
  // Pure leaf helpers — carved into ./execution-backend/util.ts (god-module carve).
52
53
  // `sha256` is re-exported below to keep the public surface byte-identical.
@@ -578,35 +579,14 @@ function runContainer(descriptor, policy, request, label, handle, attestation) {
578
579
  }
579
580
  return delegatedEnvelope(descriptor, label, handle, attestation, command, args, exitCode, String(result.stdout || ""));
580
581
  }
581
- // A self-contained Node child that performs the remote/CI delegation: it reads a
582
- // JSON job on stdin, POSTs it to the endpoint, optionally polls a returned jobId,
583
- // and prints `{ exitCode, stdout }` (or `{ error }`) on stdout. Node-only (global
584
- // fetch, node >=18), so the driver stays portable and synchronous from CW's view.
585
- const HTTP_DELEGATE_CHILD = `
586
- (async () => {
587
- const read = () => new Promise((res) => { let b = ""; process.stdin.on("data", (c) => (b += c)); process.stdin.on("end", () => res(b)); });
588
- try {
589
- const job = JSON.parse((await read()) || "{}");
590
- const endpoint = process.env.CW_DELEGATE_ENDPOINT;
591
- if (!endpoint) throw new Error("no endpoint");
592
- const post = await fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(job) });
593
- if (!post.ok) throw new Error("runner responded " + post.status);
594
- let data = await post.json();
595
- // Poll a returned jobId until the runner reports done.
596
- let guard = 0;
597
- while (data && data.jobId && data.done !== true && guard++ < 600) {
598
- await new Promise((r) => setTimeout(r, 1000));
599
- const poll = await fetch(endpoint + (endpoint.includes("?") ? "&" : "?") + "jobId=" + encodeURIComponent(data.jobId));
600
- if (!poll.ok) throw new Error("poll responded " + poll.status);
601
- data = await poll.json();
602
- }
603
- if (typeof data.exitCode !== "number") throw new Error("runner did not report an exitCode");
604
- process.stdout.write(JSON.stringify({ exitCode: data.exitCode, stdout: String(data.stdout || "") }));
605
- } catch (e) {
606
- process.stdout.write(JSON.stringify({ error: e && e.message ? e.message : String(e) }));
607
- }
608
- })();
609
- `;
582
+ // The remote/CI delegation child is a real, packaged Node script (not an embedded
583
+ // `node -e` string — F11): it reads a JSON job on stdin, POSTs it to the endpoint,
584
+ // optionally polls a returned jobId, and prints `{ exitCode, stdout }` (or
585
+ // `{ error }`) on stdout. Node-only (global fetch, node >=18), so the driver stays
586
+ // portable and synchronous from CW's view. We spawn it BY PATH (shell:false). The
587
+ // path is resolved from this compiled module (dist/execution-backend.js) up to the
588
+ // package's `scripts/children/` dir, which package.json ships in "files".
589
+ const HTTP_DELEGATE_CHILD_SCRIPT = node_path_1.default.resolve(__dirname, "..", "scripts", "children", "http-delegate-child.js");
610
590
  /** remote / ci — real HTTP delegation. POSTs the job to the configured endpoint
611
591
  * (and polls a returned jobId) via a Node child, then records the runner's exit +
612
592
  * stdout digest as canonical evidence. Fails closed when the endpoint is missing,
@@ -628,7 +608,7 @@ function runHttpDelegation(descriptor, policy, request, label, handle, attestati
628
608
  sandboxProfileId: policy.id,
629
609
  jobId: handle.jobId
630
610
  });
631
- const child = (0, node_child_process_1.spawnSync)(process.execPath, ["-e", HTTP_DELEGATE_CHILD], {
611
+ const child = (0, node_child_process_1.spawnSync)(process.execPath, [HTTP_DELEGATE_CHILD_SCRIPT], {
632
612
  input: job,
633
613
  env: { ...process.env, CW_DELEGATE_ENDPOINT: endpoint },
634
614
  encoding: "utf8",
@@ -752,7 +732,7 @@ function runAgentEndpoint(descriptor, policy, request, label, resolved, attestat
752
732
  resultPath: manifest?.resultPath,
753
733
  sandboxProfileId: policy.id
754
734
  });
755
- const child = (0, node_child_process_1.spawnSync)(process.execPath, ["-e", HTTP_DELEGATE_CHILD], {
735
+ const child = (0, node_child_process_1.spawnSync)(process.execPath, [HTTP_DELEGATE_CHILD_SCRIPT], {
756
736
  input: job,
757
737
  env: { ...process.env, CW_DELEGATE_ENDPOINT: endpoint },
758
738
  encoding: "utf8",
package/dist/gates.js ADDED
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ // Shared, drift-proof gate primitives (F4). These two helpers were previously
3
+ // duplicated "in sync by comment" in commit.ts and candidate-scoring.ts — the
4
+ // no-false-green gate and the sandbox-profile lookup. A by-comment sync is a
5
+ // structural drift hazard: nothing prevents one copy from changing without the
6
+ // other, and the two gates MUST agree (selection + commit must reach the same
7
+ // empty-capture verdict). Extracting ONE implementation each, imported by both
8
+ // call sites, makes that drift impossible.
9
+ //
10
+ // Pure functions over PERSISTED state only — no clock, no randomness, no fs —
11
+ // so both selection and commit replays reach the same gate decision. Keep them
12
+ // fail-closed: when in doubt the caller must BLOCK, never silently pass.
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.emptyCaptureWarning = emptyCaptureWarning;
15
+ exports.sandboxProfileForCandidate = sandboxProfileForCandidate;
16
+ /** The HARD no-false-green gate (DIRECTION.md "ambiguity is a visible state").
17
+ * A verifier node is built FROM a result node; when that result captured no
18
+ * structured signal at all the result node carries a `metadata.captureWarning`
19
+ * marker (set in worker-isolation / lifecycle ingest via isEmptyCapture). The
20
+ * worker output is still ACCEPTED (a recorded warning, never a silent pass), but
21
+ * a verifier-GATED commit — and candidate SELECTION — must NOT be able to
22
+ * present that zero-evidence result as clean/green. We detect it here, reading
23
+ * ONLY persisted state (the source result node's metadata) — purely functional,
24
+ * no clock/ordering — so snapshot replay reaches the same gate decision.
25
+ * Returns the marker string, or undefined.
26
+ *
27
+ * Resolution trail: verifier node -> its input/parent result node. We look at
28
+ * `inputs.inputNodeId` (set by runPipelineStage) first, then fall back to the
29
+ * first parent, so it works regardless of which ingest path produced the node. */
30
+ function emptyCaptureWarning(run, verifierNode) {
31
+ const resultNodeId = (typeof verifierNode.inputs?.inputNodeId === "string" ? verifierNode.inputs.inputNodeId : undefined) ||
32
+ verifierNode.parents[0];
33
+ const resultNode = resultNodeId ? (run.nodes || []).find((node) => node.id === resultNodeId) : undefined;
34
+ const warning = resultNode?.metadata?.captureWarning;
35
+ return typeof warning === "string" && warning ? warning : undefined;
36
+ }
37
+ /** Resolve the sandbox profile that backed a candidate's acceptance: the
38
+ * worker's profile if the candidate has a worker, else the originating task's.
39
+ * Read by both the commit gate's acceptance rationale and selection. Accepts an
40
+ * optional candidate so the commit-side caller (which may not have resolved a
41
+ * candidate) can share the same lookup. Pure over persisted run state. */
42
+ function sandboxProfileForCandidate(run, candidate) {
43
+ const worker = candidate?.workerId ? (run.workers || []).find((entry) => entry.id === candidate.workerId) : undefined;
44
+ if (worker?.sandboxProfileId)
45
+ return worker.sandboxProfileId;
46
+ const task = candidate?.taskId ? (run.tasks || []).find((entry) => entry.id === candidate.taskId) : undefined;
47
+ return task?.sandboxProfileId;
48
+ }
@@ -17,6 +17,11 @@ const runner = new orchestrator_1.CoolWorkflowRunner({
17
17
  });
18
18
  process.stdin.setEncoding("utf8");
19
19
  let buffer = "";
20
+ // A single line-delimited JSON-RPC request is bounded; an un-terminated stream
21
+ // (a peer that never sends a newline, or a slow byte dribble) must not grow the
22
+ // long-lived server's buffer without limit until it OOMs. Cap the unconsumed
23
+ // buffer and fail closed on a frame that exceeds it.
24
+ const MAX_LINE_BYTES = 16 * 1024 * 1024;
20
25
  process.stdin.on("data", (chunk) => {
21
26
  buffer += chunk;
22
27
  let newlineIndex;
@@ -26,6 +31,12 @@ process.stdin.on("data", (chunk) => {
26
31
  if (line)
27
32
  handleLine(line);
28
33
  }
34
+ if (buffer.length > MAX_LINE_BYTES) {
35
+ // No newline in an over-cap buffer ⇒ a malformed or hostile frame. Reject it
36
+ // and drop the partial bytes rather than accumulate toward OOM.
37
+ buffer = "";
38
+ sendError(null, -32700, `Parse error: request line exceeds ${MAX_LINE_BYTES} bytes`);
39
+ }
29
40
  });
30
41
  function handleLine(line) {
31
42
  let message;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MULTI_AGENT_SCHEMA_VERSION = void 0;
3
+ exports.createId = exports.MULTI_AGENT_SCHEMA_VERSION = void 0;
4
4
  exports.indexRow = indexRow;
5
5
  exports.assertNoRecordPathCollisions = assertNoRecordPathCollisions;
6
6
  exports.pluralKind = pluralKind;
@@ -9,7 +9,6 @@ exports.assertLifecycleTransition = assertLifecycleTransition;
9
9
  exports.lifecycleEvent = lifecycleEvent;
10
10
  exports.isMembershipReported = isMembershipReported;
11
11
  exports.touch = touch;
12
- exports.createId = createId;
13
12
  exports.compact = compact;
14
13
  exports.unique = unique;
15
14
  exports.countBy = countBy;
@@ -106,14 +105,11 @@ function touch(record) {
106
105
  record.updatedAt = new Date().toISOString();
107
106
  return record;
108
107
  }
109
- // Deterministic record id (FreeBSD-audit L12/L13): the record's POSITION in its
110
- // per-run collection, threaded from the call site. No wall-clock stamp, no PRNG
111
- // suffix re-running the same multi-agent topology mints byte-identical ids, so
112
- // snapshot/replay digests match. Each call site already asserts the minted id is
113
- // unique within its collection, and these collections only ever append.
114
- function createId(prefix, seq) {
115
- return `${prefix}-${String(seq).padStart(4, "0")}`;
116
- }
108
+ // Deterministic record id single source of truth in ./ids. Re-exported here so
109
+ // multi-agent.ts importers stay byte-unchanged (F10 dedup: the coordinator layer
110
+ // shares the exact same helper).
111
+ var ids_1 = require("./ids");
112
+ Object.defineProperty(exports, "createId", { enumerable: true, get: function () { return ids_1.createId; } });
117
113
  function compact(value) {
118
114
  if (!value)
119
115
  return undefined;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createId = createId;
4
+ // Single source of truth for the deterministic record id scheme shared by the
5
+ // multi-agent kernel (multi-agent/helpers.ts) and the coordinator/blackboard
6
+ // layer (coordinator/util.ts). Both previously copy-pasted byte-identical
7
+ // zero-padded-seq logic; this dedup (F10) collapses them onto one helper.
8
+ //
9
+ // Deterministic record id (FreeBSD-audit L12/L13): the record's POSITION in its
10
+ // per-run collection, threaded from the call site. No wall-clock stamp, no PRNG
11
+ // suffix — re-running the same multi-agent topology / coordination mints
12
+ // byte-identical ids, so snapshot/replay digests match. Each call site already
13
+ // asserts the minted id is unique within its collection, and these collections
14
+ // only ever append.
15
+ //
16
+ // REPLAY DETERMINISM: pure function of its arguments — no Date, no random.
17
+ // The output MUST stay byte-identical: `${prefix}-${4-digit-zero-padded-seq}`.
18
+ function createId(prefix, seq) {
19
+ return `${prefix}-${String(seq).padStart(4, "0")}`;
20
+ }
@@ -124,6 +124,14 @@ function replayMultiAgentSnapshot(target, options = {}) {
124
124
  const replayDir = node_path_1.default.join(suiteDir, "replay");
125
125
  const replayRunPath = node_path_1.default.join(suiteDir, "replay-run.json");
126
126
  node_fs_1.default.mkdirSync(replayDir, { recursive: true });
127
+ // RE-DERIVE the projection from the raw captured state instead of copying
128
+ // snapshot.normalized. Copying the baseline would make compareMultiAgentReplay
129
+ // diff the baseline against a byte-copy of itself, so a projection-determinism
130
+ // regression in normalizeRun could never be caught (the determinism moat would
131
+ // be false-green). We reconstruct the run from the captured raw state and run
132
+ // the SAME normalizeRun pipeline used at capture time, producing an INDEPENDENT
133
+ // re-projection the comparison can pit against the baseline.
134
+ const replayed = rederiveNormalizedFromSnapshot(snapshot);
127
135
  const replay = {
128
136
  schemaVersion: 1,
129
137
  kind: "multi-agent-replay-run",
@@ -139,7 +147,7 @@ function replayMultiAgentSnapshot(target, options = {}) {
139
147
  replayRunPath,
140
148
  snapshotPath: snapshot.paths.snapshotPath
141
149
  },
142
- replay: snapshot.normalized,
150
+ replay: replayed,
143
151
  errors: []
144
152
  };
145
153
  (0, state_1.writeJson)(replayRunPath, replay);
@@ -398,6 +406,24 @@ function loadScoreForTarget(target, scorePath) {
398
406
  }
399
407
  return scoreMultiAgentReplay(target);
400
408
  }
409
+ // Re-derive snapshot.normalized INDEPENDENTLY at replay time. The raw captured
410
+ // state is the baseline run state file (snapshot.paths.baselineStatePath) — the
411
+ // same WorkflowRun captureRun/normalizeRun projected at capture time. We re-load
412
+ // it and re-run the IDENTICAL normalizeRun pipeline, so the result is a genuine
413
+ // re-projection rather than a copy of the baseline. Fail-closed: if the raw
414
+ // state cannot be reconstructed we throw (never silently fall back to the
415
+ // baseline copy — that is exactly the false-green this guards against).
416
+ function rederiveNormalizedFromSnapshot(snapshot) {
417
+ const statePath = snapshot.paths.baselineStatePath;
418
+ if (!statePath || !node_fs_1.default.existsSync(statePath)) {
419
+ throw new Error(`Cannot re-derive replay projection: baseline run state missing at ${statePath || "<unset>"}; re-snapshot from a live run before replaying.`);
420
+ }
421
+ const result = (0, state_1.loadRunStateFile)(statePath, { dryRun: true });
422
+ if (result.report.status === "unsupported") {
423
+ throw new Error(`Cannot re-derive replay projection: baseline run state at ${statePath} is unsupported: ${result.report.errors.join("; ")}`);
424
+ }
425
+ return normalizeRun(result.run);
426
+ }
401
427
  function captureRun(run) {
402
428
  return {
403
429
  topology: run.topologies || { schemaVersion: 1, runs: [] },
@@ -384,22 +384,54 @@ function hostSelect(run, options = {}) {
384
384
  }
385
385
  return envelope(run, "select", { performed: "selected-candidate", data: selection });
386
386
  }
387
+ function memoize(compute) {
388
+ let cached;
389
+ return (run) => (cached ??= { value: compute(run) }).value;
390
+ }
391
+ function createHostSummaryCache(run) {
392
+ const topologies = memoize(topology_1.summarizeTopologies);
393
+ const multiAgent = memoize(multi_agent_1.summarizeMultiAgent);
394
+ const blackboard = memoize(coordinator_1.summarizeBlackboard);
395
+ const workers = memoize(operator_ux_1.summarizeOperatorWorkers);
396
+ const candidates = memoize(operator_ux_1.summarizeOperatorCandidates);
397
+ const feedback = memoize(operator_ux_1.summarizeOperatorFeedback);
398
+ const commits = memoize(operator_ux_1.summarizeOperatorCommits);
399
+ const trust = memoize(trust_audit_1.summarizeTrustAudit);
400
+ const operator = memoize(operator_ux_1.summarizeOperatorRun);
401
+ const multiAgentOperator = memoize(multi_agent_operator_ux_1.summarizeMultiAgentOperator);
402
+ const active = memoize(activeTopologies);
403
+ return {
404
+ run,
405
+ topologies: () => topologies(run),
406
+ multiAgent: () => multiAgent(run),
407
+ blackboard: () => blackboard(run),
408
+ workers: () => workers(run),
409
+ candidates: () => candidates(run),
410
+ feedback: () => feedback(run),
411
+ commits: () => commits(run),
412
+ trust: () => trust(run),
413
+ operator: () => operator(run),
414
+ multiAgentOperator: () => multiAgentOperator(run),
415
+ active: () => active(run)
416
+ };
417
+ }
387
418
  function envelope(run, command, options = {}) {
388
- const topologies = (0, topology_1.summarizeTopologies)(run);
389
- const multiAgent = (0, multi_agent_1.summarizeMultiAgent)(run);
390
- const blackboard = (0, coordinator_1.summarizeBlackboard)(run);
391
- const workers = (0, operator_ux_1.summarizeOperatorWorkers)(run);
392
- const candidates = (0, operator_ux_1.summarizeOperatorCandidates)(run);
393
- const feedback = (0, operator_ux_1.summarizeOperatorFeedback)(run);
394
- const commits = (0, operator_ux_1.summarizeOperatorCommits)(run);
395
- const trust = (0, trust_audit_1.summarizeTrustAudit)(run);
396
- const operator = (0, operator_ux_1.summarizeOperatorRun)(run);
397
- const multiAgentOperator = (0, multi_agent_operator_ux_1.summarizeMultiAgentOperator)(run);
398
- const active = activeTopologies(run);
419
+ const cache = createHostSummaryCache(run);
420
+ const topologies = cache.topologies();
421
+ const multiAgent = cache.multiAgent();
422
+ const blackboard = cache.blackboard();
423
+ const workers = cache.workers();
424
+ const candidates = cache.candidates();
425
+ const feedback = cache.feedback();
426
+ const commits = cache.commits();
427
+ const trust = cache.trust();
428
+ const operator = cache.operator();
429
+ const multiAgentOperator = cache.multiAgentOperator();
430
+ const active = cache.active();
399
431
  const blockedReasons = unique([...operator.blockedReasons, ...(options.extraBlockedReasons || [])]);
400
- const state = blockedReasons.length ? "blocked" : classifyHostState(run);
432
+ const state = blockedReasons.length ? "blocked" : classifyHostState(run, cache);
401
433
  const ids = activeIds(run, active);
402
- const nextActions = hostNextActions(run, state, active, options.requiredHostAction);
434
+ const nextActions = hostNextActions(run, state, active, options.requiredHostAction, cache);
403
435
  return {
404
436
  schemaVersion: 1,
405
437
  surface: "multi-agent-host",
@@ -427,12 +459,12 @@ function envelope(run, command, options = {}) {
427
459
  data: options.data
428
460
  };
429
461
  }
430
- function classifyHostState(run) {
431
- const active = activeTopologies(run);
432
- const feedback = (0, operator_ux_1.summarizeOperatorFeedback)(run);
433
- const workers = (0, operator_ux_1.summarizeOperatorWorkers)(run);
434
- const candidates = (0, operator_ux_1.summarizeOperatorCandidates)(run);
435
- const commits = (0, operator_ux_1.summarizeOperatorCommits)(run);
462
+ function classifyHostState(run, cache = createHostSummaryCache(run)) {
463
+ const active = cache.active();
464
+ const feedback = cache.feedback();
465
+ const workers = cache.workers();
466
+ const candidates = cache.candidates();
467
+ const commits = cache.commits();
436
468
  if (!active.length && !(run.multiAgent?.runs || []).length)
437
469
  return "needs-run";
438
470
  if (feedback.open.some((entry) => !entry.retryable))
@@ -475,7 +507,7 @@ function activeIds(run, active) {
475
507
  auditEventIds: unique(active.flatMap((entry) => entry.links.auditEventIds))
476
508
  };
477
509
  }
478
- function hostNextActions(run, state, active, requiredHostAction) {
510
+ function hostNextActions(run, state, active, requiredHostAction, cache = createHostSummaryCache(run)) {
479
511
  if (requiredHostAction)
480
512
  return [{ command: "host-action", reason: requiredHostAction, priority: "high" }];
481
513
  const runId = run.id;
@@ -493,7 +525,7 @@ function hostNextActions(run, state, active, requiredHostAction) {
493
525
  case "ready-for-selection":
494
526
  return [{ command: `node scripts/cw.js multi-agent select ${runId} --candidate <candidate-id> --reason "<rationale>"`, reason: "Select a scored candidate after verifier gates pass.", priority: "high" }];
495
527
  case "ready-for-commit": {
496
- const ready = (0, operator_ux_1.summarizeOperatorCandidates)(run).readyForCommit[0];
528
+ const ready = cache.candidates().readyForCommit[0];
497
529
  return [{ command: `node scripts/cw.js commit ${runId} --selection ${ready.selectionId} --reason "<verified rationale>"`, reason: "Create a verifier-gated CW state commit.", priority: "high" }];
498
530
  }
499
531
  case "complete":
@@ -15,6 +15,7 @@ const coordinator_1 = require("./coordinator");
15
15
  const multi_agent_1 = require("./multi-agent");
16
16
  const topology_1 = require("./topology");
17
17
  const trust_audit_1 = require("./trust-audit");
18
+ const validation_1 = require("./validation");
18
19
  function summarizeMultiAgentOperator(run) {
19
20
  const topologies = (0, topology_1.summarizeTopologies)(run);
20
21
  const multiAgent = (0, multi_agent_1.summarizeMultiAgent)(run);
@@ -447,7 +448,7 @@ function readScores(run, candidateId) {
447
448
  .readdirSync(dir)
448
449
  .filter((file) => file.endsWith(".json"))
449
450
  .sort()
450
- .map((file) => JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(dir, file), "utf8")));
451
+ .map((file) => (0, validation_1.validateCandidateScore)(JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(dir, file), "utf8"))));
451
452
  }
452
453
  function scorePath(run, candidateId, scoreId) {
453
454
  const file = node_path_1.default.join(run.paths.candidatesDir || node_path_1.default.join(run.paths.runDir, "candidates"), safeFileName(candidateId), "scores", `${safeFileName(scoreId)}.json`);