cool-workflow 0.1.78 → 0.1.80

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 (74) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +29 -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/capability-core.js +71 -0
  12. package/dist/capability-registry.js +13 -8
  13. package/dist/cli.js +49 -1
  14. package/dist/drive.js +74 -1
  15. package/dist/evidence-reasoning.js +2 -2
  16. package/dist/execution-backend.js +6 -1
  17. package/dist/mcp-server.js +56 -13
  18. package/dist/orchestrator/lifecycle-operations.js +2 -1
  19. package/dist/orchestrator.js +1 -1
  20. package/dist/run-export.js +370 -25
  21. package/dist/run-registry.js +11 -4
  22. package/dist/state-explosion.js +100 -21
  23. package/dist/telemetry-demo.js +154 -0
  24. package/dist/version.js +1 -1
  25. package/docs/agent-delegation-drive.7.md +60 -0
  26. package/docs/canonical-workflow-apps.7.md +37 -0
  27. package/docs/cli-mcp-parity.7.md +14 -0
  28. package/docs/contract-migration-tooling.7.md +6 -0
  29. package/docs/control-plane-scheduling.7.md +6 -0
  30. package/docs/durable-state-and-locking.7.md +6 -0
  31. package/docs/evidence-adoption-reasoning-chain.7.md +6 -0
  32. package/docs/execution-backends.7.md +6 -0
  33. package/docs/index.md +1 -0
  34. package/docs/launch/demo.tape +28 -0
  35. package/docs/launch/launch-kit.md +172 -0
  36. package/docs/launch/pre-launch-checklist.md +53 -0
  37. package/docs/multi-agent-cli-mcp-surface.7.md +6 -0
  38. package/docs/multi-agent-eval-replay-harness.7.md +6 -0
  39. package/docs/multi-agent-operator-ux.7.md +6 -0
  40. package/docs/node-snapshot-diff-replay.7.md +6 -0
  41. package/docs/observability-cost-accounting.7.md +6 -0
  42. package/docs/project-index.md +16 -6
  43. package/docs/real-execution-backends.7.md +6 -0
  44. package/docs/release-and-migration.7.md +6 -0
  45. package/docs/release-tooling.7.md +6 -0
  46. package/docs/routines.md +23 -0
  47. package/docs/run-registry-control-plane.7.md +44 -1
  48. package/docs/run-retention-reclamation.7.md +6 -0
  49. package/docs/source-context-profiles.7.md +119 -0
  50. package/docs/state-explosion-management.7.md +13 -0
  51. package/docs/team-collaboration.7.md +6 -0
  52. package/docs/unix-principles.md +49 -1
  53. package/docs/web-desktop-workbench.7.md +6 -0
  54. package/manifest/plugin.manifest.json +1 -1
  55. package/manifest/source-context-profiles.json +142 -0
  56. package/package.json +2 -1
  57. package/scripts/agents/claude-p-agent.js +129 -43
  58. package/scripts/architecture-review-fast.js +362 -0
  59. package/scripts/bump-version.js +1 -0
  60. package/scripts/canonical-apps.js +21 -4
  61. package/scripts/coverage-gate.js +211 -0
  62. package/scripts/dogfood-release.js +1 -1
  63. package/scripts/golden-path.js +4 -4
  64. package/scripts/source-context.js +291 -0
  65. package/scripts/version-sync-check.js +1 -0
  66. package/skills/ci-triage/SKILL.md +50 -0
  67. package/skills/ci-triage/agents/openai.yaml +4 -0
  68. package/skills/cool-workflow/SKILL.md +4 -1
  69. package/skills/deploy-check/SKILL.md +55 -0
  70. package/skills/deploy-check/agents/openai.yaml +4 -0
  71. package/skills/design-qa/SKILL.md +49 -0
  72. package/skills/design-qa/agents/openai.yaml +4 -0
  73. package/skills/pr-review/SKILL.md +45 -0
  74. package/skills/pr-review/agents/openai.yaml +4 -0
@@ -1,64 +1,409 @@
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.importManifestPath = importManifestPath;
15
18
  const node_fs_1 = __importDefault(require("node:fs"));
16
19
  const node_path_1 = __importDefault(require("node:path"));
