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
@@ -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);
@@ -807,6 +821,7 @@ function formatHelp() {
807
821
  "",
808
822
  "Commands:",
809
823
  " list",
824
+ " doctor [--json] (check your setup: Node, agent backend, git, writable state)",
810
825
  " init <workflow-id> [--title TEXT] [--output PATH]",
811
826
  " quickstart [app-id] [--repo PATH] [--question TEXT] [--agent-command CMD] [--once] [--preview]",
812
827
  " plan <workflow-id> [--repo PATH] [--question TEXT] [--invariant TEXT]",
@@ -859,6 +874,18 @@ function formatHelp() {
859
874
  " run search|list|show|resume|archive|rerun|export|import|verify-import [run-id|archive] [--scope repo|home] [--json]",
860
875
  " queue add|list|drain|show [queue-id] [--repo PATH] [--priority N]",
861
876
  " history [--scope repo|home] [--app ID] [--status STATE] [--json]",
877
+ " audit-run <app-id> [--repo PATH] [--question TEXT] [--agent-command CMD]",
878
+ " metrics show|summary <run-id> [--scope repo|home] [--json]",
879
+ " telemetry verify <run-id> [--pubkey PEM|PATH] [--json]",
880
+ " gc plan|run|verify [run-id] [--json]",
881
+ " sched plan|lease|release|complete|reclaim|reset|policy [--json]",
882
+ " migration list|check|prove [target] [--json]",
883
+ " operator status|report <run-id> [--json]",
884
+ " review status|policy <run-id> [--json]",
885
+ " approve|reject|comment <kind> <run-id> <target-id> [--reason TEXT]",
886
+ " handoff <kind> <run-id> <target-id> [--to ROLE]",
887
+ " loop --prompt TEXT [--interval-minutes N]",
888
+ " demo tamper",
862
889
  " workbench view <run-id> [--json]",
863
890
  " workbench serve [--port N] [--scope repo|home] [--once|--json]",
864
891
  ""
@@ -0,0 +1,72 @@
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.sha256OfString = sha256OfString;
7
+ exports.sha256OfFile = sha256OfFile;
8
+ exports.dirBytes = dirBytes;
9
+ exports.contentDigest = contentDigest;
10
+ // Content addressing + byte measurement for run reclamation (NO `du` — in-process
11
+ // only). Carved out of reclamation.ts (FreeBSD-audit god-module carve) so the pure
12
+ // content-addressing leaf no longer sits inside the write-ahead reclamation
13
+ // transaction. These are pure functions of their path/string inputs — no run
14
+ // state, no module-level mutable state.
15
+ //
16
+ // BEHAVIOR-PRESERVING — pure code movement, zero logic change. reclamation.ts
17
+ // re-exports the public symbols (sha256OfString/sha256OfFile/dirBytes) so the
18
+ // module's surface stays byte-identical.
19
+ const node_crypto_1 = __importDefault(require("node:crypto"));
20
+ const node_fs_1 = __importDefault(require("node:fs"));
21
+ const node_path_1 = __importDefault(require("node:path"));
22
+ const compare_1 = require("../compare");
23
+ function sha256Hex(value) {
24
+ return node_crypto_1.default.createHash("sha256").update(value).digest("hex");
25
+ }
26
+ function sha256OfString(value) {
27
+ return `sha256:${sha256Hex(value)}`;
28
+ }
29
+ function sha256OfFile(file) {
30
+ return `sha256:${sha256Hex(node_fs_1.default.readFileSync(file))}`;
31
+ }
32
+ /** Walk a path and sum file sizes IN-PROCESS (no `du`). Returns 0 if absent. A
33
+ * file returns its own size; a dir returns the recursive sum. */
34
+ function dirBytes(p) {
35
+ let total = 0;
36
+ let stat;
37
+ try {
38
+ stat = node_fs_1.default.statSync(p);
39
+ }
40
+ catch {
41
+ return 0;
42
+ }
43
+ if (stat.isFile())
44
+ return stat.size;
45
+ if (!stat.isDirectory())
46
+ return 0;
47
+ for (const entry of node_fs_1.default.readdirSync(p, { withFileTypes: true })) {
48
+ total += dirBytes(node_path_1.default.join(p, entry.name));
49
+ }
50
+ return total;
51
+ }
52
+ /** Stable content digest of a path (file = its bytes; dir = digest over each
53
+ * member's relative path + bytes, sorted). Lets the freed-manifest record a
54
+ * single sha per freed dir. */
55
+ function contentDigest(p) {
56
+ const stat = node_fs_1.default.statSync(p);
57
+ if (stat.isFile())
58
+ return sha256OfFile(p);
59
+ const parts = [];
60
+ const walk = (dir, rel) => {
61
+ for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true }).sort((a, b) => (0, compare_1.compareBytes)(a.name, b.name))) {
62
+ const abs = node_path_1.default.join(dir, entry.name);
63
+ const r = node_path_1.default.join(rel, entry.name);
64
+ if (entry.isDirectory())
65
+ walk(abs, r);
66
+ else
67
+ parts.push(`${r}:${sha256OfFile(abs)}`);
68
+ }
69
+ };
70
+ walk(p, "");
71
+ return sha256OfString(parts.join("\n"));
72
+ }
@@ -29,10 +29,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
29
29
  return (mod && mod.__esModule) ? mod : { "default": mod };
