cool-workflow 0.1.79 → 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 (131) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +51 -3
  4. package/apps/architecture-review/app.json +1 -1
  5. package/apps/architecture-review-fast/app.json +64 -0
  6. package/apps/architecture-review-fast/workflow.js +153 -0
  7. package/apps/end-to-end-golden-path/app.json +1 -1
  8. package/apps/pr-review-fix-ci/app.json +1 -1
  9. package/apps/release-cut/app.json +1 -1
  10. package/apps/research-synthesis/app.json +1 -1
  11. package/dist/agent-config.js +21 -7
  12. package/dist/candidate-scoring.js +42 -22
  13. package/dist/capability-core.js +132 -17
  14. package/dist/capability-registry.js +138 -168
  15. package/dist/cli.js +97 -98
  16. package/dist/collaboration.js +5 -6
  17. package/dist/commit.js +20 -6
  18. package/dist/compare.js +18 -0
  19. package/dist/coordinator/classify.js +45 -0
  20. package/dist/coordinator/paths.js +42 -0
  21. package/dist/coordinator/util.js +129 -0
  22. package/dist/coordinator.js +127 -300
  23. package/dist/dispatch.js +35 -0
  24. package/dist/drive.js +79 -6
  25. package/dist/error-feedback.js +8 -4
  26. package/dist/evidence-reasoning.js +3 -3
  27. package/dist/execution-backend/agent.js +331 -0
  28. package/dist/execution-backend/probes.js +96 -0
  29. package/dist/execution-backend/util.js +47 -0
  30. package/dist/execution-backend.js +73 -421
  31. package/dist/mcp-server.js +79 -183
  32. package/dist/multi-agent/graph.js +84 -0
  33. package/dist/multi-agent/helpers.js +145 -0
  34. package/dist/multi-agent/paths.js +22 -0
  35. package/dist/multi-agent-eval/format.js +194 -0
  36. package/dist/multi-agent-eval/normalize.js +51 -0
  37. package/dist/multi-agent-eval.js +39 -244
  38. package/dist/multi-agent-host.js +0 -19
  39. package/dist/multi-agent.js +125 -314
  40. package/dist/node-snapshot.js +3 -3
  41. package/dist/observability/format.js +61 -0
  42. package/dist/observability/intake.js +98 -0
  43. package/dist/observability.js +14 -160
  44. package/dist/operator-ux/format.js +364 -0
  45. package/dist/operator-ux.js +22 -363
  46. package/dist/orchestrator/lifecycle-operations.js +2 -1
  47. package/dist/orchestrator/report.js +8 -0
  48. package/dist/orchestrator.js +26 -9
  49. package/dist/reclamation.js +26 -21
  50. package/dist/run-export.js +494 -25
  51. package/dist/run-registry/derive.js +172 -0
  52. package/dist/run-registry/format.js +124 -0
  53. package/dist/run-registry/gc.js +251 -0
  54. package/dist/run-registry/policy.js +16 -0
  55. package/dist/run-registry/queue.js +116 -0
  56. package/dist/run-registry.js +89 -597
  57. package/dist/run-state-schema.js +1 -0
  58. package/dist/sandbox-profile.js +43 -2
  59. package/dist/state-explosion/format.js +159 -0
  60. package/dist/state-explosion/helpers.js +82 -0
  61. package/dist/state-explosion.js +165 -304
  62. package/dist/state-node.js +19 -4
  63. package/dist/telemetry-attestation.js +55 -0
  64. package/dist/telemetry-demo.js +15 -3
  65. package/dist/telemetry-ledger.js +60 -15
  66. package/dist/topology.js +25 -8
  67. package/dist/triggers.js +33 -14
  68. package/dist/trust-audit.js +145 -33
  69. package/dist/version.js +1 -1
  70. package/dist/worker-isolation/helpers.js +51 -0
  71. package/dist/worker-isolation/paths.js +46 -0
  72. package/dist/worker-isolation.js +39 -115
  73. package/docs/agent-delegation-drive.7.md +71 -0
  74. package/docs/canonical-workflow-apps.7.md +37 -0
  75. package/docs/cli-mcp-parity.7.md +16 -0
  76. package/docs/contract-migration-tooling.7.md +6 -0
  77. package/docs/control-plane-scheduling.7.md +6 -0
  78. package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +40 -0
  79. package/docs/durable-state-and-locking.7.md +8 -0
  80. package/docs/evidence-adoption-reasoning-chain.7.md +6 -0
  81. package/docs/execution-backends.7.md +6 -0
  82. package/docs/index.md +2 -0
  83. package/docs/launch/demo.tape +28 -0
  84. package/docs/launch/launch-kit.md +96 -17
  85. package/docs/launch/pre-launch-checklist.md +53 -0
  86. package/docs/multi-agent-cli-mcp-surface.7.md +8 -0
  87. package/docs/multi-agent-eval-replay-harness.7.md +6 -0
  88. package/docs/multi-agent-operator-ux.7.md +6 -0
  89. package/docs/multi-agent-trust-policy-audit.7.md +27 -0
  90. package/docs/node-snapshot-diff-replay.7.md +6 -0
  91. package/docs/observability-cost-accounting.7.md +6 -0
  92. package/docs/project-index.md +27 -6
  93. package/docs/real-execution-backends.7.md +6 -0
  94. package/docs/release-and-migration.7.md +8 -0
  95. package/docs/release-tooling.7.md +6 -0
  96. package/docs/routines.md +23 -0
  97. package/docs/run-registry-control-plane.7.md +89 -2
  98. package/docs/run-retention-reclamation.7.md +8 -0
  99. package/docs/source-context-profiles.7.md +119 -0
  100. package/docs/state-explosion-management.7.md +13 -0
  101. package/docs/team-collaboration.7.md +6 -0
  102. package/docs/trust-model.md +267 -0
  103. package/docs/unix-principles.md +49 -1
  104. package/docs/vendor-manifest-loadability.7.md +43 -0
  105. package/docs/web-desktop-workbench.7.md +6 -0
  106. package/manifest/plugin.manifest.json +1 -1
  107. package/manifest/source-context-profiles.json +142 -0
  108. package/package.json +4 -1
  109. package/scripts/agents/builtin-templates.json +7 -0
  110. package/scripts/agents/claude-p-agent.js +129 -43
  111. package/scripts/architecture-review-fast.js +362 -0
  112. package/scripts/bump-version.js +5 -10
  113. package/scripts/canonical-apps-list.js +64 -0
  114. package/scripts/canonical-apps.js +36 -4
  115. package/scripts/coverage-gate.js +211 -0
  116. package/scripts/dogfood-release.js +1 -1
  117. package/scripts/golden-path.js +4 -4
  118. package/scripts/parity-check.js +5 -0
  119. package/scripts/release-check.js +5 -1
  120. package/scripts/source-context.js +291 -0
  121. package/scripts/version-sync-check.js +5 -7
  122. package/skills/ci-triage/SKILL.md +50 -0
  123. package/skills/ci-triage/agents/openai.yaml +4 -0
  124. package/skills/cool-workflow/SKILL.md +4 -1
  125. package/skills/deploy-check/SKILL.md +55 -0
  126. package/skills/deploy-check/agents/openai.yaml +4 -0
  127. package/skills/design-qa/SKILL.md +49 -0
  128. package/skills/design-qa/agents/openai.yaml +4 -0
  129. package/skills/pr-review/SKILL.md +45 -0
  130. package/skills/pr-review/agents/openai.yaml +4 -0
  131. package/dist/capability-dispatcher.js +0 -86