20
+ const node_crypto_1 = __importDefault(require("node:crypto"));
17
21
  const state_1 = require("./state");
18
22
  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. */
23
+ const telemetry_ledger_1 = require("./telemetry-ledger");
24
+ /** Export a run to a portable JSON archive with run-local bytes and digests. */
21
25
  function exportRun(run, outputPath) {
22
26
  const exportedAt = new Date().toISOString();
27
+ const files = collectArchiveFiles(run);
28
+ const manifestSha256 = digestManifest(files);
23
29
  const exported = {
24
30
  schemaVersion: 1,
25
31
  exportedAt,
26
32
  sourceVersion: version_1.CURRENT_COOL_WORKFLOW_VERSION,
27
33
  run,
28
- artifacts: [],
29
- audit: []
34
+ files,
35
+ integrity: {
36
+ fileCount: files.length,
37
+ manifestSha256
38
+ },
39
+ // Legacy field retained so old readers still find an artifact-ish list.
40
+ artifacts: files
41
+ .filter((file) => file.role === "artifact")
42
+ .map((file) => ({
43
+ path: file.relativePath,
44
+ contentBase64: file.contentBase64,
45
+ sha256: file.sha256,
46
+ sizeBytes: file.sizeBytes
47
+ })),
48
+ audit: files.filter((file) => file.role === "audit").map((file) => file.relativePath)
30
49
  };
31
50
  (0, state_1.writeJson)(outputPath, exported);
51
+ const archiveSha256 = sha256Bytes(node_fs_1.default.readFileSync(outputPath));
32
52
  return {
33
53
  runId: run.id,
34
54
  exportedAt,
35
55
  path: outputPath,
36
56
  taskCount: run.tasks.length,
37
- commitCount: run.commits.length
57
+ commitCount: run.commits.length,
58
+ fileCount: files.length,
59
+ artifactCount: files.filter((file) => file.role === "artifact").length,
60
+ auditFileCount: files.filter((file) => file.role === "audit").length,
61
+ telemetryIncluded: files.some((file) => file.role === "telemetry"),
62
+ manifestSha256,
63
+ archiveSha256
38
64
  };
39
65
  }
40
66
  /** Import a run from a portable JSON file into a target directory.
41
67
  * Rebuilds run paths relative to the target dir. */
42
68
  function importRun(exportPath, targetDir) {
43
- const raw = JSON.parse(node_fs_1.default.readFileSync(exportPath, "utf8"));
69
+ const raw = (0, state_1.readJson)(exportPath);
44
70
  if (raw.schemaVersion !== 1)
45
71
  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);
72
+ const archiveSha256 = sha256Bytes(node_fs_1.default.readFileSync(exportPath));
73
+ const files = normalizeArchiveFiles(raw);
74
+ verifyArchiveFileDigests(files, raw.integrity);
75
+ const oldRunDir = raw.run.paths.runDir;
76
+ const oldCwd = raw.run.cwd;
77
+ const runDir = node_path_1.default.join(targetDir, ".cw", "runs", raw.run.id);
48
78
  const paths = (0, state_1.createRunPaths)(runDir);
49
79
  (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
- }
80
+ for (const file of files) {
81
+ const destination = node_path_1.default.join(runDir, file.relativePath);
82
+ if (!(0, state_1.isContainedPath)(destination, runDir)) {
83
+ throw new Error(`Archive file escapes restore directory: ${file.relativePath}`);
60
84
  }
85
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(destination), { recursive: true });
86
+ node_fs_1.default.writeFileSync(destination, Buffer.from(file.contentBase64, "base64"));
61
87
  }
88
+ const externalPathMap = new Map();
89
+ for (const file of files) {
90
+ if (file.sourcePath)
91
+ externalPathMap.set(file.sourcePath, node_path_1.default.join(runDir, file.relativePath));
92
+ }
93
+ const run = rebaseRun(raw.run, {
94
+ oldRunDir,
95
+ newRunDir: runDir,
96
+ oldCwd,
97
+ newCwd: targetDir,
98
+ paths,
99
+ externalPathMap
100
+ });
62
101
  (0, state_1.saveCheckpoint)(run);