30
30
  };
31
31
  Object.defineProperty(exports, "__esModule", { value: true });
32
- exports.ReclamationError = exports.ReclamationAbort = exports.SKELETON_REQUIRED_KEYS = void 0;
33
- exports.sha256OfString = sha256OfString;
34
- exports.sha256OfFile = sha256OfFile;
35
- exports.dirBytes = dirBytes;
32
+ exports.dirBytes = exports.sha256OfFile = exports.sha256OfString = exports.ReclamationError = exports.ReclamationAbort = exports.SKELETON_REQUIRED_KEYS = void 0;
36
33
  exports.reclaimedLogPath = reclaimedLogPath;
37
34
  exports.loadReclamationLog = loadReclamationLog;
38
35
  exports.extractSkeleton = extractSkeleton;
@@ -49,10 +46,14 @@ exports.runReclamation = runReclamation;
49
46
  exports.reconstructArtifact = reconstructArtifact;
50
47
  exports.verifyReclamation = verifyReclamation;
51
48
  exports.dominantFailureCode = dominantFailureCode;
52
- const node_crypto_1 = __importDefault(require("node:crypto"));
53
49
  const node_fs_1 = __importDefault(require("node:fs"));
54
50
  const node_path_1 = __importDefault(require("node:path"));
51
+ const hash_1 = require("./reclamation/hash");
52
+ Object.defineProperty(exports, "dirBytes", { enumerable: true, get: function () { return hash_1.dirBytes; } });
53
+ Object.defineProperty(exports, "sha256OfFile", { enumerable: true, get: function () { return hash_1.sha256OfFile; } });
54
+ Object.defineProperty(exports, "sha256OfString", { enumerable: true, get: function () { return hash_1.sha256OfString; } });
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");
@@ -94,59 +95,6 @@ class ReclamationError extends Error {
94
95
  }
95
96
  }
96
97
  exports.ReclamationError = ReclamationError;
