cool-workflow 0.1.81 → 0.1.82

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 (66) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +1 -1
  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 +64 -85
  12. package/dist/capability-registry.js +22 -3
  13. package/dist/commit.js +212 -203
  14. package/dist/coordinator/util.js +6 -9
  15. package/dist/dispatch.js +11 -3
  16. package/dist/evidence-reasoning.js +4 -1
  17. package/dist/execution-backend/agent.js +11 -48
  18. package/dist/execution-backend.js +11 -31
  19. package/dist/gates.js +48 -0
  20. package/dist/multi-agent/helpers.js +6 -10
  21. package/dist/multi-agent/ids.js +20 -0
  22. package/dist/multi-agent-eval.js +27 -1
  23. package/dist/multi-agent-host.js +53 -21
  24. package/dist/multi-agent-operator-ux.js +2 -1
  25. package/dist/multi-agent-trust.js +5 -5
  26. package/dist/node-projection.js +59 -0
  27. package/dist/node-snapshot.js +8 -18
  28. package/dist/orchestrator/lifecycle-operations.js +22 -1
  29. package/dist/orchestrator.js +16 -2
  30. package/dist/reclamation.js +8 -36
  31. package/dist/scheduler.js +34 -4
  32. package/dist/topology.js +25 -4
  33. package/dist/trust-audit.js +70 -38
  34. package/dist/validation.js +328 -0
  35. package/dist/version.js +1 -1
  36. package/dist/worker-isolation.js +143 -58
  37. package/docs/agent-delegation-drive.7.md +1 -0
  38. package/docs/cli-mcp-parity.7.md +1 -0
  39. package/docs/contract-migration-tooling.7.md +1 -0
  40. package/docs/control-plane-scheduling.7.md +1 -0
  41. package/docs/durable-state-and-locking.7.md +1 -0
  42. package/docs/evidence-adoption-reasoning-chain.7.md +1 -0
  43. package/docs/execution-backends.7.md +1 -0
  44. package/docs/multi-agent-cli-mcp-surface.7.md +1 -0
  45. package/docs/multi-agent-eval-replay-harness.7.md +1 -0
  46. package/docs/multi-agent-operator-ux.7.md +1 -0
  47. package/docs/node-snapshot-diff-replay.7.md +1 -0
  48. package/docs/observability-cost-accounting.7.md +1 -0
  49. package/docs/project-index.md +8 -4
  50. package/docs/real-execution-backends.7.md +1 -0
  51. package/docs/release-and-migration.7.md +1 -0
  52. package/docs/release-tooling.7.md +33 -2
  53. package/docs/run-registry-control-plane.7.md +1 -0
  54. package/docs/run-retention-reclamation.7.md +1 -0
  55. package/docs/state-explosion-management.7.md +1 -0
  56. package/docs/team-collaboration.7.md +1 -0
  57. package/docs/web-desktop-workbench.7.md +1 -0
  58. package/manifest/plugin.manifest.json +1 -1
  59. package/manifest/source-context-profiles.json +1 -1
  60. package/package.json +1 -1
  61. package/scripts/canonical-apps.js +4 -4
  62. package/scripts/children/batch-delegate-child.js +58 -0
  63. package/scripts/children/http-delegate-child.js +39 -0
  64. package/scripts/dogfood-release.js +1 -1
  65. package/scripts/golden-path.js +4 -4
  66. package/scripts/release-flow.js +181 -5