63
- return { run, runDir, statePath: paths.state };
102
+ const manifest = {
103
+ schemaVersion: 1,
104
+ runId: run.id,
105
+ importedAt: new Date().toISOString(),
106
+ sourceVersion: raw.sourceVersion,
107
+ archiveSha256,
108
+ manifestSha256: digestManifest(files),
109
+ files: files.map(({ contentBase64: _contentBase64, ...file }) => file)
110
+ };
111
+ const manifestPath = importManifestPath(run);
112
+ (0, state_1.writeJson)(manifestPath, manifest, { durable: true });
113
+ const verification = verifyImportedRun(run);
114
+ return {
115
+ run,
116
+ runDir,
117
+ statePath: paths.state,
118
+ manifestPath,
119
+ verifyCommand: `cw run verify-import ${run.id} --cwd ${targetDir} --json`,
120
+ verification
121
+ };
122
+ }
123
+ /** Verify an imported run against its restore manifest and telemetry chain. */
124
+ function verifyImportedRun(run) {
125
+ const manifestPath = importManifestPath(run);
126
+ const checks = [];
127
+ if (!node_fs_1.default.existsSync(manifestPath)) {
128
+ return {
129
+ runId: run.id,
130
+ ok: false,
131
+ manifestPath,
132
+ checkedFiles: 0,
133
+ checks: [{ name: "import-manifest", pass: false, code: "missing-import-manifest", path: manifestPath }]
134
+ };
135
+ }
136
+ let manifest;
137
+ try {
138
+ manifest = (0, state_1.readJson)(manifestPath);
139
+ }
140
+ catch (error) {
141
+ return {
142
+ runId: run.id,
143
+ ok: false,
144
+ manifestPath,
145
+ checkedFiles: 0,
146
+ checks: [{ name: "import-manifest", pass: false, code: "invalid-import-manifest", path: manifestPath, actual: messageOf(error) }]
147
+ };
148
+ }
149
+ const currentManifestDigest = digestManifest(manifest.files.map((file) => ({ ...file, contentBase64: "" })));
150
+ checks.push({
151
+ name: "import-manifest",
152
+ pass: manifest.runId === run.id && manifest.manifestSha256 === currentManifestDigest,
153
+ code: manifest.runId !== run.id ? "run-id-mismatch" : manifest.manifestSha256 === currentManifestDigest ? undefined : "manifest-digest-mismatch",
154
+ expected: manifest.manifestSha256,
155
+ actual: currentManifestDigest
156
+ });
157
+ let filesOk = true;
158
+ for (const file of manifest.files) {
159
+ const restoredPath = node_path_1.default.join(run.paths.runDir, file.relativePath);
160
+ if (!(0, state_1.isContainedPath)(restoredPath, run.paths.runDir)) {
161
+ filesOk = false;
162
+ checks.push({ name: "archive-file", pass: false, code: "path-escape", path: file.relativePath });
163
+ continue;
164
+ }
165
+ if (!node_fs_1.default.existsSync(restoredPath)) {
166
+ filesOk = false;
167
+ checks.push({ name: "archive-file", pass: false, code: "missing-file", path: file.relativePath, expected: file.sha256 });
168
+ continue;
169
+ }
170
+ const actual = sha256Bytes(node_fs_1.default.readFileSync(restoredPath));
171
+ const pass = actual === file.sha256;
172
+ if (!pass)
173
+ filesOk = false;
174
+ checks.push({
175
+ name: "archive-file",
176
+ pass,
177
+ code: pass ? undefined : "digest-mismatch",
178
+ path: file.relativePath,
179
+ expected: file.sha256,
180
+ actual
181
+ });
182
+ }
183
+ checks.push({ name: "archive-files", pass: filesOk, code: filesOk ? undefined : "archive-files-invalid" });
184
+ const telemetry = (0, telemetry_ledger_1.verifyTelemetryLedger)(run);
185
+ checks.push({
186
+ name: "telemetry-ledger",
187
+ pass: telemetry.verified,
188
+ code: telemetry.verified ? undefined : "telemetry-ledger-invalid"
189
+ });
190
+ return {
191
+ runId: run.id,
192
+ ok: checks.every((check) => check.pass),
193
+ manifestPath,
194
+ checkedFiles: manifest.files.length,
195
+ checks
196
+ };
197
+ }
198
+ function importManifestPath(run) {
199
+ return node_path_1.default.join(run.paths.runDir, "import-manifest.json");
200
+ }
201
+ function collectArchiveFiles(run) {
202
+ const entries = new Map();
203
+ for (const file of walkFiles(run.paths.runDir)) {
204
+ const relativePath = toArchivePath(node_path_1.default.relative(run.paths.runDir, file));
205
+ if (!relativePath || relativePath === "state.json" || relativePath === "import-manifest.json" || relativePath.endsWith(".lock"))
206
+ continue;
207
+ addFile(entries, run, file, roleForRelativePath(relativePath));
208
+ }
209
+ for (const artifactPath of collectReferencedArtifactPaths(run)) {
210
+ if (!artifactPath || !node_fs_1.default.existsSync(artifactPath) || !node_fs_1.default.statSync(artifactPath).isFile())
211
+ continue;
212
+ if ((0, state_1.isContainedPath)(artifactPath, run.paths.runDir)) {
213
+ addFile(entries, run, artifactPath, "artifact");
214
+ continue;
215
+ }
216
+ if ((0, state_1.isContainedPath)(artifactPath, run.cwd))
217
+ addExternalArtifactFile(entries, run, artifactPath);
218
+ }
219
+ return [...entries.values()].sort((left, right) => left.relativePath.localeCompare(right.relativePath));
220
+ }
221
+ function addFile(entries, run, file, role) {
222
+ const relativePath = toArchivePath(node_path_1.default.relative(run.paths.runDir, file));
223
+ if (relativePath === "state.json" || relativePath === "import-manifest.json")
224
+ return;
225
+ if (!relativePath || relativePath.startsWith("../"))
226
+ return;
227
+ const bytes = node_fs_1.default.readFileSync(file);
228
+ entries.set(relativePath, {
229
+ relativePath,
230
+ role,
231
+ contentBase64: bytes.toString("base64"),
232
+ sha256: sha256Bytes(bytes),
233
+ sizeBytes: bytes.length
234
+ });
235
+ }
236
+ function addExternalArtifactFile(entries, run, file) {
237
+ const sourcePath = node_path_1.default.resolve(file);
238
+ const bytes = node_fs_1.default.readFileSync(sourcePath);
239
+ const relativePath = `external-artifacts/${sha256Bytes(Buffer.from(sourcePath, "utf8")).slice(0, 16)}-${safeArchiveBasename(node_path_1.default.basename(sourcePath))}`;
240
+ entries.set(relativePath, {
241
+ relativePath,
242
+ role: "artifact",
243
+ contentBase64: bytes.toString("base64"),
244
+ sha256: sha256Bytes(bytes),
245
+ sizeBytes: bytes.length,
246
+ sourcePath
247
+ });
248
+ }
249
+ function collectReferencedArtifactPaths(run) {
250
+ const paths = new Set();
251
+ for (const node of run.nodes || []) {
252
+ for (const artifact of node.artifacts || [])
253
+ addArtifactPath(paths, run, artifact.path);
254
+ }
255
+ for (const candidate of run.candidates || []) {
256
+ for (const artifact of candidate.artifacts || [])
257
+ addArtifactPath(paths, run, artifact.path);
258
+ }
259
+ for (const selection of run.candidateSelections || []) {
260
+ for (const artifact of selection.artifacts || [])
261
+ addArtifactPath(paths, run, artifact.path);
262
+ }
263
+ for (const artifact of run.blackboard?.artifacts || [])
264
+ addArtifactPath(paths, run, artifact.path);
265
+ return [...paths].sort();
266
+ }
267
+ function addArtifactPath(paths, run, value) {
268
+ if (!value)
269
+ return;
270
+ paths.add(node_path_1.default.isAbsolute(value) ? value : node_path_1.default.resolve(run.cwd, value));
271
+ }
272
+ function walkFiles(root) {
273
+ if (!node_fs_1.default.existsSync(root))
274
+ return [];
275
+ const found = [];
276
+ for (const name of node_fs_1.default.readdirSync(root)) {
277
+ const file = node_path_1.default.join(root, name);
278
+ const stat = node_fs_1.default.lstatSync(file);
279
+ if (stat.isSymbolicLink())
280
+ continue;
281
+ if (stat.isDirectory())
282
+ found.push(...walkFiles(file));
283
+ else if (stat.isFile())
284
+ found.push(file);
285
+ }
286
+ return found;
287
+ }
288
+ function roleForRelativePath(relativePath) {
289
+ if (relativePath === "telemetry.json")
290
+ return "telemetry";
291
+ if (relativePath === "audit" || relativePath.startsWith("audit/"))
292
+ return "audit";
293
+ if (relativePath === "artifacts" || relativePath.startsWith("artifacts/"))
294
+ return "artifact";
295
+ return "run-file";
296
+ }
297
+ function normalizeArchiveFiles(raw) {
298
+ const modern = raw.files || [];
299
+ if (modern.length) {
300
+ return modern.map((file) => ({
301
+ relativePath: cleanArchiveRelativePath(file.relativePath),
302
+ role: file.role,
303
+ contentBase64: file.contentBase64,
304
+ sha256: file.sha256,
305
+ sizeBytes: file.sizeBytes,
306
+ sourcePath: file.sourcePath
307
+ }));
308
+ }
309
+ return (raw.artifacts || []).map((artifact) => {
310
+ const contentBase64 = artifact.contentBase64 || Buffer.from(artifact.content || "", "utf8").toString("base64");
311
+ const bytes = Buffer.from(contentBase64, "base64");
312
+ return {
313
+ relativePath: cleanArchiveRelativePath(artifact.path),
314
+ role: "artifact",
315
+ contentBase64,
316
+ sha256: artifact.sha256 || sha256Bytes(bytes),
317
+ sizeBytes: artifact.sizeBytes ?? bytes.length
318
+ };
319
+ });
320
+ }
321
+ function verifyArchiveFileDigests(files, integrity) {
322
+ for (const file of files) {
323
+ const bytes = Buffer.from(file.contentBase64, "base64");
324
+ 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}`);
329
+ }
330
+ if (integrity) {
331
+ 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
+ }
337
+ }
338
+ }
339
+ function digestManifest(files) {
340
+ const manifest = files
341
+ .map((file) => ({
342
+ relativePath: file.relativePath,
343
+ role: file.role,
344
+ sha256: file.sha256,
345
+ sizeBytes: file.sizeBytes,
346
+ sourcePath: file.sourcePath
347
+ }))
348
+ .sort((left, right) => left.relativePath.localeCompare(right.relativePath));
349
+ return sha256Bytes(Buffer.from(JSON.stringify(manifest), "utf8"));
350
+ }
351
+ function rebaseRun(source, context) {
352
+ const cloned = deepRebase(JSON.parse(JSON.stringify(source)), context);
353
+ cloned.cwd = context.newCwd;
354
+ cloned.paths = context.paths;
355
+ cloned.updatedAt = new Date().toISOString();
356
+ cloned.audit = cloned.audit
357
+ ? {
358
+ schemaVersion: 1,
359
+ eventLogPath: node_path_1.default.join(context.paths.auditDir || node_path_1.default.join(context.paths.runDir, "audit"), "events.jsonl"),
360
+ summaryPath: node_path_1.default.join(context.paths.auditDir || node_path_1.default.join(context.paths.runDir, "audit"), "summary.json"),
361
+ indexPath: node_path_1.default.join(context.paths.auditDir || node_path_1.default.join(context.paths.runDir, "audit"), "index.json")
362
+ }
363
+ : cloned.audit;
364
+ return cloned;
365
+ }
366
+ function deepRebase(value, context) {
367
+ if (typeof value === "string")
368
+ return rebaseString(value, context);
369
+ if (Array.isArray(value))
370
+ return value.map((entry) => deepRebase(entry, context));
371
+ if (value && typeof value === "object") {
372
+ const out = {};
373
+ for (const [key, entry] of Object.entries(value))
374
+ out[key] = deepRebase(entry, context);
375
+ return out;
376
+ }
377
+ return value;
378
+ }
379
+ function rebaseString(value, context) {
380
+ const archivedExternal = context.externalPathMap?.get(value);
381
+ if (archivedExternal)
382
+ return archivedExternal;
383
+ if (value === context.oldRunDir || value.startsWith(context.oldRunDir + node_path_1.default.sep)) {
384
+ return context.newRunDir + value.slice(context.oldRunDir.length);
385
+ }
386
+ if (value === context.oldCwd || value.startsWith(context.oldCwd + node_path_1.default.sep)) {
387
+ return context.newCwd + value.slice(context.oldCwd.length);
388
+ }
389
+ return value;
390
+ }
391
+ function cleanArchiveRelativePath(value) {
392
+ const cleaned = toArchivePath(value).replace(/^\/+/, "");
393
+ if (!cleaned || cleaned === "." || cleaned.startsWith("../") || cleaned.includes("/../")) {
394
+ throw new Error(`Invalid archive relative path: ${value}`);
395
+ }
396
+ return cleaned;
397
+ }
398
+ function toArchivePath(value) {
399
+ return value.split(node_path_1.default.sep).join("/");
400
+ }
401
+ function safeArchiveBasename(value) {
402
+ return value.replace(/[^A-Za-z0-9._-]/g, "_") || "artifact";
403
+ }
404
+ function messageOf(error) {
405
+ return error instanceof Error ? error.message : String(error);
406
+ }
407
+ function sha256Bytes(bytes) {
408
+ return node_crypto_1.default.createHash("sha256").update(bytes).digest("hex");
64
409
  }
@@ -210,6 +210,12 @@ class RunRegistry {
210
210
  return { schemaVersion: 1, links: {} };
211
211
  }
212
212
  }
213
+ loadRepoOverlays(repo) {
214
+ return {
215
+ archive: this.loadArchiveOverlay(repo),
216
+ provenance: this.loadProvenanceOverlay(repo)
217
+ };
218
+ }
213
219
  // ---- home registry files ------------------------------------------------
214
220
  reposFilePath() {
215
221
  return node_path_1.default.join(this.homeRegistryDir(), "repos.json");
@@ -286,7 +292,7 @@ class RunRegistry {
286
292
  /** Derive a RunRecord from a run directory's source state.json. Returns the
287
293
  * record, or null when source is unreadable/unsupported (caller decides how to
288
294
  * surface `missing` — we never fabricate a status). */
289
- deriveRecord(repo, runDir) {
295
+ deriveRecord(repo, runDir, overlays = this.loadRepoOverlays(repo)) {
290
296
  const statePath = node_path_1.default.join(runDir, "state.json");
291
297
  if (!node_fs_1.default.existsSync(statePath))
292
298
  return null;
@@ -302,8 +308,8 @@ class RunRegistry {
302
308
  }
303
309
  const li = lifecycleInputs(run);
304
310
  const derived = deriveLifecycle(li);
305
- const archive = this.loadArchiveOverlay(repo).archived[run.id];
306
- const provenance = this.loadProvenanceOverlay(repo).links[run.id];
311
+ const archive = overlays.archive.archived[run.id];
312
+ const provenance = overlays.provenance.links[run.id];
307
313
  // Run Retention & Provable Reclamation (v0.1.39): the per-run reclaimed.json
308
314
  // overlay (if any) raises the disk-tier above `archived` and downgrades the
309
315
  // capability. Derived from source, never invented.
@@ -364,11 +370,12 @@ class RunRegistry {
364
370
  const runsDir = this.repoRunsDir(repo);
365
371
  if (!node_fs_1.default.existsSync(runsDir))
366
372
  return [];
373
+ const overlays = this.loadRepoOverlays(repo);
367
374
  const records = [];
368
375
  for (const entry of node_fs_1.default.readdirSync(runsDir, { withFileTypes: true })) {
369
376
  if (!entry.isDirectory())
370
377
  continue;
371
- const record = this.deriveRecord(repo, node_path_1.default.join(runsDir, entry.name));
378
+ const record = this.deriveRecord(repo, node_path_1.default.join(runsDir, entry.name), overlays);
372
379
  if (record)
373
380
  records.push(record);
374
381
  }