97
- // ---------------------------------------------------------------------------
98
- // Content addressing + byte measurement (NO `du` — in-process only).
99
- // ---------------------------------------------------------------------------
100
- function sha256Hex(value) {
101
- return node_crypto_1.default.createHash("sha256").update(value).digest("hex");
102
- }
103
- function sha256OfString(value) {
104
- return `sha256:${sha256Hex(value)}`;
105
- }
106
- function sha256OfFile(file) {
107
- return `sha256:${sha256Hex(node_fs_1.default.readFileSync(file))}`;
108
- }
109
- /** Walk a path and sum file sizes IN-PROCESS (no `du`). Returns 0 if absent. A
110
- * file returns its own size; a dir returns the recursive sum. */
111
- function dirBytes(p) {
112
- let total = 0;
113
- let stat;
114
- try {
115
- stat = node_fs_1.default.statSync(p);
116
- }
117
- catch {
118
- return 0;
119
- }
120
- if (stat.isFile())
121
- return stat.size;
122
- if (!stat.isDirectory())
123
- return 0;
124
- for (const entry of node_fs_1.default.readdirSync(p, { withFileTypes: true })) {
125
- total += dirBytes(node_path_1.default.join(p, entry.name));
126
- }
127
- return total;
128
- }
129
- /** Stable content digest of a path (file = its bytes; dir = digest over each
130
- * member's relative path + bytes, sorted). Lets the freed-manifest record a
131
- * single sha per freed dir. */
132
- function contentDigest(p) {
133
- const stat = node_fs_1.default.statSync(p);
134
- if (stat.isFile())
135
- return sha256OfFile(p);
136
- const parts = [];
137
- const walk = (dir, rel) => {
138
- for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true }).sort((a, b) => (0, compare_1.compareBytes)(a.name, b.name))) {
139
- const abs = node_path_1.default.join(dir, entry.name);
140
- const r = node_path_1.default.join(rel, entry.name);
141
- if (entry.isDirectory())
142
- walk(abs, r);
143
- else
144
- parts.push(`${r}:${sha256OfFile(abs)}`);
145
- }
146
- };
147
- walk(p, "");
148
- return sha256OfString(parts.join("\n"));
149
- }
150
98
  /** Persist a run's authoritative state.json DURABLY (atomic temp → fsync →
151
99
  * rename). The re-point that scratch reclamation depends on MUST be persisted
152
100
  * this way BEFORE any byte is freed — see prepareFree(). */
@@ -219,13 +167,13 @@ function digestEvidenceEntry(entry) {
219
167
  try {
220
168
  const stat = node_fs_1.default.statSync(candidatePath);
221
169
  if (stat.isFile())
222
- return { ref, digest: sha256OfFile(candidatePath) };
170
+ return { ref, digest: (0, hash_1.sha256OfFile)(candidatePath) };
223
171
  }
224
172
  catch {
225
173
  /* fall through to locator digest */
226
174
  }
227
175
  }
228
- return { ref, digest: sha256OfString(ref) };
176
+ return { ref, digest: (0, hash_1.sha256OfString)(ref) };
229
177
  }
230
178
  /** STEP 1: extract + seal the skeleton. Pure read over the run; never mutates. */
231
179
  function extractSkeleton(run) {
@@ -263,7 +211,7 @@ function extractSkeleton(run) {
263
211
  .map(([ref, digest]) => ({ ref, digest }))
264
212
  .sort((a, b) => (0, compare_1.compareBytes)(a.ref, b.ref));
265
213
  const eventLog = auditEventLogPath(run);
266
- const auditLogDigest = node_fs_1.default.existsSync(eventLog) ? sha256OfFile(eventLog) : sha256OfString("");
214
+ const auditLogDigest = node_fs_1.default.existsSync(eventLog) ? (0, hash_1.sha256OfFile)(eventLog) : (0, hash_1.sha256OfString)("");
267
215
  const events = node_fs_1.default.existsSync(eventLog)
268
216
  ? node_fs_1.default
269
217
  .readFileSync(eventLog, "utf8")
@@ -283,18 +231,18 @@ function extractSkeleton(run) {
283
231
  const metricsReport = node_path_1.default.join(run.paths.runDir, "metrics", "metrics-report.json");
284
232
  const costRecord = {
285
233
  tasks: (run.tasks || []).map((task) => ({ taskId: task.id, model: task.usage?.model, source: task.usage?.source })),
286
- metricsDigest: node_fs_1.default.existsSync(metricsReport) ? sha256OfFile(metricsReport) : undefined
234
+ metricsDigest: node_fs_1.default.existsSync(metricsReport) ? (0, hash_1.sha256OfFile)(metricsReport) : undefined
287
235
  };
288
236
  const collaboration = run.collaboration;
289
237
  const collaborationLog = {
290
- digest: sha256OfString((0, multi_agent_eval_1.replayStableStringify)(collaboration || {})),
238
+ digest: (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)(collaboration || {})),
291
239
  approvals: collaboration?.approvals?.length || 0,
292
240
  comments: collaboration?.comments?.length || 0,
293
241
  handoffs: collaboration?.handoffs?.length || 0
294
242
  };
295
243
  // Empty (not a hash-of-empty) when state.json is absent, so the skeleton fails
296
244
  // closed — you cannot seal a run whose authoritative state is gone.
297
- const stateDigest = node_fs_1.default.existsSync(run.paths.state) ? sha256OfFile(run.paths.state) : "";
245
+ const stateDigest = node_fs_1.default.existsSync(run.paths.state) ? (0, hash_1.sha256OfFile)(run.paths.state) : "";
298
246
  return {
299
247
  schemaVersion: 1,
300
248
  runId: run.id,
@@ -397,46 +345,17 @@ function buildReferenceGraph(run) {
397
345
  add(message.id);
398
346
  return refs;
399
347
  }
348
+ /** expectDigest of a node's deterministic projection. Re-uses the SHARED
349
+ * node-projection field set (node-projection.ts) so reconstruction matches
350
+ * node-snapshot.ts's body byte-for-byte — the projection can no longer drift. */
400
351
  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));
352
+ return (0, hash_1.sha256OfString)((0, node_projection_1.nodeProjectionDigestInput)(node));
418
353
  }