@@ -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
+ }
@@ -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`);
@@ -1,7 +1,4 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.policyForRole = policyForRole;
7
4
  exports.policyForGroup = policyForGroup;
@@ -16,7 +13,7 @@ exports.summarizeMultiAgentTrust = summarizeMultiAgentTrust;
16
13
  exports.hasAcceptedJudgeRationale = hasAcceptedJudgeRationale;
17
14
  exports.sourceForActor = sourceForActor;
18
15
  exports.hashText = hashText;
19
- const node_crypto_1 = __importDefault(require("node:crypto"));
16
+ const execution_backend_1 = require("./execution-backend");
20
17
  const trust_audit_1 = require("./trust-audit");
21
18
  function policyForRole(role) {
22
19
  const topologyRole = String(role.metadata?.topologyRoleId || role.title || "").toLowerCase();
@@ -294,8 +291,11 @@ function sourceForActor(actor) {
294
291
  return "runtime-derived";
295
292
  return "cw-validated";
296
293
  }
294
+ // Delegates to the shared execution-backend sha256 (F10 dedup). Byte-identical:
295
+ // both emit `sha256:<hex>` and Node's Hash.update(string) defaults to utf8, the
296
+ // same encoding the shared helper passes explicitly.
297
297
  function hashText(value) {
298
- return `sha256:${node_crypto_1.default.createHash("sha256").update(value).digest("hex")}`;
298
+ return (0, execution_backend_1.sha256)(value);
299
299
  }
300
300
  function resolvePolicy(run, input) {
301
301
  const membership = input.membershipId ? run.multiAgent?.memberships.find((entry) => entry.id === input.membershipId) : undefined;
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ // Canonical StateNode projection (F9 de-triplication) — the SINGLE source of
3
+ // truth for which StateNode fields make up the derived, fingerprinted body.
4
+ //
5
+ // Previously this 13-field set was hand-copied in three places: node-snapshot.ts's
6
+ // snapshotBody, reclamation.ts's snapshotProjectionDigest, and reclamation.ts's
7
+ // rawNodeBody/nodeBodyDigest (whose comment literally said "Mirror
8
+ // node-snapshot.ts"). Any field added in one place but not the others would drift
9
+ // the projection — and because reclamation hashes are tombstone-chained, a drift
10
+ // would silently break the chain. This module collapses the list to ONE export so
11
+ // the field set can only change in one place.
12
+ //
13
+ // BYTE-IDENTITY [load-bearing]: the projection / digest OUTPUT here is identical to
14
+ // the prior in-line implementations. `normalizeValue` sorts object keys, so the
15
+ // literal field ORDER never affected the bytes — only the field SET ever mattered.
16
+ // `replayStableStringify(value) === JSON.stringify(normalizeValue(value))` and
17
+ // `normalizeValue` is idempotent, so the projected-body digest and the raw-body
18
+ // digest funnel through the same canonical bytes (node-snapshot-diff-replay +
19
+ // reclamation smokes verify this).
20
+ //
21
+ // Pure: no I/O, no wall-clock, no random — safe in core/state/replay logic.
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.rawNodeProjection = rawNodeProjection;
24
+ exports.projectNodeBody = projectNodeBody;
25
+ exports.nodeProjectionDigestInput = nodeProjectionDigestInput;
26
+ const multi_agent_eval_1 = require("./multi-agent-eval");
27
+ /** The raw (un-normalized) projection input: the canonical field set, copied off
28
+ * the StateNode with no scrubbing. `normalizeValue`/`replayStableStringify` apply
29
+ * the timestamp/path scrubbing downstream. This is the ONE place the field list
30
+ * lives — add or drop a projected field here and every consumer follows. */
31
+ function rawNodeProjection(node) {
32
+ return {
33
+ id: node.id,
34
+ kind: node.kind,
35
+ status: node.status,
36
+ loopStage: node.loopStage,
37
+ inputs: node.inputs,
38
+ outputs: node.outputs,
39
+ artifacts: node.artifacts,
40
+ evidence: node.evidence,
41
+ errors: node.errors,
42
+ parents: node.parents,
43
+ children: node.children,
44
+ contractId: node.contractId,
45
+ metadata: node.metadata
46
+ };
47
+ }
48
+ /** The normalized, derived projected body of one StateNode — timestamps/paths
49
+ * stripped via the eval normalizer, so it is byte-stable across captures of the
50
+ * same logical state. This is exactly node-snapshot.ts's historical snapshotBody. */
51
+ function projectNodeBody(node) {
52
+ return (0, multi_agent_eval_1.normalizeValue)(rawNodeProjection(node));
53
+ }
54
+ /** Stable digest input for the projected body: the canonical bytes that the
55
+ * snapshot/reconstruction digests bind. `replayStableStringify` re-normalizes, so
56
+ * feeding the raw projection or the already-normalized body yields identical bytes. */
57
+ function nodeProjectionDigestInput(node) {
58
+ return (0, multi_agent_eval_1.replayStableStringify)(rawNodeProjection(node));
59
+ }
@@ -37,7 +37,9 @@ const node_path_1 = __importDefault(require("node:path"));
37
37
  const pipeline_runner_1 = require("./pipeline-runner");
38
38
  const state_1 = require("./state");
39
39
  const multi_agent_eval_1 = require("./multi-agent-eval");
40
+ const node_projection_1 = require("./node-projection");
40
41
  const state_explosion_1 = require("./state-explosion");
42
+ const validation_1 = require("./validation");
41
43
  exports.NODE_SNAPSHOT_SCHEMA_VERSION = 1;
42
44
  /** Structured fail-closed error (mirrors the PipelineContractError shape). */
43
45
  class NodeSnapshotError extends Error {
@@ -64,23 +66,11 @@ const SNAPSHOT_SECTIONS = [
64
66
  "metadata"
65
67
  ];
66
68
  /** The normalized projection of a node — timestamps/paths stripped by the eval
67
- * normalizer, so it is byte-stable across captures of the same logical state. */
69
+ * normalizer, so it is byte-stable across captures of the same logical state. The
70
+ * canonical field set lives in node-projection.ts (shared with reclamation.ts so
71
+ * the projection can never drift across the two). */
68
72
  function snapshotBody(node) {
69
- return (0, multi_agent_eval_1.normalizeValue)({
70
- id: node.id,
71
- kind: node.kind,
72
- status: node.status,
73
- loopStage: node.loopStage,
74
- inputs: node.inputs,
75
- outputs: node.outputs,
76
- artifacts: node.artifacts,
77
- evidence: node.evidence,
78
- errors: node.errors,
79
- parents: node.parents,
80
- children: node.children,
81
- contractId: node.contractId,
82
- metadata: node.metadata
83
- });
73
+ return (0, node_projection_1.projectNodeBody)(node);
84
74
  }
85
75
  /** RAW fingerprint (NOT normalized): any transition (updatedAt/status) or
86
76
  * artifact/evidence change flips it, which is how drift is detected. */
@@ -113,7 +103,7 @@ function readNodeSnapshot(run, snapshotId) {
113
103
  for (const nodeDir of node_fs_1.default.readdirSync(root)) {
114
104
  const file = node_path_1.default.join(root, nodeDir, `${snapshotId}.json`);
115
105
  if (node_fs_1.default.existsSync(file))
116
- return JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
106
+ return (0, validation_1.validateNodeSnapshot)(JSON.parse(node_fs_1.default.readFileSync(file, "utf8")));
117
107
  }
118
108
  }
119
109
  throw new NodeSnapshotError("snapshot-not-found", `Node snapshot ${snapshotId} not found in run ${run.id}`, { freshness: "absent" });
@@ -125,7 +115,7 @@ function readNodeReplay(run, replayId) {
125
115
  for (const nodeDir of node_fs_1.default.readdirSync(root)) {
126
116
  const file = node_path_1.default.join(root, nodeDir, "replays", `${replayId}.json`);
127
117
  if (node_fs_1.default.existsSync(file))
128
- return JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
118
+ return (0, validation_1.validateNodeReplayRun)(JSON.parse(node_fs_1.default.readFileSync(file, "utf8")));
129
119
  }
130
120
  }
131
121
  throw new NodeSnapshotError("replay-not-found", `Node replay ${replayId} not found in run ${run.id}`, { freshness: "absent" });
@@ -17,6 +17,7 @@ exports.commit = commit;
17
17
  // plan() receives an already-resolved workflow app record (the runner still owns
18
18
  // app loading, which is instance-stateful). Behavior is identical to the inline
19
19
  // implementations; only the location changed.
20
+ const node_crypto_1 = __importDefault(require("node:crypto"));
20
21
  const node_fs_1 = __importDefault(require("node:fs"));
21
22
  const node_path_1 = __importDefault(require("node:path"));
22
23
  const state_1 = require("../state");
@@ -456,8 +457,28 @@ function renderPrompt(prompt, inputs) {
456
457
  }
457
458
  return rendered;
458
459
  }
460
+ // Deterministic run id (replay-determinism self-audit): the wall-clock stamp is an
461
+ // edge timestamp (recorded once and stripped on replay), but the former
462
+ // Math.random() suffix made the run id itself non-reproducible — re-deriving the id
463
+ // for the SAME recorded run would never match. The suffix is now a content hash of
464
+ // the run's deterministic identity (workflowId + the recorded stamp), so the id is a
465
+ // pure function of inputs that already live in state. Distinct plan() invocations
466
+ // still get distinct ids because the per-millisecond stamp differs; replaying a
467
+ // recorded run reproduces the byte-identical id. Mirrors the de-clock done for
468
+ // worker ids in src/worker-isolation/paths.ts.
469
+ let runIdSequence = 0;
459
470
  function createRunId(workflowId) {
460
471
  const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
461
- const suffix = Math.random().toString(36).slice(2, 8);
472
+ // The stamp is second-resolution, so several runs of the same workflowId minted
473
+ // within one second would otherwise hash to the SAME id. process.pid + a monotonic
474
+ // counter break the tie across BOTH same-process (counter) and concurrent-process
475
+ // (pid) minting — deterministic-by-environment, not a PRNG, so it keeps the
476
+ // replay-determinism intent; the id is an edge stamp stripped on replay anyway.
477
+ runIdSequence += 1;
478
+ const suffix = node_crypto_1.default
479
+ .createHash("sha256")
480
+ .update(`${workflowId}:${stamp}:${process.pid}:${runIdSequence}`)
481
+ .digest("hex")
482
+ .slice(0, 6);
462
483
  return `${workflowId}-${stamp}-${suffix}`;
463
484
  }
@@ -90,10 +90,24 @@ class CoolWorkflowRunner {
90
90
  pluginRoot;
91
91
  workflowsDir;
92
92
  appsDir;
93
- constructor({ pluginRoot }) {
93
+ // F7: the directory a run is resolved against (replaces the former process.chdir
94
+ // bracket in capability-core). undefined => fall back to process.cwd(). The runner
95
+ // reads runs from disk per call (no in-memory run state), so withBaseDir hands back
96
+ // a cheap scoped clone instead of mutating the global process cwd.
97
+ baseDir;
98
+ constructor({ pluginRoot, baseDir }) {
94
99
  this.pluginRoot = resolvePluginRoot(pluginRoot);
95
100
  this.workflowsDir = node_path_1.default.join(this.pluginRoot, "workflows");
96
101
  this.appsDir = node_path_1.default.join(this.pluginRoot, "apps");
102
+ this.baseDir = baseDir ? node_path_1.default.resolve(baseDir) : undefined;
103
+ }
104
+ /** Return a runner that resolves runs against `dir` instead of process.cwd(),
105
+ * WITHOUT chdir-ing the process (F7). Same instance when the dir is unchanged. */
106
+ withBaseDir(dir) {
107
+ const resolved = dir ? node_path_1.default.resolve(dir) : undefined;
108
+ if (resolved === this.baseDir)
109
+ return this;
110
+ return new CoolWorkflowRunner({ pluginRoot: this.pluginRoot, baseDir: resolved });
97
111
  }
98
112
  listWorkflows() {
99
113
  return this.loadWorkflowApps().map((record) => {
@@ -700,7 +714,7 @@ class CoolWorkflowRunner {
700
714
  return feedbackOps.resolveFeedback(this.loadRun(runId), feedbackId, options);
701
715
  }
702
716
  loadRun(runId) {
703
- return (0, state_1.loadRunFromCwd)(runId);
717
+ return (0, state_1.loadRunFromCwd)(runId, this.baseDir);
704
718
  }
705
719
  loadWorkflowAppById(appId) {
706
720
  const record = this.loadWorkflowApps().find((candidate) => candidate.app.id === appId);
@@ -53,6 +53,7 @@ const node_crypto_1 = __importDefault(require("node:crypto"));
53
53
  const node_fs_1 = __importDefault(require("node:fs"));
54
54
  const node_path_1 = __importDefault(require("node:path"));
55
55
  const multi_agent_eval_1 = require("./multi-agent-eval");
56
+ const node_projection_1 = require("./node-projection");
56
57
  const node_snapshot_1 = require("./node-snapshot");
57
58
  const state_1 = require("./state");
58
59
  const trust_audit_1 = require("./trust-audit");
@@ -397,46 +398,17 @@ function buildReferenceGraph(run) {
397
398
  add(message.id);
398
399
  return refs;
399
400
  }
401
+ /** expectDigest of a node's deterministic projection. Re-uses the SHARED
402
+ * node-projection field set (node-projection.ts) so reconstruction matches
403
+ * node-snapshot.ts's body byte-for-byte — the projection can no longer drift. */
400
404
  function snapshotProjectionDigest(node) {
401
- // Mirror node-snapshot.ts's deterministic projection so reconstruction matches.
402
- const body = (0, multi_agent_eval_1.normalizeValue)({
403
- id: node.id,
404
- kind: node.kind,
405
- status: node.status,
406
- loopStage: node.loopStage,
407
- inputs: node.inputs,
408
- outputs: node.outputs,
409
- artifacts: node.artifacts,
410
- evidence: node.evidence,
411
- errors: node.errors,
412
- parents: node.parents,
413
- children: node.children,
414
- contractId: node.contractId,
415
- metadata: node.metadata
416
- });
417
- return sha256OfString((0, multi_agent_eval_1.replayStableStringify)(body));
405
+ return sha256OfString((0, node_projection_1.nodeProjectionDigestInput)(node));
418
406
  }
419
407
  /** Body digest of the RETAINED node (lives in state.json). The reconstruction
420
- * verifier re-derives the projection from this retained input. */
408
+ * verifier re-derives the projection from this retained input. Same shared field
409
+ * set / canonical bytes as snapshotProjectionDigest. */
421
410
  function nodeBodyDigest(node) {
422
- return sha256OfString((0, multi_agent_eval_1.replayStableStringify)(rawNodeBody(node)));
423
- }
424
- function rawNodeBody(node) {
425
- return {
426
- id: node.id,
427
- kind: node.kind,
428
- status: node.status,
429
- loopStage: node.loopStage,
430
- inputs: node.inputs,
431
- outputs: node.outputs,
432
- artifacts: node.artifacts,
433
- evidence: node.evidence,
434
- errors: node.errors,
435
- parents: node.parents,
436
- children: node.children,
437
- contractId: node.contractId,
438
- metadata: node.metadata
439
- };
411
+ return sha256OfString((0, node_projection_1.nodeProjectionDigestInput)(node));
440
412
  }
441
413
  /** Build the retention plan: which paths are freeable under `policy`, of what
442
414
  * kind, how many bytes, and the resulting capability downgrade. */