@@ -1,64 +1,533 @@
1
1
  "use strict";
2
- // Run Export / Import — portable run archive format (v0.1.74).
2
+ // Run Export / Import — portable run archive format (Track B).
3
3
  //
4
- // BSD discipline: explicit state, portable format. Export serializes a run
5
- // to a single JSON file; import restores it in a new location. Both functions
6
- // are pure they read the run, write the export/import, and return the result.
7
- //
8
- // Track B: users can export a run on one machine and restore it on another.
4
+ // BSD discipline: explicit state, portable format. Export serializes a run plus
5
+ // its run-local files (artifacts, audit overlays, telemetry ledger, reports,
6
+ // worker files, commit snapshots) to a single JSON archive. Import restores those
7
+ // bytes into a new .cw/runs/<id>/ tree, rebases paths, writes a restore manifest,
8
+ // and exposes a deterministic verification pass. No hidden database, no trust in
9
+ // paths from the archive without containment checks.
9
10
  var __importDefault = (this && this.__importDefault) || function (mod) {
10
11
  return (mod && mod.__esModule) ? mod : { "default": mod };
11
12
  };
12
13
  Object.defineProperty(exports, "__esModule", { value: true });
13
14
  exports.exportRun = exportRun;
14
15
  exports.importRun = importRun;