419
354
  /** Body digest of the RETAINED node (lives in state.json). The reconstruction
420
- * verifier re-derives the projection from this retained input. */
355
+ * verifier re-derives the projection from this retained input. Same shared field
356
+ * set / canonical bytes as snapshotProjectionDigest. */
421
357
  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
- };
358
+ return (0, hash_1.sha256OfString)((0, node_projection_1.nodeProjectionDigestInput)(node));
440
359
  }
441
360
  /** Build the retention plan: which paths are freeable under `policy`, of what
442
361
  * kind, how many bytes, and the resulting capability downgrade. */
@@ -459,7 +378,7 @@ function planReclamation(run, policy = {}) {
459
378
  const resultsCopy = task?.resultPath;
460
379
  if (!resultNodeId || !resultsCopy || !node_fs_1.default.existsSync(resultsCopy))
461
380
  continue;
462
- const bytes = dirBytes(workerDir);
381
+ const bytes = (0, hash_1.dirBytes)(workerDir);
463
382
  if (bytes <= 0)
464
383
  continue;
465
384
  freeable.push({
@@ -506,14 +425,14 @@ function planReclamation(run, policy = {}) {
506
425
  continue; // source node gone → cannot reconstruct → retain
507
426
  if (repointNodeIds.has(node.id))
508
427
  continue; // body will be re-pointed → retain
509
- const bytes = dirBytes(snapFile);
428
+ const bytes = (0, hash_1.dirBytes)(snapFile);
510
429
  if (bytes <= 0)
511
430
  continue;
512
431
  const inputDigest = nodeBodyDigest(node);
513
432
  const recipe = {
514
433
  recipeKind: "node-snapshot-projection",
515
434
  inputDigests: [inputDigest],
516
- inputsDigest: sha256OfString((0, multi_agent_eval_1.replayStableStringify)([inputDigest])),
435
+ inputsDigest: (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)([inputDigest])),
517
436
  expectDigest: snapshotProjectionDigest(node),
518
437
  sourceRef: node.id
519
438
  };
@@ -562,11 +481,11 @@ function planReclamation(run, policy = {}) {
562
481
  return { freeable, bytesToFree, byKind, capability, capabilityReason };
563
482
  }
564
483
  function policyDigestOf(policy) {
565
- return sha256OfString((0, multi_agent_eval_1.replayStableStringify)(policy));
484
+ return (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)(policy));
566
485
  }
567
486
  /** genesis prevTombstoneHash = sha256 of the sealed skeleton. */
568
487
  function genesisPrevHash(skeleton) {
569
- return sha256OfString((0, multi_agent_eval_1.replayStableStringify)(skeleton));
488
+ return (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)(skeleton));
570
489
  }
571
490
  /** The canonical bytes a tombstoneHash binds: freed-manifest + sealed skeleton +
572
491
  * prevTombstoneHash + capability. Recomputed independently by `gc verify`. */
@@ -579,14 +498,14 @@ function tombstoneHashInput(t) {
579
498
  policyDigest: t.policyDigest,
580
499
  freed: t.freed.map((f) => ({ path: f.path, kind: f.kind, bytes: f.bytes, sha256: f.sha256, recipe: f.recipe || null })),
581
500
  bytesFreed: t.bytesFreed,
582
- skeletonDigest: sha256OfString((0, multi_agent_eval_1.replayStableStringify)(t.skeleton)),
501
+ skeletonDigest: (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)(t.skeleton)),
583
502
  capability: t.capability,
584
503
  capabilityReason: t.capabilityReason,
585
504
  prevTombstoneHash: t.prevTombstoneHash
586
505
  });
