cool-workflow 0.1.80 → 0.1.81

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 (110) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +42 -2
  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/agent-config.js +21 -7
  11. package/dist/candidate-scoring.js +42 -22
  12. package/dist/capability-core.js +94 -17
  13. package/dist/capability-registry.js +138 -171
  14. package/dist/cli.js +90 -100
  15. package/dist/collaboration.js +5 -6
  16. package/dist/commit.js +20 -6
  17. package/dist/compare.js +18 -0
  18. package/dist/coordinator/classify.js +45 -0
  19. package/dist/coordinator/paths.js +42 -0
  20. package/dist/coordinator/util.js +129 -0
  21. package/dist/coordinator.js +127 -300
  22. package/dist/dispatch.js +35 -0
  23. package/dist/drive.js +7 -7
  24. package/dist/error-feedback.js +8 -4
  25. package/dist/evidence-reasoning.js +1 -1
  26. package/dist/execution-backend/agent.js +331 -0
  27. package/dist/execution-backend/probes.js +96 -0
  28. package/dist/execution-backend/util.js +47 -0
  29. package/dist/execution-backend.js +67 -420
  30. package/dist/mcp-server.js +34 -173
  31. package/dist/multi-agent/graph.js +84 -0
  32. package/dist/multi-agent/helpers.js +145 -0
  33. package/dist/multi-agent/paths.js +22 -0
  34. package/dist/multi-agent-eval/format.js +194 -0
  35. package/dist/multi-agent-eval/normalize.js +51 -0
  36. package/dist/multi-agent-eval.js +39 -244
  37. package/dist/multi-agent-host.js +0 -19
  38. package/dist/multi-agent.js +125 -314
  39. package/dist/node-snapshot.js +3 -3
  40. package/dist/observability/format.js +61 -0
  41. package/dist/observability/intake.js +98 -0
  42. package/dist/observability.js +14 -160
  43. package/dist/operator-ux/format.js +364 -0
  44. package/dist/operator-ux.js +22 -363
  45. package/dist/orchestrator/report.js +8 -0
  46. package/dist/orchestrator.js +25 -8
  47. package/dist/reclamation.js +26 -21
  48. package/dist/run-export.js +138 -14
  49. package/dist/run-registry/derive.js +172 -0
  50. package/dist/run-registry/format.js +124 -0
  51. package/dist/run-registry/gc.js +251 -0
  52. package/dist/run-registry/policy.js +16 -0
  53. package/dist/run-registry/queue.js +116 -0
  54. package/dist/run-registry.js +78 -593
  55. package/dist/run-state-schema.js +1 -0
  56. package/dist/sandbox-profile.js +43 -2
  57. package/dist/state-explosion/format.js +159 -0
  58. package/dist/state-explosion/helpers.js +82 -0
  59. package/dist/state-explosion.js +65 -283
  60. package/dist/state-node.js +19 -4
  61. package/dist/telemetry-attestation.js +55 -0
  62. package/dist/telemetry-demo.js +15 -3
  63. package/dist/telemetry-ledger.js +60 -15
  64. package/dist/topology.js +25 -8
  65. package/dist/triggers.js +33 -14
  66. package/dist/trust-audit.js +145 -33
  67. package/dist/version.js +1 -1
  68. package/dist/worker-isolation/helpers.js +51 -0
  69. package/dist/worker-isolation/paths.js +46 -0
  70. package/dist/worker-isolation.js +39 -115
  71. package/docs/agent-delegation-drive.7.md +13 -0
  72. package/docs/cli-mcp-parity.7.md +4 -0
  73. package/docs/contract-migration-tooling.7.md +2 -0
  74. package/docs/control-plane-scheduling.7.md +2 -0
  75. package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +40 -0
  76. package/docs/durable-state-and-locking.7.md +4 -0
  77. package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
  78. package/docs/execution-backends.7.md +2 -0
  79. package/docs/index.md +1 -0
  80. package/docs/launch/launch-kit.md +46 -23
  81. package/docs/launch/pre-launch-checklist.md +14 -14
  82. package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
  83. package/docs/multi-agent-eval-replay-harness.7.md +2 -0
  84. package/docs/multi-agent-operator-ux.7.md +2 -0
  85. package/docs/multi-agent-trust-policy-audit.7.md +27 -0
  86. package/docs/node-snapshot-diff-replay.7.md +2 -0
  87. package/docs/observability-cost-accounting.7.md +2 -0
  88. package/docs/project-index.md +18 -5
  89. package/docs/real-execution-backends.7.md +2 -0
  90. package/docs/release-and-migration.7.md +4 -0
  91. package/docs/release-tooling.7.md +2 -0
  92. package/docs/run-registry-control-plane.7.md +54 -8
  93. package/docs/run-retention-reclamation.7.md +4 -0
  94. package/docs/state-explosion-management.7.md +2 -0
  95. package/docs/team-collaboration.7.md +2 -0
  96. package/docs/trust-model.md +267 -0
  97. package/docs/vendor-manifest-loadability.7.md +43 -0
  98. package/docs/web-desktop-workbench.7.md +2 -0
  99. package/manifest/plugin.manifest.json +1 -1
  100. package/package.json +4 -2
  101. package/scripts/agents/builtin-templates.json +7 -0
  102. package/scripts/bump-version.js +5 -11
  103. package/scripts/canonical-apps-list.js +64 -0
  104. package/scripts/canonical-apps.js +19 -4
  105. package/scripts/dogfood-release.js +1 -1
  106. package/scripts/golden-path.js +4 -4
  107. package/scripts/parity-check.js +5 -0
  108. package/scripts/release-check.js +5 -1
  109. package/scripts/version-sync-check.js +5 -8
  110. package/dist/capability-dispatcher.js +0 -86
