cool-workflow 0.1.79 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +9 -1
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +64 -0
- package/apps/architecture-review-fast/workflow.js +153 -0
- package/apps/end-to-end-golden-path/app.json +1 -1
- package/apps/pr-review-fix-ci/app.json +1 -1
- package/apps/release-cut/app.json +1 -1
- package/apps/research-synthesis/app.json +1 -1
- package/dist/capability-core.js +38 -0
- package/dist/capability-registry.js +11 -8
- package/dist/cli.js +10 -1
- package/dist/drive.js +74 -1
- package/dist/evidence-reasoning.js +2 -2
- package/dist/execution-backend.js +6 -1
- package/dist/mcp-server.js +48 -13
- package/dist/orchestrator/lifecycle-operations.js +2 -1
- package/dist/orchestrator.js +1 -1
- package/dist/run-export.js +370 -25
- package/dist/run-registry.js +11 -4
- package/dist/state-explosion.js +100 -21
- package/dist/version.js +1 -1
- package/docs/agent-delegation-drive.7.md +58 -0
- package/docs/canonical-workflow-apps.7.md +37 -0
- package/docs/cli-mcp-parity.7.md +12 -0
- package/docs/contract-migration-tooling.7.md +4 -0
- package/docs/control-plane-scheduling.7.md +4 -0
- package/docs/durable-state-and-locking.7.md +4 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
- package/docs/execution-backends.7.md +4 -0
- package/docs/index.md +1 -0
- package/docs/launch/demo.tape +28 -0
- package/docs/launch/launch-kit.md +59 -3
- package/docs/launch/pre-launch-checklist.md +53 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
- package/docs/multi-agent-eval-replay-harness.7.md +4 -0
- package/docs/multi-agent-operator-ux.7.md +4 -0
- package/docs/node-snapshot-diff-replay.7.md +4 -0
- package/docs/observability-cost-accounting.7.md +4 -0
- package/docs/project-index.md +13 -5
- package/docs/real-execution-backends.7.md +4 -0
- package/docs/release-and-migration.7.md +4 -0
- package/docs/release-tooling.7.md +4 -0
- package/docs/routines.md +23 -0
- package/docs/run-registry-control-plane.7.md +42 -1
- package/docs/run-retention-reclamation.7.md +4 -0
- package/docs/source-context-profiles.7.md +119 -0
- package/docs/state-explosion-management.7.md +11 -0
- package/docs/team-collaboration.7.md +4 -0
- package/docs/unix-principles.md +49 -1
- package/docs/web-desktop-workbench.7.md +4 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +142 -0
- package/package.json +2 -1
- package/scripts/agents/claude-p-agent.js +129 -43
- package/scripts/architecture-review-fast.js +362 -0
- package/scripts/bump-version.js +1 -0
- package/scripts/canonical-apps.js +21 -4
- package/scripts/coverage-gate.js +211 -0
- package/scripts/dogfood-release.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/source-context.js +291 -0
- package/scripts/version-sync-check.js +1 -0
- package/skills/ci-triage/SKILL.md +50 -0
- package/skills/ci-triage/agents/openai.yaml +4 -0
- package/skills/cool-workflow/SKILL.md +4 -1
- package/skills/deploy-check/SKILL.md +55 -0
- package/skills/deploy-check/agents/openai.yaml +4 -0
- package/skills/design-qa/SKILL.md +49 -0
- package/skills/design-qa/agents/openai.yaml +4 -0
- package/skills/pr-review/SKILL.md +45 -0
- package/skills/pr-review/agents/openai.yaml +4 -0
package/dist/mcp-server.js
CHANGED
|
@@ -391,6 +391,12 @@ function callTool(name, args) {
|
|
|
391
391
|
return (0, capability_core_1.runArchive)((0, capability_core_1.runRegistryFor)(args, runner), (0, capability_core_1.optionalString)(args.runId), args);
|
|
392
392
|
case "cw_run_rerun":
|
|
393
393
|
return (0, capability_core_1.runRerun)((0, capability_core_1.runRegistryFor)(args, runner), String(args.runId || ""), args);
|
|
394
|
+
case "cw_run_export":
|
|
395
|
+
return (0, capability_core_1.runExportArchive)(runner, String(args.runId || ""), args);
|
|
396
|
+
case "cw_run_import":
|
|
397
|
+
return (0, capability_core_1.runImportArchive)(runner, args);
|
|
398
|
+
case "cw_run_verify_import":
|
|
399
|
+
return (0, capability_core_1.runVerifyImport)(runner, String(args.runId || ""), args);
|
|
394
400
|
case "cw_run_drive":
|
|
395
401
|
return (0, capability_core_1.runDrivePreview)(runner, args);
|
|
396
402
|
case "cw_run_drive_step":
|
|
@@ -512,8 +518,10 @@ function requiredArgsForTool(name) {
|
|
|
512
518
|
return ["runId", "targetKind|kind", "targetId|target", "body|message|text"];
|
|
513
519
|
if (name === "cw_handoff")
|
|
514
520
|
return ["runId", "targetKind|kind", "targetId|target", "to|toActor"];
|
|
515
|
-
if (name === "cw_run_show" || name === "cw_run_resume" || name === "cw_run_rerun")
|
|
521
|
+
if (name === "cw_run_show" || name === "cw_run_resume" || name === "cw_run_rerun" || name === "cw_run_export" || name === "cw_run_verify_import")
|
|
516
522
|
return ["runId"];
|
|
523
|
+
if (name === "cw_run_import")
|
|
524
|
+
return ["archive|path|file"];
|
|
517
525
|
if (name === "cw_run_archive")
|
|
518
526
|
return ["runId|olderThanDays"];
|
|
519
527
|
if (name === "cw_gc_verify")
|
|
@@ -704,24 +712,23 @@ function toolDefinitions() {
|
|
|
704
712
|
contract: stringSchema("run-state | workflow-app (default run-state)"),
|
|
705
713
|
cwd: stringSchema("Run workspace")
|
|
706
714
|
}),
|
|
707
|
-
|
|
708
|
-
tool("cw_operator_graph", "Read the structured Operator UX run graph.", runIdSchema()),
|
|
709
|
-
tool("cw_operator_report", "Refresh and read the structured Operator UX report summary.", runIdSchema()),
|
|
710
|
-
tool("cw_worker_summary", "Read the structured worker summary for a run.", runIdSchema()),
|
|
715
|
+
...runIdCapabilityTools(["operator.status", "graph", "operator.report", "worker.summary"]),
|
|
711
716
|
tool("cw_workbench_view", "Read the read-only five-panel Workbench view (graph, blackboard, worker, candidate, audit) for one run. Each panel embeds the verbatim `cw <cmd> --json` payload of one existing capability; absent panels are surfaced honestly. Peer of `cw workbench view`.", runIdSchema()),
|
|
712
717
|
tool("cw_workbench_serve", "Describe the optional localhost-only, read-only Workbench host (bind, scope, routes). Returns the serve descriptor identical to `cw workbench serve --json`; MCP never starts the blocking server.", {
|
|
713
718
|
cwd: stringSchema("Run workspace"),
|
|
714
719
|
port: numberSchema("Optional loopback port, defaults to 7717"),
|
|
715
720
|
scope: stringSchema("Registry scope: repo|home")
|
|
716
721
|
}),
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
722
|
+
...runIdCapabilityTools([
|
|
723
|
+
"candidate.summary",
|
|
724
|
+
"feedback.summary",
|
|
725
|
+
"commit.summary",
|
|
726
|
+
"multi-agent.summary",
|
|
727
|
+
"multi-agent.graph",
|
|
728
|
+
"multi-agent.dependencies",
|
|
729
|
+
"multi-agent.failures",
|
|
730
|
+
"multi-agent.evidence"
|
|
731
|
+
]),
|
|
725
732
|
tool("cw_evidence_reasoning", "Explain WHY each evidence item was adopted/rejected/superseded/conflicting: a derived, fingerprinted reasoning chain with decision, basis, authority, rationale, and counterfactual per gate (fanin, candidate-score, selection, verifier, commit). Fails closed to `unexplained` when a rationale cannot be traced. Reads valid|stale|absent freshness against current source state.", {
|
|
726
733
|
...runIdSchema(),
|
|
727
734
|
evidence: stringSchema("Optional evidence id/ref to explain a single adoption"),
|
|
@@ -1440,6 +1447,25 @@ function toolDefinitions() {
|
|
|
1440
1447
|
scope: stringSchema("home (default, cross-repo) or repo"),
|
|
1441
1448
|
reason: stringSchema("Rerun reason")
|
|
1442
1449
|
}),
|
|
1450
|
+
tool("cw_run_export", "Export a run to a portable, digest-checked archive containing run-local artifacts, audit overlays, telemetry, reports, workers, and commit snapshots.", {
|
|
1451
|
+
runId: stringSchema("Run id to export"),
|
|
1452
|
+
cwd: stringSchema("Repo workspace containing .cw/runs/<run-id>"),
|
|
1453
|
+
output: stringSchema("Archive output path"),
|
|
1454
|
+
path: stringSchema("Alias for output"),
|
|
1455
|
+
archive: stringSchema("Alias for output")
|
|
1456
|
+
}),
|
|
1457
|
+
tool("cw_run_import", "Restore a portable run archive into a target repo and immediately verify restored file digests.", {
|
|
1458
|
+
archive: stringSchema("Archive path"),
|
|
1459
|
+
path: stringSchema("Alias for archive"),
|
|
1460
|
+
file: stringSchema("Alias for archive"),
|
|
1461
|
+
target: stringSchema("Restore target repo directory"),
|
|
1462
|
+
repo: stringSchema("Alias for target"),
|
|
1463
|
+
cwd: stringSchema("Invocation workspace")
|
|
1464
|
+
}),
|
|
1465
|
+
tool("cw_run_verify_import", "Verify an imported run against its restore manifest and telemetry chain; detects missing or tampered restored files.", {
|
|
1466
|
+
runId: stringSchema("Imported run id to verify"),
|
|
1467
|
+
cwd: stringSchema("Restored repo workspace")
|
|
1468
|
+
}),
|
|
1443
1469
|
tool("cw_run_drive", "Preview the next agent-delegation drive step for a run (read-only, deterministic). Counts come from state; no spawn, no mutation.", {
|
|
1444
1470
|
runId: stringSchema("Run id to preview"),
|
|
1445
1471
|
cwd: stringSchema("Run workspace")
|
|
@@ -1560,6 +1586,15 @@ function tool(name, description, properties) {
|
|
|
1560
1586
|
}
|
|
1561
1587
|
};
|
|
1562
1588
|
}
|
|
1589
|
+
function runIdCapabilityTools(capabilityIds) {
|
|
1590
|
+
return capabilityIds.map((capabilityId) => capabilityTool(capabilityId, runIdSchema()));
|
|
1591
|
+
}
|
|
1592
|
+
function capabilityTool(capabilityId, properties) {
|
|
1593
|
+
const descriptor = capability_registry_1.CAPABILITY_REGISTRY.find((capability) => capability.capability === capabilityId);
|
|
1594
|
+
if (!descriptor?.mcp)
|
|
1595
|
+
throw new Error(`MCP capability not declared: ${capabilityId}`);
|
|
1596
|
+
return tool(descriptor.mcp.tool, descriptor.summary, properties);
|
|
1597
|
+
}
|
|
1563
1598
|
function stringSchema(description) {
|
|
1564
1599
|
return { type: "string", description };
|
|
1565
1600
|
}
|
|
@@ -435,7 +435,8 @@ function flattenTasks(workflow, inputs) {
|
|
|
435
435
|
// model (per-task delegation override), agentType (dispatch backend).
|
|
436
436
|
...(task.label ? { label: task.label } : {}),
|
|
437
437
|
...(task.model ? { model: task.model } : {}),
|
|
438
|
-
...(task.agentType ? { agentType: task.agentType } : {})
|
|
438
|
+
...(task.agentType ? { agentType: task.agentType } : {}),
|
|
439
|
+
...(task.resultCache ? { resultCache: task.resultCache } : {})
|
|
439
440
|
});
|
|
440
441
|
}
|
|
441
442
|
}
|
package/dist/orchestrator.js
CHANGED
|
@@ -838,7 +838,7 @@ function formatHelp() {
|
|
|
838
838
|
" schedule create|list|due|complete|pause|resume|run-now|history|daemon|delete",
|
|
839
839
|
" routine create|fire|list|events|delete",
|
|
840
840
|
" registry refresh|show [--scope repo|home] [--json]",
|
|
841
|
-
" run search|list|show|resume|archive|rerun [run-id] [--scope repo|home] [--json]",
|
|
841
|
+
" run search|list|show|resume|archive|rerun|export|import|verify-import [run-id|archive] [--scope repo|home] [--json]",
|
|
842
842
|
" queue add|list|drain|show [queue-id] [--repo PATH] [--priority N]",
|
|
843
843
|
" history [--scope repo|home] [--app ID] [--status STATE] [--json]",
|
|
844
844
|
" workbench view <run-id> [--json]",
|
package/dist/run-export.js
CHANGED
|
@@ -1,64 +1,409 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// Run Export / Import — portable run archive format (
|
|
2
|
+
// Run Export / Import — portable run archive format (Track B).
|
|
3
3
|
//
|
|
4
|
-
// BSD discipline: explicit state, portable format. Export serializes a run
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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 =
|
|
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
|
|
47
|
-
const
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/run-registry.js
CHANGED
|
@@ -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 =
|
|
306
|
-
const provenance =
|
|
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
|
}
|