587
506
  }
588
507
  function computeTombstoneHash(t) {
589
- return sha256OfString(tombstoneHashInput(t));
508
+ return (0, hash_1.sha256OfString)(tombstoneHashInput(t));
590
509
  }
591
510
  function tombstoneId(seq) {
592
511
  // Deterministic (FreeBSD-audit L13): the chain POSITION, not a process-global
@@ -604,7 +523,7 @@ function buildTombstone(run, skeleton, plan, options = {}) {
604
523
  path: entry.path,
605
524
  kind: entry.kind,
606
525
  bytes: entry.bytes,
607
- sha256: contentDigest(entry.absPath),
526
+ sha256: (0, hash_1.contentDigest)(entry.absPath),
608
527
  recipe: entry.recipe
609
528
  }));
610
529
  const base = {
@@ -713,7 +632,7 @@ function freeBulk(run, tombstone) {
713
632
  let freedBytes = 0;
714
633
  for (const entry of tombstone.freed) {
715
634
  const abs = node_path_1.default.join(runDir, entry.path);
716
- const before = dirBytes(abs);
635
+ const before = (0, hash_1.dirBytes)(abs);
717
636
  node_fs_1.default.rmSync(abs, { recursive: true, force: true });
718
637
  freedBytes += before;
719
638
  }
@@ -802,15 +721,15 @@ function reconstructArtifact(run, recipe) {
802
721
  if (recipe.recipeKind === "node-snapshot-projection") {
803
722
  const node = (run.nodes || []).find((n) => n.id === recipe.sourceRef);
804
723
  if (!node) {
805
- return { inputsDigest: sha256OfString("absent"), expectDigest: sha256OfString("absent") };
724
+ return { inputsDigest: (0, hash_1.sha256OfString)("absent"), expectDigest: (0, hash_1.sha256OfString)("absent") };
806
725
  }
807
726
  const inputDigest = nodeBodyDigest(node);
808
- const inputsDigest = sha256OfString((0, multi_agent_eval_1.replayStableStringify)([inputDigest]));
727
+ const inputsDigest = (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)([inputDigest]));
809
728
  const expectDigest = snapshotProjectionDigest(node);
810
729
  return { inputsDigest, expectDigest };
811
730
  }
812
731
  // Unknown recipe kind → fail closed (digest can't match expectDigest).
813
- return { inputsDigest: sha256OfString("unknown-recipe"), expectDigest: sha256OfString("unknown-recipe") };
732
+ return { inputsDigest: (0, hash_1.sha256OfString)("unknown-recipe"), expectDigest: (0, hash_1.sha256OfString)("unknown-recipe") };
814
733
  }
815
734
  /** Re-prove the whole reclamation chain for a run: skeleton schema-complete,
816
735
  * tombstoneHash/prevTombstoneHash chain recomputed-and-untampered, and each
@@ -33,15 +33,14 @@ function queueFilePath(host) {
33
33
  }
34
34
  function loadQueue(host) {
35
35
  const file = queueFilePath(host);
36
+ // Absent => empty queue. A present-but-corrupt queue must FAIL CLOSED rather
37
+ // than read as empty: silently draining to [] would lose every queued run and
38
+ // let scheduling/lease ops proceed as if the store were clean. readJson throws
39
+ // `Invalid JSON in <file>` on a present, unparseable store; let it propagate.
36
40
  if (!node_fs_1.default.existsSync(file))
37
41
  return [];
38
- try {
39
- const parsed = (0, state_1.readJson)(file);
40
- return Array.isArray(parsed.entries) ? parsed.entries : [];
41
- }
42
- catch {
43
- return [];
44
- }
42
+ const parsed = (0, state_1.readJson)(file);
43
+ return Array.isArray(parsed.entries) ? parsed.entries : [];
45
44
  }
46
45
  function saveQueue(host, entries) {
47
46
  (0, state_1.writeJson)(queueFilePath(host), { schemaVersion: 1, entries }, { durable: true });