16
+ exports.verifyImportedRun = verifyImportedRun;
17
+ exports.inspectArchive = inspectArchive;
18
+ exports.importManifestPath = importManifestPath;
15
19
  const node_fs_1 = __importDefault(require("node:fs"));
16
20
  const node_path_1 = __importDefault(require("node:path"));
21
+ const node_crypto_1 = __importDefault(require("node:crypto"));
17
22
  const state_1 = require("./state");
18
23
  const version_1 = require("./version");
19
- /** Export a run to a portable JSON file. The export includes the full run
20
- * state but NOT raw artifact files — only their paths and digests. */
24
+ const telemetry_ledger_1 = require("./telemetry-ledger");
25
+ const trust_audit_1 = require("./trust-audit");
26
+ const compare_1 = require("./compare");
27
+ /** Export a run to a portable JSON archive with run-local bytes and digests. */
21
28
  function exportRun(run, outputPath) {
22
29
  const exportedAt = new Date().toISOString();
30
+ const files = collectArchiveFiles(run);
31
+ const manifestSha256 = digestManifest(files);
23
32
  const exported = {
24
33
  schemaVersion: 1,
25
34
  exportedAt,
26
35
  sourceVersion: version_1.CURRENT_COOL_WORKFLOW_VERSION,
27
36
  run,
28
- artifacts: [],
29
- audit: []
37
+ files,
38
+ integrity: {
39
+ fileCount: files.length,
40
+ manifestSha256
41
+ },
42
+ // Legacy field retained so old readers still find an artifact-ish list.
43
+ artifacts: files
44
+ .filter((file) => file.role === "artifact")
45
+ .map((file) => ({
46
+ path: file.relativePath,
47
+ contentBase64: file.contentBase64,
48
+ sha256: file.sha256,
49
+ sizeBytes: file.sizeBytes
50
+ })),
51
+ audit: files.filter((file) => file.role === "audit").map((file) => file.relativePath)
30
52
  };
31
53
  (0, state_1.writeJson)(outputPath, exported);
54
+ const archiveSha256 = sha256Bytes(node_fs_1.default.readFileSync(outputPath));
32
55
  return {
33
56
  runId: run.id,
34
57
  exportedAt,
35
58
  path: outputPath,
36
59
  taskCount: run.tasks.length,
37
- commitCount: run.commits.length
60
+ commitCount: run.commits.length,
61
+ fileCount: files.length,
62
+ artifactCount: files.filter((file) => file.role === "artifact").length,
63
+ auditFileCount: files.filter((file) => file.role === "audit").length,
64
+ telemetryIncluded: files.some((file) => file.role === "telemetry"),
65
+ manifestSha256,
66
+ archiveSha256
38
67
  };
39
68
  }
40
69
  /** Import a run from a portable JSON file into a target directory.
41
70
  * Rebuilds run paths relative to the target dir. */