@@ -29,7 +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 = exports.RECLAMATION_SCHEMA_VERSION = void 0;
32
+ exports.ReclamationError = exports.ReclamationAbort = exports.SKELETON_REQUIRED_KEYS = void 0;
33
33
  exports.sha256OfString = sha256OfString;
34
34
  exports.sha256OfFile = sha256OfFile;
35
35
  exports.dirBytes = dirBytes;
@@ -56,7 +56,7 @@ const multi_agent_eval_1 = require("./multi-agent-eval");
56
56
  const node_snapshot_1 = require("./node-snapshot");
57
57
  const state_1 = require("./state");
58
58
  const trust_audit_1 = require("./trust-audit");
59
- exports.RECLAMATION_SCHEMA_VERSION = 1;
59
+ const compare_1 = require("./compare");
60
60
  /** The skeleton schema is the contract for what MUST survive every reclamation.
61
61
  * Machine-checkable via validateSkeleton(). If extraction can't produce all of
62
62
  * these, reclamation fails closed and frees nothing. */
@@ -135,7 +135,7 @@ function contentDigest(p) {
135
135
  return sha256OfFile(p);
136
136
  const parts = [];
137
137
  const walk = (dir, rel) => {
138
- for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
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
139
  const abs = node_path_1.default.join(dir, entry.name);
140
140
  const r = node_path_1.default.join(rel, entry.name);
141
141
  if (entry.isDirectory())
@@ -261,7 +261,7 @@ function extractSkeleton(run) {
261
261
  }
262
262
  const evidenceDigests = [...evidenceMap.entries()]
263
263
  .map(([ref, digest]) => ({ ref, digest }))
264
- .sort((a, b) => a.ref.localeCompare(b.ref));
264
+ .sort((a, b) => (0, compare_1.compareBytes)(a.ref, b.ref));
265
265
  const eventLog = auditEventLogPath(run);
266
266
  const auditLogDigest = node_fs_1.default.existsSync(eventLog) ? sha256OfFile(eventLog) : sha256OfString("");
267
267
  const events = node_fs_1.default.existsSync(eventLog)
@@ -287,7 +287,7 @@ function extractSkeleton(run) {
287
287
  };
288
288
  const collaboration = run.collaboration;
289
289
  const collaborationLog = {
290
- digest: sha256OfString((0, multi_agent_eval_1.stableStringify)(collaboration || {})),
290
+ digest: sha256OfString((0, multi_agent_eval_1.replayStableStringify)(collaboration || {})),
291
291
  approvals: collaboration?.approvals?.length || 0,
292
292
  comments: collaboration?.comments?.length || 0,
293
293
  handoffs: collaboration?.handoffs?.length || 0
@@ -414,12 +414,12 @@ function snapshotProjectionDigest(node) {
414
414
  contractId: node.contractId,
415
415
  metadata: node.metadata
416
416
  });
417
- return sha256OfString((0, multi_agent_eval_1.stableStringify)(body));
417
+ return sha256OfString((0, multi_agent_eval_1.replayStableStringify)(body));
418
418
  }
419
419
  /** Body digest of the RETAINED node (lives in state.json). The reconstruction
420
420
  * verifier re-derives the projection from this retained input. */
421
421
  function nodeBodyDigest(node) {
422
- return sha256OfString((0, multi_agent_eval_1.stableStringify)(rawNodeBody(node)));
422
+ return sha256OfString((0, multi_agent_eval_1.replayStableStringify)(rawNodeBody(node)));
423
423
  }
424
424
  function rawNodeBody(node) {
425
425
  return {
@@ -449,7 +449,6 @@ function planReclamation(run, policy = {}) {
449
449
  // freeable once the result node's worker-result artifact is re-pointed.
450
450
  let reclaimedScratch = false;
451
451
  if (!policy.keepScratch) {
452
- const workersDir = run.paths.workersDir || node_path_1.default.join(runDir, "workers");
453
452
  for (const scope of run.workers || []) {
454
453
  const workerDir = scope.workerDir;
455
454
  if (!workerDir || !node_fs_1.default.existsSync(workerDir))
@@ -472,7 +471,6 @@ function planReclamation(run, policy = {}) {
472
471
  });
473
472
  reclaimedScratch = true;
474
473
  }
475
- void workersDir;
476
474
  }
477
475
  // A node whose scratch is being re-pointed THIS pass must NOT also have its
478
476
  // snapshot freed in the same pass — re-pointing mutates the node body, which
@@ -515,7 +513,7 @@ function planReclamation(run, policy = {}) {
515
513
  const recipe = {
516
514
  recipeKind: "node-snapshot-projection",
517
515
  inputDigests: [inputDigest],
518
- inputsDigest: sha256OfString((0, multi_agent_eval_1.stableStringify)([inputDigest])),
516
+ inputsDigest: sha256OfString((0, multi_agent_eval_1.replayStableStringify)([inputDigest])),
519
517
  expectDigest: snapshotProjectionDigest(node),
520
518
  sourceRef: node.id
521
519
  };
@@ -531,6 +529,13 @@ function planReclamation(run, policy = {}) {
531
529
  // retention, and we do not yet auto-capture reconstruction recipes for them.
532
530
  // The reference graph is consulted so the door is closed, not merely unbuilt.
533
531
  void buildReferenceGraph;
532
+ // Determinism (HARD constraint): the snapshot candidates above are gathered in
533
+ // fs.readdirSync order, which is filesystem-dependent. freeable feeds the freed
534
+ // manifest that buildTombstone binds into tombstoneHash (and the prevTombstoneHash
535
+ // chain), so an unsorted order makes the tombstone hash irreproducible across
536
+ // hosts. Sort by path — the same compareBytes discipline the directory reads at
537
+ // :128 and the reference list at :243 already use — before anything hashes it.
538
+ freeable.sort((a, b) => (0, compare_1.compareBytes)(a.path, b.path));
534
539
  const byKind = {};
535
540
  let bytesToFree = 0;
536
541
  for (const entry of freeable) {
@@ -557,16 +562,16 @@ function planReclamation(run, policy = {}) {
557
562
  return { freeable, bytesToFree, byKind, capability, capabilityReason };
558
563
  }
559
564
  function policyDigestOf(policy) {
560
- return sha256OfString((0, multi_agent_eval_1.stableStringify)(policy));
565
+ return sha256OfString((0, multi_agent_eval_1.replayStableStringify)(policy));
561
566
  }
562
567
  /** genesis prevTombstoneHash = sha256 of the sealed skeleton. */
563
568
  function genesisPrevHash(skeleton) {
564
- return sha256OfString((0, multi_agent_eval_1.stableStringify)(skeleton));
569
+ return sha256OfString((0, multi_agent_eval_1.replayStableStringify)(skeleton));
565
570
  }
566
571
  /** The canonical bytes a tombstoneHash binds: freed-manifest + sealed skeleton +
567
572
  * prevTombstoneHash + capability. Recomputed independently by `gc verify`. */
568
573
  function tombstoneHashInput(t) {
569
- return (0, multi_agent_eval_1.stableStringify)({
574
+ return (0, multi_agent_eval_1.replayStableStringify)({
570
575
  runId: t.runId,
571
576
  tombstoneId: t.tombstoneId,
572
577
  reclaimedAt: t.reclaimedAt,
@@ -574,7 +579,7 @@ function tombstoneHashInput(t) {
574
579
  policyDigest: t.policyDigest,
575
580
  freed: t.freed.map((f) => ({ path: f.path, kind: f.kind, bytes: f.bytes, sha256: f.sha256, recipe: f.recipe || null })),
576
581
  bytesFreed: t.bytesFreed,
577
- skeletonDigest: sha256OfString((0, multi_agent_eval_1.stableStringify)(t.skeleton)),
582
+ skeletonDigest: sha256OfString((0, multi_agent_eval_1.replayStableStringify)(t.skeleton)),
578
583
  capability: t.capability,
579
584
  capabilityReason: t.capabilityReason,
580
585
  prevTombstoneHash: t.prevTombstoneHash
@@ -583,11 +588,11 @@ function tombstoneHashInput(t) {
583
588
  function computeTombstoneHash(t) {
584
589
  return sha256OfString(tombstoneHashInput(t));
585
590
  }
586
- let tombstoneCounter = 0;
587
- function tombstoneId(run, now) {
588
- tombstoneCounter += 1;
589
- const stamp = now.replace(/[-:.TZ]/g, "").slice(0, 14);
590
- return `tomb-${stamp}-${String(tombstoneCounter).padStart(3, "0")}`;
591
+ function tombstoneId(seq) {
592
+ // Deterministic (FreeBSD-audit L13): the chain POSITION, not a process-global
593
+ // counter or wall-clock stamp — tombstoneId is bound into the tombstoneHash
594
+ // chain that `gc verify` recomputes, so it must be reproducible.
595
+ return `tomb-${String(seq).padStart(3, "0")}`;
591
596
  }
592
597
  /** STEP 2: build the FULL tombstone (pre-deletion sha256 per freed path + the
593
598
  * hash chain). Reads the freed files (still present); mutates nothing on disk. */
@@ -605,7 +610,7 @@ function buildTombstone(run, skeleton, plan, options = {}) {
605
610
  const base = {
606
611
  schemaVersion: 1,
607
612
  runId: run.id,
608
- tombstoneId: tombstoneId(run, now),
613
+ tombstoneId: tombstoneId(prior.length + 1),
609
614
  reclaimedAt: now,
610
615
  actor: options.actor,
611
616
  policyDigest: policyDigestOf(options.policy || {}),
@@ -800,7 +805,7 @@ function reconstructArtifact(run, recipe) {
800
805
  return { inputsDigest: sha256OfString("absent"), expectDigest: sha256OfString("absent") };
801
806
  }
802
807
  const inputDigest = nodeBodyDigest(node);
803
- const inputsDigest = sha256OfString((0, multi_agent_eval_1.stableStringify)([inputDigest]));
808
+ const inputsDigest = sha256OfString((0, multi_agent_eval_1.replayStableStringify)([inputDigest]));
804
809
  const expectDigest = snapshotProjectionDigest(node);
805
810
  return { inputsDigest, expectDigest };
806
811
  }
@@ -14,6 +14,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
14
14
  exports.exportRun = exportRun;
15
15
  exports.importRun = importRun;
16
16
  exports.verifyImportedRun = verifyImportedRun;
17
+ exports.inspectArchive = inspectArchive;
17
18
  exports.importManifestPath = importManifestPath;
18
19
  const node_fs_1 = __importDefault(require("node:fs"));
19
20
  const node_path_1 = __importDefault(require("node:path"));
@@ -21,6 +22,8 @@ const node_crypto_1 = __importDefault(require("node:crypto"));
21
22
  const state_1 = require("./state");
22
23
  const version_1 = require("./version");
23
24
  const telemetry_ledger_1 = require("./telemetry-ledger");
25
+ const trust_audit_1 = require("./trust-audit");
26
+ const compare_1 = require("./compare");
24
27
  /** Export a run to a portable JSON archive with run-local bytes and digests. */
25
28
  function exportRun(run, outputPath) {
26
29
  const exportedAt = new Date().toISOString();
@@ -187,6 +190,17 @@ function verifyImportedRun(run) {
187
190
  pass: telemetry.verified,
188
191
  code: telemetry.verified ? undefined : "telemetry-ledger-invalid"
189
192
  });
193
+ // Re-prove the trust-audit hash chain on restore too. Telemetry was already
194
+ // re-proven above, but the decisions/sandbox/commit-gate audit chain — also
195
+ // exported under audit/ — was not, an asymmetry a tampered restore could slip
196
+ // through. An absent chain is verified:true (nothing to prove), so archives
197
+ // predating audit export append a PASSING check — no false-red.
198
+ const audit = (0, trust_audit_1.verifyTrustAudit)(run);
199
+ checks.push({
200
+ name: "trust-audit",
201
+ pass: audit.verified,
202
+ code: audit.verified ? undefined : "trust-audit-invalid"
203
+ });
190
204
  return {
191
205
  runId: run.id,
192
206
  ok: checks.every((check) => check.pass),
@@ -195,6 +209,70 @@ function verifyImportedRun(run) {
195
209
  checks
196
210
  };
197
211
  }
212
+ /** Read-only integrity inspection of a portable archive WITHOUT importing it. Never
213
+ * throws — a read error, bad JSON, unsupported schema, or any digest/size/count/
214
+ * manifest mismatch is reported as a structured check with ok:false. Writes nothing. */
215
+ function inspectArchive(archivePath) {
216
+ const base = {
217
+ schemaVersion: 1,
218
+ archivePath,
219
+ ok: false,
220
+ schemaSupported: false,
221
+ runId: null,
222
+ fileCount: 0,
223
+ manifestSha256: null,
224
+ archiveSha256: null,
225
+ checks: []
226
+ };
227
+ let bytes;
228
+ try {
229
+ bytes = node_fs_1.default.readFileSync(archivePath);
230
+ }
231
+ catch (error) {
232
+ return { ...base, checks: [{ name: "archive", pass: false, code: "archive-unreadable", path: archivePath, actual: messageOf(error) }] };
233
+ }
234
+ base.archiveSha256 = sha256Bytes(bytes);
235
+ let raw;
236
+ try {
237
+ raw = JSON.parse(bytes.toString("utf8"));
238
+ }
239
+ catch (error) {
240
+ return { ...base, checks: [{ name: "archive", pass: false, code: "archive-invalid-json", path: archivePath, actual: messageOf(error) }] };
241
+ }
242
+ if (raw.schemaVersion !== 1) {
243
+ return {
244
+ ...base,
245
+ schemaVersion: typeof raw.schemaVersion === "number" ? raw.schemaVersion : base.schemaVersion,
246
+ checks: [{ name: "schema", pass: false, code: "unsupported-schema", expected: "1", actual: String(raw.schemaVersion) }]
247
+ };
248
+ }
249
+ try {
250
+ const files = normalizeArchiveFiles(raw);
251
+ const { checks } = collectArchiveDigestChecks(files, raw.integrity);
252
+ // Faithful preview of what `run import` would do under the same env: with
253
+ // CW_REQUIRE_ARCHIVE_INTEGRITY=1 a stripped-integrity archive is refused by
254
+ // import, so inspect must also report it as failing (ok:false / exit 1) rather
255
+ // than green — otherwise inspect-before-import is misleading in that policy.
256
+ // Default (env unset) is unchanged: inspection only reports the digest checks.
257
+ if (!raw.integrity && /^(1|true|yes|on)$/i.test(process.env.CW_REQUIRE_ARCHIVE_INTEGRITY || "")) {
258
+ checks.push({ name: "archive-integrity", pass: false, code: "archive-integrity-required" });
259
+ }
260
+ return {
261
+ schemaVersion: 1,
262
+ archivePath,
263
+ ok: checks.every((c) => c.pass),
264
+ schemaSupported: true,
265
+ runId: raw.run && raw.run.id ? raw.run.id : null,
266
+ fileCount: files.length,
267
+ manifestSha256: raw.integrity ? digestManifest(files) : null,
268
+ archiveSha256: base.archiveSha256,
269
+ checks
270
+ };
271
+ }
272
+ catch (error) {
273
+ return { ...base, schemaSupported: true, checks: [{ name: "archive", pass: false, code: "archive-malformed", path: archivePath, actual: messageOf(error) }] };
274
+ }
275
+ }
198
276
  function importManifestPath(run) {
199
277
  return node_path_1.default.join(run.paths.runDir, "import-manifest.json");
200
278
  }
@@ -216,7 +294,7 @@ function collectArchiveFiles(run) {
216
294
  if ((0, state_1.isContainedPath)(artifactPath, run.cwd))
217
295
  addExternalArtifactFile(entries, run, artifactPath);
218
296
  }
219
- return [...entries.values()].sort((left, right) => left.relativePath.localeCompare(right.relativePath));
297
+ return [...entries.values()].sort((left, right) => (0, compare_1.compareBytes)(left.relativePath, right.relativePath));
220
298
  }
221
299
  function addFile(entries, run, file, role) {
222
300
  const relativePath = toArchivePath(node_path_1.default.relative(run.paths.runDir, file));
@@ -318,34 +396,80 @@ function normalizeArchiveFiles(raw) {
318
396
  };
319
397
  });
320
398
  }
321
- function verifyArchiveFileDigests(files, integrity) {
399
+ /** NON-throwing digest/size/count/manifest verification: one structured check per
400
+ * file (in import order), then the integrity file-count + manifest checks. Shared
401
+ * by the throwing import path (verifyArchiveFileDigests) and the read-only
402
+ * inspectArchive, so a single offender list has one source of truth. */
403
+ function collectArchiveDigestChecks(files, integrity) {
404
+ const checks = [];
322
405
  for (const file of files) {
323
406
  const bytes = Buffer.from(file.contentBase64, "base64");
324
407
  const actual = sha256Bytes(bytes);
325
- if (actual !== file.sha256)
326
- throw new Error(`Archive digest mismatch for ${file.relativePath}: expected ${file.sha256}, got ${actual}`);
327
- if (bytes.length !== file.sizeBytes)
328
- throw new Error(`Archive size mismatch for ${file.relativePath}: expected ${file.sizeBytes}, got ${bytes.length}`);
408
+ const digestOk = actual === file.sha256;
409
+ checks.push(digestOk
410
+ ? { name: "archive-file", pass: true, path: file.relativePath }
411
+ : { name: "archive-file", pass: false, code: "digest-mismatch", path: file.relativePath, expected: file.sha256, actual });
412
+ const sizeOk = bytes.length === file.sizeBytes;
413
+ checks.push(sizeOk
414
+ ? { name: "archive-file", pass: true, path: file.relativePath }
415
+ : { name: "archive-file", pass: false, code: "size-mismatch", path: file.relativePath, expected: String(file.sizeBytes), actual: String(bytes.length) });
329
416
  }
330
417
  if (integrity) {
418
+ const countOk = integrity.fileCount === files.length;
419
+ checks.push(countOk
420
+ ? { name: "archive-file-count", pass: true }
421
+ : { name: "archive-file-count", pass: false, code: "file-count-mismatch", expected: String(integrity.fileCount), actual: String(files.length) });
331
422
  const actualManifest = digestManifest(files);
332
- if (integrity.fileCount !== files.length)
333
- throw new Error(`Archive file count mismatch: expected ${integrity.fileCount}, got ${files.length}`);
334
- if (integrity.manifestSha256 !== actualManifest) {
335
- throw new Error(`Archive manifest digest mismatch: expected ${integrity.manifestSha256}, got ${actualManifest}`);
336
- }
423
+ const manifestOk = integrity.manifestSha256 === actualManifest;
424
+ checks.push(manifestOk
425
+ ? { name: "archive-manifest", pass: true }
426
+ : { name: "archive-manifest", pass: false, code: "manifest-digest-mismatch", expected: integrity.manifestSha256, actual: actualManifest });
427
+ }
428
+ return { checks, ok: checks.every((c) => c.pass) };
429
+ }
430
+ /** Reconstruct the legacy throw message for a failing check, so the throwing import
431
+ * path stays BYTE-IDENTICAL after the collector refactor. */
432
+ function archiveCheckMessage(check) {
433
+ switch (check.code) {
434
+ case "digest-mismatch": return `Archive digest mismatch for ${check.path}: expected ${check.expected}, got ${check.actual}`;
435
+ case "size-mismatch": return `Archive size mismatch for ${check.path}: expected ${check.expected}, got ${check.actual}`;
436
+ case "file-count-mismatch": return `Archive file count mismatch: expected ${check.expected}, got ${check.actual}`;
437
+ case "manifest-digest-mismatch": return `Archive manifest digest mismatch: expected ${check.expected}, got ${check.actual}`;
438
+ default: return `Archive verification failed: ${check.name}`;
439
+ }
440
+ }
441
+ function verifyArchiveFileDigests(files, integrity) {
442
+ // Opt-in hardening (CW_REQUIRE_ARCHIVE_INTEGRITY=1): refuse an archive whose
443
+ // top-level integrity block (manifest digest + file count) is absent, closing the
444
+ // legacy fail-open seam where a stripped-integrity archive imported unverified.
445
+ // Same env-boolish convention as CW_REQUIRE_RESOLVABLE_EVIDENCE (evidence-grounding.ts:57).
446
+ // Default (unset) keeps legacy integrity-less archives byte-identical.
447
+ if (!integrity && /^(1|true|yes|on)$/i.test(process.env.CW_REQUIRE_ARCHIVE_INTEGRITY || "")) {
448
+ throw new Error("Archive integrity block required but absent (CW_REQUIRE_ARCHIVE_INTEGRITY=1)");
337
449
  }
450
+ // Throw-before-write preserved: throw on the FIRST failing check, in the same
451
+ // order (per-file digest then size, then file-count, then manifest) and with the
452
+ // same message the inline checks produced.
453
+ const failed = collectArchiveDigestChecks(files, integrity).checks.find((c) => !c.pass);
454
+ if (failed)
455
+ throw new Error(archiveCheckMessage(failed));
338
456
  }
339
457
  function digestManifest(files) {
340
458
  const manifest = files
459
+ // sourcePath is deliberately EXCLUDED: it is a host-absolute bookkeeping path
460
+ // (for externalPathMap), not integrity-bearing content — the file's bytes are
461
+ // already bound by sha256 + sizeBytes. Including it would make the digest
462
+ // differ across hosts for byte-identical content, defeating cross-host repro.
341
463
  .map((file) => ({
342
464
  relativePath: file.relativePath,
343
465
  role: file.role,
344
466
  sha256: file.sha256,
345
- sizeBytes: file.sizeBytes,
346
- sourcePath: file.sourcePath
467
+ sizeBytes: file.sizeBytes
347
468
  }))
348
- .sort((left, right) => left.relativePath.localeCompare(right.relativePath));
469
+ // Codepoint order, NOT localeCompare: this manifest feeds a sha256 integrity
470
+ // digest. Locale-sensitive collation would order identical bytes differently
471
+ // across hosts/locales, making the digest non-reproducible cross-host.
472
+ .sort((left, right) => (left.relativePath < right.relativePath ? -1 : left.relativePath > right.relativePath ? 1 : 0));
349
473
  return sha256Bytes(Buffer.from(JSON.stringify(manifest), "utf8"));
350
474
  }
351
475
  function rebaseRun(source, context) {
@@ -0,0 +1,172 @@
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.LIFECYCLE_STATES = void 0;
7
+ exports.compareRecords = compareRecords;
8
+ exports.compareHistory = compareHistory;
9
+ exports.compareQueue = compareQueue;
10
+ exports.matchesQuery = matchesQuery;
11
+ exports.distinctBackends = distinctBackends;
12
+ exports.digestInputs = digestInputs;
13
+ exports.countRecords = countRecords;
14
+ exports.optionalLower = optionalLower;
15
+ exports.clampInt = clampInt;
16
+ exports.queueId = queueId;
17
+ exports.isRunLifecycleState = isRunLifecycleState;
18
+ exports.loadReclaimedFromDir = loadReclaimedFromDir;
19
+ // Pure, stateless helpers for the run registry — comparison, query matching,
20
+ // input digesting, counting, and small utilities. Carved out of run-registry.ts
21
+ // (FreeBSD-audit R2) so the stateful RunRegistry class no longer bundles the pure
22
+ // derivation layer. Nothing here touches `this`; everything is a pure function of
23
+ // its arguments (queueId is the lone exception — a process-local counter, kept as
24
+ // it was; making ID minting deterministic is a separate tracked item).
25
+ const node_fs_1 = __importDefault(require("node:fs"));
26
+ const node_path_1 = __importDefault(require("node:path"));
27
+ const compare_1 = require("../compare");
28
+ exports.LIFECYCLE_STATES = [
29
+ "queued",
30
+ "running",
31
+ "blocked",
32
+ "completed",
33
+ "failed",
34
+ "archived",
35
+ "reclaimed"
36
+ ];
37
+ function compareRecords(a, b) {
38
+ if (a.createdAt !== b.createdAt)
39
+ return a.createdAt < b.createdAt ? -1 : 1;
40
+ return (0, compare_1.compareBytes)(a.runId, b.runId);
41
+ }
42
+ function compareHistory(a, b) {
43
+ // Newest first.
44
+ if (a.createdAt !== b.createdAt)
45
+ return a.createdAt < b.createdAt ? 1 : -1;
46
+ return (0, compare_1.compareBytes)(a.runId, b.runId);
47
+ }
48
+ function compareQueue(a, b) {
49
+ if (a.priority !== b.priority)
50
+ return a.priority - b.priority;
51
+ if (a.enqueuedAt !== b.enqueuedAt)
52
+ return a.enqueuedAt < b.enqueuedAt ? -1 : 1;
53
+ return (0, compare_1.compareBytes)(a.id, b.id);
54
+ }
55
+ function matchesQuery(record, query) {
56
+ if (query.app && !(record.appId || record.workflowId || "").toLowerCase().includes(query.app))
57
+ return false;
58
+ if (query.status && record.lifecycle !== query.status && record.derivedLifecycle !== query.status)
59
+ return false;
60
+ if (query.repo && node_path_1.default.resolve(record.repo) !== query.repo)
61
+ return false;
62
+ if (query.since && record.createdAt < query.since)
63
+ return false;
64
+ if (query.until && record.createdAt > query.until)
65
+ return false;
66
+ if (query.text) {
67
+ const haystack = [
68
+ record.runId,
69
+ record.appId,
70
+ record.workflowId,
71
+ record.title,
72
+ record.repo,
73
+ record.lifecycle,
74
+ record.loopStage,
75
+ record.inputsDigest
76
+ ]
77
+ .filter(Boolean)
78
+ .join(" ")
79
+ .toLowerCase();
80
+ if (!haystack.includes(query.text))
81
+ return false;
82
+ }
83
+ return true;
84
+ }
85
+ /** Bounded, deterministic stringification of run inputs for free-text search.
86
+ * Descriptive intent keys (question, prompt, ...) come first so they survive
87
+ * truncation; the rest follow alphabetically. Deterministic and compact. */
88
+ const DIGEST_PRIORITY_KEYS = ["question", "prompt", "task", "summary", "title", "objective", "focus", "topic"];
89
+ /** Distinct execution backends used by a run's dispatches/tasks, recomputed from
90
+ * source state. Sorted; empty for pre-v0.1.29 / default-only runs that never
91
+ * recorded a backend. The registry stays backend-agnostic — this is metadata. */
92
+ function distinctBackends(run) {
93
+ const backends = new Set();
94
+ for (const dispatch of run.dispatches || []) {
95
+ if (dispatch.backendId)
96
+ backends.add(dispatch.backendId);
97
+ }
98
+ for (const task of run.tasks || []) {
99
+ if (task.backendId)
100
+ backends.add(task.backendId);
101
+ }
102
+ return [...backends].sort();
103
+ }
104
+ function digestInputs(inputs) {
105
+ if (!inputs || typeof inputs !== "object")
106
+ return undefined;
107
+ const keys = Object.keys(inputs);
108
+ const ordered = [
109
+ ...DIGEST_PRIORITY_KEYS.filter((k) => keys.includes(k)),
110
+ ...keys.filter((k) => !DIGEST_PRIORITY_KEYS.includes(k)).sort()
111
+ ];
112
+ const parts = [];
113
+ for (const key of ordered) {
114
+ const value = inputs[key];
115
+ if (value === undefined || value === null)
116
+ continue;
117
+ const rendered = Array.isArray(value) ? value.join(",") : typeof value === "object" ? JSON.stringify(value) : String(value);
118
+ parts.push(`${key}=${rendered}`);
119
+ }
120
+ const joined = parts.join(" ").replace(/\s+/g, " ").trim();
121
+ return joined.length > 360 ? `${joined.slice(0, 357)}...` : joined;
122
+ }
123
+ function countRecords(records) {
124
+ const counts = {
125
+ total: records.length,
126
+ queued: 0,
127
+ running: 0,
128
+ blocked: 0,
129
+ completed: 0,
130
+ failed: 0,
131
+ archived: 0,
132
+ reclaimed: 0
133
+ };
134
+ for (const record of records) {
135
+ counts[record.lifecycle] = (counts[record.lifecycle] || 0) + 1;
136
+ }
137
+ return counts;
138
+ }
139
+ function optionalLower(value) {
140
+ if (value === undefined || value === null || value === "")
141
+ return undefined;
142
+ return String(value).toLowerCase();
143
+ }
144
+ function clampInt(value, fallback, min) {
145
+ const n = Number(value);
146
+ if (!Number.isFinite(n))
147
+ return fallback;
148
+ return Math.max(min, Math.floor(n));
149
+ }
150
+ let queueCounter = 0;
151
+ function queueId() {
152
+ queueCounter += 1;
153
+ const stamp = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
154
+ return `q-${stamp}-${String(queueCounter).padStart(3, "0")}`;
155
+ }
156
+ function isRunLifecycleState(value) {
157
+ return typeof value === "string" && exports.LIFECYCLE_STATES.includes(value);
158
+ }
159
+ /** Read a run dir's `reclaimed.json` overlay (v0.1.39). Fail-closed to an empty
160
+ * chain on absence/corruption — a malformed overlay must never brick the run. */
161
+ function loadReclaimedFromDir(runDir) {
162
+ const file = node_path_1.default.join(runDir, "reclaimed.json");
163
+ if (!node_fs_1.default.existsSync(file))
164
+ return { schemaVersion: 1, runId: "", tombstones: [] };
165
+ try {
166
+ const parsed = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
167
+ return { schemaVersion: 1, runId: parsed.runId || "", tombstones: Array.isArray(parsed.tombstones) ? parsed.tombstones : [] };
168
+ }
169
+ catch {
170
+ return { schemaVersion: 1, runId: "", tombstones: [] };
171
+ }
172
+ }
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatRegistryReport = formatRegistryReport;
4
+ exports.formatRunSearch = formatRunSearch;
5
+ exports.formatRunShow = formatRunShow;
6
+ exports.formatGcPlan = formatGcPlan;
7
+ exports.formatGcRun = formatGcRun;
8
+ exports.formatGcVerify = formatGcVerify;
9
+ exports.formatResume = formatResume;
10
+ exports.formatHistory = formatHistory;
11
+ exports.formatQueueList = formatQueueList;
12
+ function countsLine(counts) {
13
+ return `total=${counts.total} queued=${counts.queued} running=${counts.running} blocked=${counts.blocked} completed=${counts.completed} failed=${counts.failed} archived=${counts.archived} reclaimed=${counts.reclaimed}`;
14
+ }
15
+ function recordLine(record) {
16
+ const flags = [record.archived ? "archived" : "", record.provenance?.rerunOf ? `rerunOf=${record.provenance.rerunOf}` : ""].filter(Boolean).join(" ");
17
+ return ` [${record.lifecycle}] ${record.runId} (${record.appId || record.workflowId}) ${record.loopStage}${flags ? ` {${flags}}` : ""}`;
18
+ }
19
+ function formatRegistryReport(report) {
20
+ const lines = [];
21
+ lines.push(`Run Registry (${report.scope}): ${report.root}`);
22
+ lines.push(`Freshness: ${report.freshness.status}${report.freshness.staleRuns.length ? ` (stale: ${report.freshness.staleRuns.join(", ")})` : ""}${report.freshness.missingRuns.length ? ` (missing: ${report.freshness.missingRuns.join(", ")})` : ""}`);
23
+ lines.push(`Repos: ${report.index.repos.length}`);
24
+ lines.push(countsLine(report.counts));
25
+ if (report.freshness.status !== "valid")
26
+ lines.push(`Next Action: ${report.nextAction}`);
27
+ return lines.join("\n");
28
+ }
29
+ function formatRunSearch(result) {
30
+ const lines = [];
31
+ lines.push(`Run Search (${result.scope}): ${result.total} match(es), showing ${result.records.length} [offset ${result.offset}] freshness=${result.freshness}`);
32
+ for (const record of result.records)
33
+ lines.push(recordLine(record));
34
+ if (!result.records.length)
35
+ lines.push(" (no matching runs)");
36
+ return lines.join("\n");
37
+ }
38
+ function formatRunShow(result) {
39
+ if (!result.found) {
40
+ return `Run ${result.runId}: MISSING (source state.json absent — fail closed). Next: ${result.nextAction}`;
41
+ }
42
+ const r = result.record;
43
+ const lines = [
44
+ `Run ${r.runId} [${r.lifecycle}] (derived: ${r.derivedLifecycle})`,
45
+ ` app=${r.appId || r.workflowId} loopStage=${r.loopStage} repo=${r.repo}`,
46
+ ` tasks: total=${r.tasks.total} pending=${r.tasks.pending} running=${r.tasks.running} failed=${r.tasks.failed} completed=${r.tasks.completed}`,
47
+ ` commits=${r.commitCount} (verifier-gated=${r.verifierGatedCommitCount}) openFeedback=${r.openFeedbackCount}`
48
+ ];
49
+ if (r.provenance?.rerunOf)
50
+ lines.push(` provenance: rerunOf=${r.provenance.rerunOf} gen=${r.provenance.generation} origin=${r.provenance.originRunId}`);
51
+ if (r.tier && r.tier !== "live") {
52
+ lines.push(` tier=${r.tier} capability=${r.capability} reason=${r.capabilityReason}${r.reclaimedBytes ? ` bytesFreed=${r.reclaimedBytes}` : ""}${r.tombstoneHash ? ` tombstone=${r.tombstoneHash.slice(0, 19)}` : ""}`);
53
+ }
54
+ return lines.join("\n");
55
+ }
56
+ function formatGcPlan(result) {
57
+ const lines = [
58
+ `GC Plan (${result.scope}): ${result.eligibleCount}/${result.total} eligible, ${result.bytesToFree} byte(s) would be freed [DRY-RUN, frees nothing]`,
59
+ ` policy: reclaimAfterArchiveDays=${result.policy.reclaimAfterArchiveDays} keepScratch=${result.policy.keepScratch} keepSnapshots=${result.policy.keepSnapshots}`
60
+ ];
61
+ for (const entry of result.entries) {
62
+ if (entry.eligible) {
63
+ const kinds = Object.entries(entry.byKind).map(([k, v]) => `${k}=${v}`).join(" ");
64
+ lines.push(` [eligible] ${entry.runId} -> ${entry.capability} (${entry.capabilityReason}) ${entry.bytesToFree}B {${kinds}}`);
65
+ }
66
+ else {
67
+ lines.push(` [skip:${entry.reason}] ${entry.runId} (tier=${entry.tier})`);
68
+ }
69
+ }
70
+ if (!result.entries.length)
71
+ lines.push(" (no runs in scope)");
72
+ return lines.join("\n");
73
+ }
74
+ function formatGcRun(result) {
75
+ const lines = [`GC Run (${result.scope}): reclaimed ${result.reclaimed.length} run(s), freed ${result.totalBytesFreed} byte(s)`];
76
+ for (const r of result.reclaimed)
77
+ lines.push(` [reclaimed] ${r.runId} -> ${r.capability} (${r.capabilityReason}) ${r.bytesFreed}B tombstone=${r.tombstoneHash.slice(0, 19)}`);
78
+ for (const r of result.refused)
79
+ lines.push(` [refused:${r.code}] ${r.runId}`);
80
+ if (!result.reclaimed.length && !result.refused.length)
81
+ lines.push(" (nothing eligible)");
82
+ return lines.join("\n");
83
+ }
84
+ function formatGcVerify(result) {
85
+ const lines = [
86
+ `GC Verify ${result.runId}: reclaimed=${result.reclaimed} verified=${result.verified} tier=${result.tier} capability=${result.capability}${result.tombstoneHash ? ` tombstone=${result.tombstoneHash.slice(0, 19)}` : ""}`
87
+ ];
88
+ for (const check of result.checks)
89
+ lines.push(` ${check.pass ? "PASS" : "FAIL"} ${check.name}${check.code ? ` [${check.code}]` : ""}${check.detail ? ` (${check.detail})` : ""}`);
90
+ return lines.join("\n");
91
+ }
92
+ function formatResume(result) {
93
+ const lines = [
94
+ `Resume ${result.runId} [${result.lifecycle}] loopStage=${result.loopStage} (resolved from ${result.resolvedFrom}, ${result.freshness})`,
95
+ ` resumable=${result.resumable} nextTasks=${result.nextTasks.length}`
96
+ ];
97
+ for (const action of result.nextActions)
98
+ lines.push(` -> ${action.command}\n ${action.reason}`);
99
+ // Only when --drive/--once continued the run; the default read-only resume text is unchanged.
100
+ if (result.drive) {
101
+ const d = result.drive;
102
+ lines.push(` drive: ${d.status} (${d.completedWorkers}/${d.plannedWorkers} workers${d.commitId ? `, committed ${d.commitId}` : ""})`);
103
+ }
104
+ return lines.join("\n");
105
+ }
106
+ function formatHistory(result) {
107
+ const lines = [];
108
+ lines.push(`Run History (${result.scope}): ${result.total} run(s) across ${result.repos.length} repo(s), freshness=${result.freshness}`);
109
+ for (const entry of result.entries) {
110
+ lines.push(` ${entry.createdAt} [${entry.lifecycle}] ${entry.runId} (${entry.appId || entry.workflowId})${entry.provenance?.rerunOf ? ` rerunOf=${entry.provenance.rerunOf}` : ""}`);
111
+ }
112
+ if (!result.entries.length)
113
+ lines.push(" (no runs)");
114
+ return lines.join("\n");
115
+ }
116
+ function formatQueueList(result) {
117
+ const lines = [`Run Queue: ${result.total} entry(ies) [priority asc]`];
118
+ for (const entry of result.entries) {
119
+ lines.push(` #${entry.priority} ${entry.id} [${entry.status}] ${entry.appId || entry.workflowId || entry.runId || "?"} repo=${entry.repo}${entry.note ? ` note=${entry.note}` : ""}`);
120
+ }
121
+ if (!result.entries.length)
122
+ lines.push(" (queue empty)");
123
+ return lines.join("\n");
124
+ }