42
71
  function importRun(exportPath, targetDir) {
43
- const raw = JSON.parse(node_fs_1.default.readFileSync(exportPath, "utf8"));
72
+ const raw = (0, state_1.readJson)(exportPath);
44
73
  if (raw.schemaVersion !== 1)
45
74
  throw new Error(`Unsupported export schema version: ${raw.schemaVersion}`);
46
- const run = raw.run;
47
- const runDir = node_path_1.default.join(targetDir, ".cw", "runs", run.id);
75
+ const archiveSha256 = sha256Bytes(node_fs_1.default.readFileSync(exportPath));
76
+ const files = normalizeArchiveFiles(raw);
77
+ verifyArchiveFileDigests(files, raw.integrity);
78
+ const oldRunDir = raw.run.paths.runDir;
79
+ const oldCwd = raw.run.cwd;
80
+ const runDir = node_path_1.default.join(targetDir, ".cw", "runs", raw.run.id);
48
81
  const paths = (0, state_1.createRunPaths)(runDir);
49
82
  (0, state_1.ensureRunDirs)(paths);
50
- // Rebase all paths to the new target directory
51
- run.paths = paths;
52
- run.cwd = targetDir;
53
- run.updatedAt = new Date().toISOString();
54
- // Rebase node artifact paths too
55
- for (const node of run.nodes || []) {
56
- for (const artifact of node.artifacts || []) {
57
- if (artifact.path && artifact.path.includes(".cw/runs/")) {
58
- // Keep the original path as-is — the artifact may not exist in new location
59
- }
83
+ for (const file of files) {
84
+ const destination = node_path_1.default.join(runDir, file.relativePath);
85
+ if (!(0, state_1.isContainedPath)(destination, runDir)) {
86
+ throw new Error(`Archive file escapes restore directory: ${file.relativePath}`);
60
87
  }
88
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(destination), { recursive: true });
89
+ node_fs_1.default.writeFileSync(destination, Buffer.from(file.contentBase64, "base64"));
90
+ }
91
+ const externalPathMap = new Map();
92
+ for (const file of files) {
93
+ if (file.sourcePath)
94
+ externalPathMap.set(file.sourcePath, node_path_1.default.join(runDir, file.relativePath));
61
95
  }
96
+ const run = rebaseRun(raw.run, {
97
+ oldRunDir,
98
+ newRunDir: runDir,
99
+ oldCwd,
100
+ newCwd: targetDir,
101
+ paths,
102
+ externalPathMap
103
+ });
62
104
  (0, state_1.saveCheckpoint)(run);
63
- return { run, runDir, statePath: paths.state };
105
+ const manifest = {
106
+ schemaVersion: 1,
107
+ runId: run.id,
108
+ importedAt: new Date().toISOString(),
109
+ sourceVersion: raw.sourceVersion,
110
+ archiveSha256,
111
+ manifestSha256: digestManifest(files),
112
+ files: files.map(({ contentBase64: _contentBase64, ...file }) => file)
113
+ };
114
+ const manifestPath = importManifestPath(run);
115
+ (0, state_1.writeJson)(manifestPath, manifest, { durable: true });
116
+ const verification = verifyImportedRun(run);
117
+ return {
118
+ run,
119
+ runDir,
120
+ statePath: paths.state,
121
+ manifestPath,
122
+ verifyCommand: `cw run verify-import ${run.id} --cwd ${targetDir} --json`,
123
+ verification
124
+ };
125
+ }
126
+ /** Verify an imported run against its restore manifest and telemetry chain. */
127
+ function verifyImportedRun(run) {
128
+ const manifestPath = importManifestPath(run);
129
+ const checks = [];
130
+ if (!node_fs_1.default.existsSync(manifestPath)) {
131
+ return {
132
+ runId: run.id,
133
+ ok: false,
134
+ manifestPath,
135
+ checkedFiles: 0,
136
+ checks: [{ name: "import-manifest", pass: false, code: "missing-import-manifest", path: manifestPath }]
137
+ };
138
+ }
139
+ let manifest;
140
+ try {
141
+ manifest = (0, state_1.readJson)(manifestPath);
142
+ }
143
+ catch (error) {
144
+ return {
145
+ runId: run.id,
146
+ ok: false,
147
+ manifestPath,
148
+ checkedFiles: 0,
149
+ checks: [{ name: "import-manifest", pass: false, code: "invalid-import-manifest", path: manifestPath, actual: messageOf(error) }]
150
+ };
151
+ }
152
+ const currentManifestDigest = digestManifest(manifest.files.map((file) => ({ ...file, contentBase64: "" })));
153
+ checks.push({
154
+ name: "import-manifest",
155
+ pass: manifest.runId === run.id && manifest.manifestSha256 === currentManifestDigest,
156
+ code: manifest.runId !== run.id ? "run-id-mismatch" : manifest.manifestSha256 === currentManifestDigest ? undefined : "manifest-digest-mismatch",
157
+ expected: manifest.manifestSha256,
158
+ actual: currentManifestDigest
159
+ });
160
+ let filesOk = true;
161
+ for (const file of manifest.files) {
162
+ const restoredPath = node_path_1.default.join(run.paths.runDir, file.relativePath);
163
+ if (!(0, state_1.isContainedPath)(restoredPath, run.paths.runDir)) {
164
+ filesOk = false;
165
+ checks.push({ name: "archive-file", pass: false, code: "path-escape", path: file.relativePath });
166
+ continue;
167
+ }
168
+ if (!node_fs_1.default.existsSync(restoredPath)) {
169
+ filesOk = false;
170
+ checks.push({ name: "archive-file", pass: false, code: "missing-file", path: file.relativePath, expected: file.sha256 });
171
+ continue;
172
+ }
173
+ const actual = sha256Bytes(node_fs_1.default.readFileSync(restoredPath));
174
+ const pass = actual === file.sha256;
175
+ if (!pass)
176
+ filesOk = false;
177
+ checks.push({
178
+ name: "archive-file",
179
+ pass,
180
+ code: pass ? undefined : "digest-mismatch",
181
+ path: file.relativePath,
182
+ expected: file.sha256,
183
+ actual
184
+ });
185
+ }
186
+ checks.push({ name: "archive-files", pass: filesOk, code: filesOk ? undefined : "archive-files-invalid" });
187
+ const telemetry = (0, telemetry_ledger_1.verifyTelemetryLedger)(run);
188
+ checks.push({
189
+ name: "telemetry-ledger",
190
+ pass: telemetry.verified,
191
+ code: telemetry.verified ? undefined : "telemetry-ledger-invalid"
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
+ });
204
+ return {
205
+ runId: run.id,
206
+ ok: checks.every((check) => check.pass),
207
+ manifestPath,
208
+ checkedFiles: manifest.files.length,
209
+ checks
210
+ };
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
+ }
276
+ function importManifestPath(run) {
277
+ return node_path_1.default.join(run.paths.runDir, "import-manifest.json");
278
+ }
279
+ function collectArchiveFiles(run) {
280
+ const entries = new Map();
281
+ for (const file of walkFiles(run.paths.runDir)) {
282
+ const relativePath = toArchivePath(node_path_1.default.relative(run.paths.runDir, file));
283
+ if (!relativePath || relativePath === "state.json" || relativePath === "import-manifest.json" || relativePath.endsWith(".lock"))
284
+ continue;
285
+ addFile(entries, run, file, roleForRelativePath(relativePath));
286
+ }
287
+ for (const artifactPath of collectReferencedArtifactPaths(run)) {
288
+ if (!artifactPath || !node_fs_1.default.existsSync(artifactPath) || !node_fs_1.default.statSync(artifactPath).isFile())
289
+ continue;
290
+ if ((0, state_1.isContainedPath)(artifactPath, run.paths.runDir)) {
291
+ addFile(entries, run, artifactPath, "artifact");
292
+ continue;
293
+ }
294
+ if ((0, state_1.isContainedPath)(artifactPath, run.cwd))
295
+ addExternalArtifactFile(entries, run, artifactPath);
296
+ }
297
+ return [...entries.values()].sort((left, right) => (0, compare_1.compareBytes)(left.relativePath, right.relativePath));
298
+ }
299
+ function addFile(entries, run, file, role) {
300
+ const relativePath = toArchivePath(node_path_1.default.relative(run.paths.runDir, file));
301
+ if (relativePath === "state.json" || relativePath === "import-manifest.json")
302
+ return;
303
+ if (!relativePath || relativePath.startsWith("../"))
304
+ return;
305
+ const bytes = node_fs_1.default.readFileSync(file);
306
+ entries.set(relativePath, {
307
+ relativePath,
308
+ role,
309
+ contentBase64: bytes.toString("base64"),
310
+ sha256: sha256Bytes(bytes),
311
+ sizeBytes: bytes.length
312
+ });
313
+ }
314
+ function addExternalArtifactFile(entries, run, file) {
315
+ const sourcePath = node_path_1.default.resolve(file);
316
+ const bytes = node_fs_1.default.readFileSync(sourcePath);
317
+ const relativePath = `external-artifacts/${sha256Bytes(Buffer.from(sourcePath, "utf8")).slice(0, 16)}-${safeArchiveBasename(node_path_1.default.basename(sourcePath))}`;
318
+ entries.set(relativePath, {
319
+ relativePath,
320
+ role: "artifact",
321
+ contentBase64: bytes.toString("base64"),
322
+ sha256: sha256Bytes(bytes),
323
+ sizeBytes: bytes.length,
324
+ sourcePath
325
+ });
326
+ }
327
+ function collectReferencedArtifactPaths(run) {
328
+ const paths = new Set();
329
+ for (const node of run.nodes || []) {
330
+ for (const artifact of node.artifacts || [])
331
+ addArtifactPath(paths, run, artifact.path);
332
+ }
333
+ for (const candidate of run.candidates || []) {
334
+ for (const artifact of candidate.artifacts || [])
335
+ addArtifactPath(paths, run, artifact.path);
336
+ }
337
+ for (const selection of run.candidateSelections || []) {
338
+ for (const artifact of selection.artifacts || [])
339
+ addArtifactPath(paths, run, artifact.path);
340
+ }
341
+ for (const artifact of run.blackboard?.artifacts || [])
342
+ addArtifactPath(paths, run, artifact.path);
343
+ return [...paths].sort();
344
+ }
345
+ function addArtifactPath(paths, run, value) {
346
+ if (!value)
347
+ return;
348
+ paths.add(node_path_1.default.isAbsolute(value) ? value : node_path_1.default.resolve(run.cwd, value));
349
+ }
350
+ function walkFiles(root) {
351
+ if (!node_fs_1.default.existsSync(root))
352
+ return [];
353
+ const found = [];
354
+ for (const name of node_fs_1.default.readdirSync(root)) {
355
+ const file = node_path_1.default.join(root, name);
356
+ const stat = node_fs_1.default.lstatSync(file);
357
+ if (stat.isSymbolicLink())
358
+ continue;
359
+ if (stat.isDirectory())
360
+ found.push(...walkFiles(file));
361
+ else if (stat.isFile())
362
+ found.push(file);
363
+ }
364
+ return found;
365
+ }
366
+ function roleForRelativePath(relativePath) {
367
+ if (relativePath === "telemetry.json")
368
+ return "telemetry";
369
+ if (relativePath === "audit" || relativePath.startsWith("audit/"))
370
+ return "audit";
371
+ if (relativePath === "artifacts" || relativePath.startsWith("artifacts/"))
372
+ return "artifact";
373
+ return "run-file";
374
+ }
375
+ function normalizeArchiveFiles(raw) {
376
+ const modern = raw.files || [];
377
+ if (modern.length) {
378
+ return modern.map((file) => ({
379
+ relativePath: cleanArchiveRelativePath(file.relativePath),
380
+ role: file.role,
381
+ contentBase64: file.contentBase64,
382
+ sha256: file.sha256,
383
+ sizeBytes: file.sizeBytes,
384
+ sourcePath: file.sourcePath
385
+ }));
386
+ }
387
+ return (raw.artifacts || []).map((artifact) => {
388
+ const contentBase64 = artifact.contentBase64 || Buffer.from(artifact.content || "", "utf8").toString("base64");
389
+ const bytes = Buffer.from(contentBase64, "base64");
390
+ return {
391
+ relativePath: cleanArchiveRelativePath(artifact.path),
392
+ role: "artifact",
393
+ contentBase64,
394
+ sha256: artifact.sha256 || sha256Bytes(bytes),
395
+ sizeBytes: artifact.sizeBytes ?? bytes.length
396
+ };
397
+ });
398
+ }
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 = [];
405
+ for (const file of files) {
406
+ const bytes = Buffer.from(file.contentBase64, "base64");
407
+ const actual = sha256Bytes(bytes);
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) });
416
+ }
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) });
422
+ const actualManifest = digestManifest(files);
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)");
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));
456
+ }
457
+ function digestManifest(files) {
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.
463
+ .map((file) => ({
464
+ relativePath: file.relativePath,
465
+ role: file.role,
466
+ sha256: file.sha256,
467
+ sizeBytes: file.sizeBytes
468
+ }))
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));
473
+ return sha256Bytes(Buffer.from(JSON.stringify(manifest), "utf8"));
474
+ }
475
+ function rebaseRun(source, context) {
476
+ const cloned = deepRebase(JSON.parse(JSON.stringify(source)), context);
477
+ cloned.cwd = context.newCwd;
478
+ cloned.paths = context.paths;
479
+ cloned.updatedAt = new Date().toISOString();
480
+ cloned.audit = cloned.audit
481
+ ? {
482
+ schemaVersion: 1,
483
+ eventLogPath: node_path_1.default.join(context.paths.auditDir || node_path_1.default.join(context.paths.runDir, "audit"), "events.jsonl"),
484
+ summaryPath: node_path_1.default.join(context.paths.auditDir || node_path_1.default.join(context.paths.runDir, "audit"), "summary.json"),
485
+ indexPath: node_path_1.default.join(context.paths.auditDir || node_path_1.default.join(context.paths.runDir, "audit"), "index.json")
486
+ }
487
+ : cloned.audit;
488
+ return cloned;
489
+ }
490
+ function deepRebase(value, context) {
491
+ if (typeof value === "string")
492
+ return rebaseString(value, context);
493
+ if (Array.isArray(value))
494
+ return value.map((entry) => deepRebase(entry, context));
495
+ if (value && typeof value === "object") {
496
+ const out = {};
497
+ for (const [key, entry] of Object.entries(value))
498
+ out[key] = deepRebase(entry, context);
499
+ return out;
500
+ }
501
+ return value;
502
+ }
503
+ function rebaseString(value, context) {
504
+ const archivedExternal = context.externalPathMap?.get(value);
505
+ if (archivedExternal)
506
+ return archivedExternal;
507
+ if (value === context.oldRunDir || value.startsWith(context.oldRunDir + node_path_1.default.sep)) {
508
+ return context.newRunDir + value.slice(context.oldRunDir.length);
509
+ }
510
+ if (value === context.oldCwd || value.startsWith(context.oldCwd + node_path_1.default.sep)) {
511
+ return context.newCwd + value.slice(context.oldCwd.length);
512
+ }
513
+ return value;
514
+ }
515
+ function cleanArchiveRelativePath(value) {
516
+ const cleaned = toArchivePath(value).replace(/^\/+/, "");
517
+ if (!cleaned || cleaned === "." || cleaned.startsWith("../") || cleaned.includes("/../")) {
518
+ throw new Error(`Invalid archive relative path: ${value}`);
519
+ }
520
+ return cleaned;
521
+ }
522
+ function toArchivePath(value) {
523
+ return value.split(node_path_1.default.sep).join("/");
524
+ }
525
+ function safeArchiveBasename(value) {
526
+ return value.replace(/[^A-Za-z0-9._-]/g, "_") || "artifact";
527
+ }
528
+ function messageOf(error) {
529
+ return error instanceof Error ? error.message : String(error);
530
+ }
531
+ function sha256Bytes(bytes) {
532
+ return node_crypto_1.default.createHash("sha256").update(bytes).digest("hex");
64
533
  }