cool-workflow 0.1.82 → 0.1.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +4 -4
- package/README.md +124 -120
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- 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 +16 -8
- package/dist/capability-registry.js +8 -0
- package/dist/cli.js +12 -1
- package/dist/commit.js +5 -1
- package/dist/doctor.js +153 -0
- package/dist/mcp-server.js +11 -0
- package/dist/orchestrator.js +13 -0
- package/dist/reclamation/hash.js +72 -0
- package/dist/reclamation.js +25 -78
- package/dist/run-registry/queue.js +6 -7
- package/dist/run-registry.js +35 -24
- package/dist/scheduler.js +78 -53
- package/dist/version.js +1 -1
- package/dist/worker-isolation.js +22 -2
- package/docs/agent-delegation-drive.7.md +90 -86
- package/docs/agent-framework.md +33 -32
- package/docs/candidate-scoring.7.md +26 -24
- package/docs/canonical-workflow-apps.7.md +40 -40
- package/docs/capability-topology-registry.7.md +24 -24
- package/docs/cli-mcp-parity.7.md +226 -154
- package/docs/contract-migration-tooling.7.md +48 -41
- package/docs/control-plane-scheduling.7.md +45 -41
- package/docs/coordinator-blackboard.7.md +30 -30
- package/docs/dogfood-one-real-repo.7.md +44 -44
- package/docs/durable-state-and-locking.7.md +34 -30
- package/docs/end-to-end-golden-path.7.md +29 -29
- package/docs/error-feedback.7.md +27 -27
- package/docs/evidence-adoption-reasoning-chain.7.md +62 -58
- package/docs/execution-backends.7.md +84 -80
- package/docs/getting-started.md +35 -18
- package/docs/index.md +3 -3
- package/docs/mcp-app-surface.7.md +64 -64
- package/docs/multi-agent-cli-mcp-surface.7.md +82 -77
- package/docs/multi-agent-eval-replay-harness.7.md +59 -55
- package/docs/multi-agent-operator-ux.7.md +69 -65
- package/docs/multi-agent-runtime-core.7.md +39 -39
- package/docs/multi-agent-topologies.7.md +24 -24
- package/docs/multi-agent-trust-policy-audit.7.md +38 -38
- package/docs/node-snapshot-diff-replay.7.md +26 -22
- package/docs/observability-cost-accounting.7.md +49 -45
- package/docs/operator-ux.7.md +30 -30
- package/docs/pipeline-runner.7.md +31 -31
- package/docs/project-index.md +10 -5
- package/docs/real-execution-backends.7.md +47 -43
- package/docs/release-and-migration.7.md +42 -38
- package/docs/release-tooling.7.md +53 -49
- package/docs/routines.md +16 -16
- package/docs/run-registry-control-plane.7.md +120 -116
- package/docs/run-retention-reclamation.7.md +45 -41
- package/docs/sandbox-profiles.7.md +32 -32
- package/docs/scheduled-tasks.md +14 -14
- package/docs/security-trust-hardening.7.md +29 -29
- package/docs/source-context-profiles.7.md +28 -28
- package/docs/state-explosion-management.7.md +63 -59
- package/docs/state-node.7.md +8 -8
- package/docs/team-collaboration.7.md +62 -58
- package/docs/trust-model.md +126 -126
- package/docs/unix-principles.md +80 -80
- package/docs/vendor-manifest-loadability.7.md +20 -20
- package/docs/verifier-gated-commit.7.md +16 -16
- package/docs/web-desktop-workbench.7.md +69 -65
- package/docs/worker-isolation.7.md +34 -37
- package/docs/workflow-app-framework.7.md +38 -38
- package/manifest/plugin.manifest.json +4 -4
- package/package.json +3 -2
- package/scripts/canonical-apps.js +4 -4
- package/scripts/dogfood-release.js +1 -1
- package/scripts/gen-parity-doc.js +106 -0
- package/scripts/golden-path.js +4 -4
- package/dist/verifier-registry.js +0 -46
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "pr-review-fix-ci",
|
|
4
4
|
"title": "PR Review Fix CI",
|
|
5
5
|
"summary": "Review a pull request or branch, inspect CI failures, diagnose actionable issues, optionally patch, verify, and summarize with evidence.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.83",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "release-cut",
|
|
4
4
|
"title": "Release Cut",
|
|
5
5
|
"summary": "Prepare a release with checklist discipline: version checks, changelog, tests, packaging, release notes, and final verification.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.83",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "research-synthesis",
|
|
4
4
|
"title": "Research Synthesis",
|
|
5
5
|
"summary": "Split a research question into claims, investigate sources, cross-check evidence, verify claims, and synthesize a concise answer.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.83",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
package/dist/capability-core.js
CHANGED
|
@@ -333,13 +333,12 @@ function queueShow(reg, id) {
|
|
|
333
333
|
// ---- control-plane scheduling (v0.1.37) -----------------------------------
|
|
334
334
|
function loadSchedulingPolicy(reg) {
|
|
335
335
|
const file = reg.schedulingPolicyPath();
|
|
336
|
+
// Absent policy => conservative DEFAULT (an unconfigured backend, which §4
|
|
337
|
+
// permits). But a PRESENT-but-corrupt policy must fail closed: silently
|
|
338
|
+
// substituting defaults would schedule under settings the operator never
|
|
339
|
+
// chose while their broken file sits on disk. Let readJson's throw surface it.
|
|
336
340
|
if (node_fs_1.default.existsSync(file)) {
|
|
337
|
-
|
|
338
|
-
return { policy: (0, scheduling_1.normalizeSchedulingPolicy)((0, state_1.readJson)(file)), source: "file" };
|
|
339
|
-
}
|
|
340
|
-
catch {
|
|
341
|
-
/* fall through to default */
|
|
342
|
-
}
|
|
341
|
+
return { policy: (0, scheduling_1.normalizeSchedulingPolicy)((0, state_1.readJson)(file)), source: "file" };
|
|
343
342
|
}
|
|
344
343
|
return { policy: scheduling_1.DEFAULT_SCHEDULING_POLICY, source: "default" };
|
|
345
344
|
}
|
|
@@ -400,8 +399,17 @@ function schedPolicySet(reg, args) {
|
|
|
400
399
|
const current = loadSchedulingPolicy(reg).policy;
|
|
401
400
|
const patch = {};
|
|
402
401
|
for (const key of ["maxConcurrent", "maxAttempts", "leaseTtlMs", "backoffBaseMs", "backoffFactor", "backoffCapMs"]) {
|
|
403
|
-
if (args[key]
|
|
404
|
-
|
|
402
|
+
if (args[key] === undefined)
|
|
403
|
+
continue;
|
|
404
|
+
// Fail closed on a non-numeric flag instead of letting normalizeSchedulingPolicy
|
|
405
|
+
// silently substitute the DEFAULT (which would report source:"file" + exit 0,
|
|
406
|
+
// so the operator believes they set a value they didn't). Matches the
|
|
407
|
+
// Number.isFinite guard the sibling reclaimPolicyFrom already uses.
|
|
408
|
+
const value = Number(args[key]);
|
|
409
|
+
if (!Number.isFinite(value)) {
|
|
410
|
+
throw new Error(`Invalid --${key} "${String(args[key])}": expected a number (e.g. --${key} 4)`);
|
|
411
|
+
}
|
|
412
|
+
patch[key] = value;
|
|
405
413
|
}
|
|
406
414
|
const policy = (0, scheduling_1.normalizeSchedulingPolicy)({ ...current, ...patch });
|
|
407
415
|
(0, state_1.writeJson)(reg.schedulingPolicyPath(), policy);
|
|
@@ -52,6 +52,14 @@ const BUILTIN_CAPABILITIES = [
|
|
|
52
52
|
cli: { path: ["list"], jsonMode: "default" },
|
|
53
53
|
mcp: { tool: "cw_list" }
|
|
54
54
|
},
|
|
55
|
+
{
|
|
56
|
+
capability: "doctor",
|
|
57
|
+
summary: "Diagnose the host for setup problems (Node version, agent backend, agent binary on PATH, git, writable home/repo state) and print an actionable fix per check.",
|
|
58
|
+
entry: "runDoctor",
|
|
59
|
+
surface: "cli-only",
|
|
60
|
+
cli: { path: ["doctor"], jsonMode: "flag" },
|
|
61
|
+
reason: "Environment diagnostics are inherently local to the CLI host — Node version, $PATH, $CW_HOME/cwd writability. An MCP client diagnosing the server process's environment is not meaningful; agents already receive the same readiness facts in their typed results (e.g. status: blocked, agentConfigured). Inspired by `brew doctor`."
|
|
62
|
+
},
|
|
55
63
|
{
|
|
56
64
|
capability: "init",
|
|
57
65
|
summary: "Scaffold a new workflow definition.",
|
package/dist/cli.js
CHANGED
|
@@ -21,6 +21,7 @@ const multi_agent_operator_ux_1 = require("./multi-agent-operator-ux");
|
|
|
21
21
|
const multi_agent_eval_1 = require("./multi-agent-eval");
|
|
22
22
|
const state_explosion_1 = require("./state-explosion");
|
|
23
23
|
const evidence_reasoning_1 = require("./evidence-reasoning");
|
|
24
|
+
const doctor_1 = require("./doctor");
|
|
24
25
|
async function main() {
|
|
25
26
|
const args = (0, orchestrator_1.parseArgv)(process.argv.slice(2));
|
|
26
27
|
const runner = new orchestrator_1.CoolWorkflowRunner({
|
|
@@ -36,6 +37,16 @@ async function main() {
|
|
|
36
37
|
case "list":
|
|
37
38
|
printJson(runner.listWorkflows());
|
|
38
39
|
return;
|
|
40
|
+
case "doctor": {
|
|
41
|
+
const report = (0, doctor_1.runDoctor)(args.options, process.env, String(args.options.cwd || process.cwd()));
|
|
42
|
+
if (wantsJson(args.options))
|
|
43
|
+
printJson(report);
|
|
44
|
+
else
|
|
45
|
+
process.stdout.write(`${(0, doctor_1.formatDoctorReport)(report)}\n`);
|
|
46
|
+
if (!report.ok)
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
39
50
|
case "init": {
|
|
40
51
|
const [workflowId] = args.positionals;
|
|
41
52
|
if (!workflowId)
|
|
@@ -1281,7 +1292,7 @@ async function main() {
|
|
|
1281
1292
|
}
|
|
1282
1293
|
function required(value, label) {
|
|
1283
1294
|
if (!value)
|
|
1284
|
-
throw new Error(`Missing ${label}
|
|
1295
|
+
throw new Error(`Missing ${label}. Run "cw.js help" for usage.`);
|
|
1285
1296
|
return value;
|
|
1286
1297
|
}
|
|
1287
1298
|
function optionalArg(value) {
|
package/dist/commit.js
CHANGED
|
@@ -584,7 +584,11 @@ function readGitHead(cwd) {
|
|
|
584
584
|
return (0, node_child_process_1.execFileSync)("git", ["rev-parse", "HEAD"], {
|
|
585
585
|
cwd,
|
|
586
586
|
encoding: "utf8",
|
|
587
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
587
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
588
|
+
// Bound the call: a hung filesystem, a held .git/index.lock, or a
|
|
589
|
+
// credential-helper prompt must not block the commit path forever. A
|
|
590
|
+
// timeout throws, which the catch below maps to "no git head".
|
|
591
|
+
timeout: 5000
|
|
588
592
|
}).trim();
|
|
589
593
|
}
|
|
590
594
|
catch {
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
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.runDoctor = runDoctor;
|
|
7
|
+
exports.formatDoctorReport = formatDoctorReport;
|
|
8
|
+
// `cw doctor` — environment diagnostics, in the spirit of `brew doctor`.
|
|
9
|
+
//
|
|
10
|
+
// Homebrew's `doctor` turned "something is subtly wrong with your setup" into a
|
|
11
|
+
// proactive, named list of problems each paired with a concrete fix. We borrow
|
|
12
|
+
// that idea: instead of letting a missing agent / old Node / unwritable state
|
|
13
|
+
// surface as a confusing mid-run failure, `cw doctor` probes the host up front
|
|
14
|
+
// and prints WHAT is wrong and WHAT TO DO about it.
|
|
15
|
+
//
|
|
16
|
+
// Discipline:
|
|
17
|
+
// - READ-ONLY. Probes versions, $PATH, and the WRITABILITY of the nearest
|
|
18
|
+
// existing ancestor dir (via access(2)) — it never creates `.cw/` or $CW_HOME
|
|
19
|
+
// as a side effect, so running `doctor` changes nothing on disk.
|
|
20
|
+
// - FAIL CLOSED. Any `fail` check ⇒ `ok:false` ⇒ the CLI exits non-zero. A
|
|
21
|
+
// `warn` (e.g. no agent yet — demo/preview still work) does not fail.
|
|
22
|
+
// - TWO RENDERINGS. Human text by default; a stable `--json` payload for scripts.
|
|
23
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
24
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
25
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
26
|
+
const node_child_process_1 = require("node:child_process");
|
|
27
|
+
const agent_config_1 = require("./agent-config");
|
|
28
|
+
/** Resolve a bare binary name against $PATH (or accept an explicit path). Returns
|
|
29
|
+
* the resolved path, or undefined when not found. No spawning. */
|
|
30
|
+
function whichBinary(bin, env) {
|
|
31
|
+
if (bin.includes("/") || bin.includes("\\")) {
|
|
32
|
+
try {
|
|
33
|
+
return node_fs_1.default.statSync(bin).isFile() ? bin : undefined;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const dirs = (env.PATH || "").split(node_path_1.default.delimiter).filter(Boolean);
|
|
40
|
+
const exts = process.platform === "win32" ? (env.PATHEXT || ".EXE;.CMD;.BAT").split(";") : [""];
|
|
41
|
+
for (const dir of dirs) {
|
|
42
|
+
for (const ext of exts) {
|
|
43
|
+
const candidate = node_path_1.default.join(dir, bin + ext);
|
|
44
|
+
try {
|
|
45
|
+
if (node_fs_1.default.statSync(candidate).isFile())
|
|
46
|
+
return candidate;
|
|
47
|
+
}
|
|
48
|
+
catch { /* keep looking */ }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
/** True when `target` could be created/written: walk up to the nearest EXISTING
|
|
54
|
+
* ancestor and require it to be a writable DIRECTORY. (A file in the path is not
|
|
55
|
+
* writable-as-a-dir even though access(2) W_OK on the file itself would pass.)
|
|
56
|
+
* Does NOT create anything — a diagnostic must not have side effects. */
|
|
57
|
+
function dirWritable(target) {
|
|
58
|
+
let dir = node_path_1.default.resolve(target);
|
|
59
|
+
for (;;) {
|
|
60
|
+
let stat;
|
|
61
|
+
try {
|
|
62
|
+
stat = node_fs_1.default.statSync(dir);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
const parent = node_path_1.default.dirname(dir);
|
|
66
|
+
if (parent === dir)
|
|
67
|
+
return false;
|
|
68
|
+
dir = parent;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (!stat.isDirectory())
|
|
72
|
+
return false; // an existing file blocks mkdir beneath it
|
|
73
|
+
try {
|
|
74
|
+
node_fs_1.default.accessSync(dir, node_fs_1.default.constants.W_OK);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function runDoctor(args = {}, env = process.env, cwd = process.cwd()) {
|
|
83
|
+
const checks = [];
|
|
84
|
+
// 1. Node runtime — the one hard prerequisite (README: v18+).
|
|
85
|
+
const major = Number((process.version.match(/^v(\d+)/) || [])[1]);
|
|
86
|
+
checks.push(Number.isFinite(major) && major >= 18
|
|
87
|
+
? { name: "node", status: "ok", detail: `Node ${process.version} (>= 18).` }
|
|
88
|
+
: { name: "node", status: "fail", detail: `Node ${process.version} is below the required v18.`, fix: "Install Node.js 18+ (e.g. `brew install node`, or https://nodejs.org)." });
|
|
89
|
+
// 2. Agent backend — CW delegates execution; without one, real runs park.
|
|
90
|
+
const cfg = (0, agent_config_1.resolveAgentConfig)(args, env);
|
|
91
|
+
if (cfg.source === "none") {
|
|
92
|
+
checks.push({
|
|
93
|
+
name: "agent",
|
|
94
|
+
status: "warn",
|
|
95
|
+
detail: "No agent backend configured — `demo` and `--preview` work, but a real run reports status: blocked.",
|
|
96
|
+
fix: 'Pass --agent-command "claude -p", set $CW_AGENT_COMMAND, or use --agent-command builtin:claude.'
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const binToken = cfg.command ? String(cfg.command).split(/\s+/)[0] : undefined;
|
|
101
|
+
checks.push({
|
|
102
|
+
name: "agent",
|
|
103
|
+
status: "ok",
|
|
104
|
+
detail: `Agent configured from ${cfg.source}${binToken ? `: ${binToken}` : cfg.endpoint ? " (HTTP endpoint)" : ""}.`
|
|
105
|
+
});
|
|
106
|
+
// 2b. If it is a command agent, is the binary actually on PATH?
|
|
107
|
+
if (binToken) {
|
|
108
|
+
const resolved = whichBinary(binToken, env);
|
|
109
|
+
checks.push(resolved
|
|
110
|
+
? { name: "agent-binary", status: "ok", detail: `Agent binary "${binToken}" found at ${resolved}.` }
|
|
111
|
+
: { name: "agent-binary", status: "warn", detail: `Configured agent binary "${binToken}" is not on $PATH.`, fix: `Install "${binToken}", or correct --agent-command / $CW_AGENT_COMMAND.` });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// 3. git — only needed for commit provenance (git HEAD); a warn, not a hard fail.
|
|
115
|
+
const git = (0, node_child_process_1.spawnSync)("git", ["--version"], { encoding: "utf8", timeout: 5000 });
|
|
116
|
+
checks.push(!git.error && git.status === 0
|
|
117
|
+
? { name: "git", status: "ok", detail: (String(git.stdout || "git").trim()) + "." }
|
|
118
|
+
: { name: "git", status: "warn", detail: "git is not available — commit provenance (git HEAD) is recorded as absent.", fix: "Install git (e.g. `brew install git`) if you want commit provenance." });
|
|
119
|
+
// 4. Home registry — the cross-repo run index lives here; must be writable.
|
|
120
|
+
const home = env.CW_HOME && String(env.CW_HOME).trim()
|
|
121
|
+
? node_path_1.default.resolve(String(env.CW_HOME))
|
|
122
|
+
: node_path_1.default.join(node_os_1.default.homedir(), ".local", "state", "cool-workflow");
|
|
123
|
+
checks.push(dirWritable(home)
|
|
124
|
+
? { name: "home-registry", status: "ok", detail: `Home registry location is writable (${home}).` }
|
|
125
|
+
: { name: "home-registry", status: "fail", detail: `Home registry location is not writable: ${home}`, fix: "Set $CW_HOME to a writable directory, or fix the permissions." });
|
|
126
|
+
// 5. Working-dir state — per-repo runs land under <cwd>/.cw.
|
|
127
|
+
const cwState = node_path_1.default.join(node_path_1.default.resolve(cwd), ".cw");
|
|
128
|
+
checks.push(dirWritable(cwState)
|
|
129
|
+
? { name: "repo-state", status: "ok", detail: `Run state location is writable (${cwState}).` }
|
|
130
|
+
: { name: "repo-state", status: "warn", detail: `Cannot write run state under ${cwState}.`, fix: "Run from a writable working directory, or pass --cwd PATH." });
|
|
131
|
+
const fails = checks.filter((c) => c.status === "fail").length;
|
|
132
|
+
const warns = checks.filter((c) => c.status === "warn").length;
|
|
133
|
+
const ok = fails === 0;
|
|
134
|
+
const summary = ok
|
|
135
|
+
? warns === 0
|
|
136
|
+
? "ready — all checks passed"
|
|
137
|
+
: `ready, with ${warns} warning${warns === 1 ? "" : "s"}`
|
|
138
|
+
: `${fails} blocking problem${fails === 1 ? "" : "s"} found`;
|
|
139
|
+
return { schemaVersion: 1, ok, checks, summary };
|
|
140
|
+
}
|
|
141
|
+
/** Human rendering (TTY/default). `--json` callers use the report object directly. */
|
|
142
|
+
function formatDoctorReport(report) {
|
|
143
|
+
const glyph = { ok: "✓", warn: "!", fail: "✗" };
|
|
144
|
+
const lines = ["cw doctor"];
|
|
145
|
+
for (const check of report.checks) {
|
|
146
|
+
lines.push(` ${glyph[check.status]} ${check.name}: ${check.detail}`);
|
|
147
|
+
if (check.fix && check.status !== "ok")
|
|
148
|
+
lines.push(` fix: ${check.fix}`);
|
|
149
|
+
}
|
|
150
|
+
lines.push("");
|
|
151
|
+
lines.push(`${report.ok ? "✓" : "✗"} ${report.summary}`);
|
|
152
|
+
return lines.join("\n");
|
|
153
|
+
}
|
package/dist/mcp-server.js
CHANGED
|
@@ -17,6 +17,11 @@ const runner = new orchestrator_1.CoolWorkflowRunner({
|
|
|
17
17
|
});
|
|
18
18
|
process.stdin.setEncoding("utf8");
|
|
19
19
|
let buffer = "";
|
|
20
|
+
// A single line-delimited JSON-RPC request is bounded; an un-terminated stream
|
|
21
|
+
// (a peer that never sends a newline, or a slow byte dribble) must not grow the
|
|
22
|
+
// long-lived server's buffer without limit until it OOMs. Cap the unconsumed
|
|
23
|
+
// buffer and fail closed on a frame that exceeds it.
|
|
24
|
+
const MAX_LINE_BYTES = 16 * 1024 * 1024;
|
|
20
25
|
process.stdin.on("data", (chunk) => {
|
|
21
26
|
buffer += chunk;
|
|
22
27
|
let newlineIndex;
|
|
@@ -26,6 +31,12 @@ process.stdin.on("data", (chunk) => {
|
|
|
26
31
|
if (line)
|
|
27
32
|
handleLine(line);
|
|
28
33
|
}
|
|
34
|
+
if (buffer.length > MAX_LINE_BYTES) {
|
|
35
|
+
// No newline in an over-cap buffer ⇒ a malformed or hostile frame. Reject it
|
|
36
|
+
// and drop the partial bytes rather than accumulate toward OOM.
|
|
37
|
+
buffer = "";
|
|
38
|
+
sendError(null, -32700, `Parse error: request line exceeds ${MAX_LINE_BYTES} bytes`);
|
|
39
|
+
}
|
|
29
40
|
});
|
|
30
41
|
function handleLine(line) {
|
|
31
42
|
let message;
|
package/dist/orchestrator.js
CHANGED
|
@@ -821,6 +821,7 @@ function formatHelp() {
|
|
|
821
821
|
"",
|
|
822
822
|
"Commands:",
|
|
823
823
|
" list",
|
|
824
|
+
" doctor [--json] (check your setup: Node, agent backend, git, writable state)",
|
|
824
825
|
" init <workflow-id> [--title TEXT] [--output PATH]",
|
|
825
826
|
" quickstart [app-id] [--repo PATH] [--question TEXT] [--agent-command CMD] [--once] [--preview]",
|
|
826
827
|
" plan <workflow-id> [--repo PATH] [--question TEXT] [--invariant TEXT]",
|
|
@@ -873,6 +874,18 @@ function formatHelp() {
|
|
|
873
874
|
" run search|list|show|resume|archive|rerun|export|import|verify-import [run-id|archive] [--scope repo|home] [--json]",
|
|
874
875
|
" queue add|list|drain|show [queue-id] [--repo PATH] [--priority N]",
|
|
875
876
|
" history [--scope repo|home] [--app ID] [--status STATE] [--json]",
|
|
877
|
+
" audit-run <app-id> [--repo PATH] [--question TEXT] [--agent-command CMD]",
|
|
878
|
+
" metrics show|summary <run-id> [--scope repo|home] [--json]",
|
|
879
|
+
" telemetry verify <run-id> [--pubkey PEM|PATH] [--json]",
|
|
880
|
+
" gc plan|run|verify [run-id] [--json]",
|
|
881
|
+
" sched plan|lease|release|complete|reclaim|reset|policy [--json]",
|
|
882
|
+
" migration list|check|prove [target] [--json]",
|
|
883
|
+
" operator status|report <run-id> [--json]",
|
|
884
|
+
" review status|policy <run-id> [--json]",
|
|
885
|
+
" approve|reject|comment <kind> <run-id> <target-id> [--reason TEXT]",
|
|
886
|
+
" handoff <kind> <run-id> <target-id> [--to ROLE]",
|
|
887
|
+
" loop --prompt TEXT [--interval-minutes N]",
|
|
888
|
+
" demo tamper",
|
|
876
889
|
" workbench view <run-id> [--json]",
|
|
877
890
|
" workbench serve [--port N] [--scope repo|home] [--once|--json]",
|
|
878
891
|
""
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.sha256OfString = sha256OfString;
|
|
7
|
+
exports.sha256OfFile = sha256OfFile;
|
|
8
|
+
exports.dirBytes = dirBytes;
|
|
9
|
+
exports.contentDigest = contentDigest;
|
|
10
|
+
// Content addressing + byte measurement for run reclamation (NO `du` — in-process
|
|
11
|
+
// only). Carved out of reclamation.ts (FreeBSD-audit god-module carve) so the pure
|
|
12
|
+
// content-addressing leaf no longer sits inside the write-ahead reclamation
|
|
13
|
+
// transaction. These are pure functions of their path/string inputs — no run
|
|
14
|
+
// state, no module-level mutable state.
|
|
15
|
+
//
|
|
16
|
+
// BEHAVIOR-PRESERVING — pure code movement, zero logic change. reclamation.ts
|
|
17
|
+
// re-exports the public symbols (sha256OfString/sha256OfFile/dirBytes) so the
|
|
18
|
+
// module's surface stays byte-identical.
|
|
19
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
20
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
21
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
22
|
+
const compare_1 = require("../compare");
|
|
23
|
+
function sha256Hex(value) {
|
|
24
|
+
return node_crypto_1.default.createHash("sha256").update(value).digest("hex");
|
|
25
|
+
}
|
|
26
|
+
function sha256OfString(value) {
|
|
27
|
+
return `sha256:${sha256Hex(value)}`;
|
|
28
|
+
}
|
|
29
|
+
function sha256OfFile(file) {
|
|
30
|
+
return `sha256:${sha256Hex(node_fs_1.default.readFileSync(file))}`;
|
|
31
|
+
}
|
|
32
|
+
/** Walk a path and sum file sizes IN-PROCESS (no `du`). Returns 0 if absent. A
|
|
33
|
+
* file returns its own size; a dir returns the recursive sum. */
|
|
34
|
+
function dirBytes(p) {
|
|
35
|
+
let total = 0;
|
|
36
|
+
let stat;
|
|
37
|
+
try {
|
|
38
|
+
stat = node_fs_1.default.statSync(p);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
if (stat.isFile())
|
|
44
|
+
return stat.size;
|
|
45
|
+
if (!stat.isDirectory())
|
|
46
|
+
return 0;
|
|
47
|
+
for (const entry of node_fs_1.default.readdirSync(p, { withFileTypes: true })) {
|
|
48
|
+
total += dirBytes(node_path_1.default.join(p, entry.name));
|
|
49
|
+
}
|
|
50
|
+
return total;
|
|
51
|
+
}
|
|
52
|
+
/** Stable content digest of a path (file = its bytes; dir = digest over each
|
|
53
|
+
* member's relative path + bytes, sorted). Lets the freed-manifest record a
|
|
54
|
+
* single sha per freed dir. */
|
|
55
|
+
function contentDigest(p) {
|
|
56
|
+
const stat = node_fs_1.default.statSync(p);
|
|
57
|
+
if (stat.isFile())
|
|
58
|
+
return sha256OfFile(p);
|
|
59
|
+
const parts = [];
|
|
60
|
+
const walk = (dir, rel) => {
|
|
61
|
+
for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true }).sort((a, b) => (0, compare_1.compareBytes)(a.name, b.name))) {
|
|
62
|
+
const abs = node_path_1.default.join(dir, entry.name);
|
|
63
|
+
const r = node_path_1.default.join(rel, entry.name);
|
|
64
|
+
if (entry.isDirectory())
|
|
65
|
+
walk(abs, r);
|
|
66
|
+
else
|
|
67
|
+
parts.push(`${r}:${sha256OfFile(abs)}`);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
walk(p, "");
|
|
71
|
+
return sha256OfString(parts.join("\n"));
|
|
72
|
+
}
|
package/dist/reclamation.js
CHANGED
|
@@ -29,10 +29,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
29
29
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
30
30
|
};
|
|
31
31
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
-
exports.ReclamationError = exports.ReclamationAbort = exports.SKELETON_REQUIRED_KEYS = void 0;
|
|
33
|
-
exports.sha256OfString = sha256OfString;
|
|
34
|
-
exports.sha256OfFile = sha256OfFile;
|
|
35
|
-
exports.dirBytes = dirBytes;
|
|
32
|
+
exports.dirBytes = exports.sha256OfFile = exports.sha256OfString = exports.ReclamationError = exports.ReclamationAbort = exports.SKELETON_REQUIRED_KEYS = void 0;
|
|
36
33
|
exports.reclaimedLogPath = reclaimedLogPath;
|
|
37
34
|
exports.loadReclamationLog = loadReclamationLog;
|
|
38
35
|
exports.extractSkeleton = extractSkeleton;
|
|
@@ -49,9 +46,12 @@ exports.runReclamation = runReclamation;
|
|
|
49
46
|
exports.reconstructArtifact = reconstructArtifact;
|
|
50
47
|
exports.verifyReclamation = verifyReclamation;
|
|
51
48
|
exports.dominantFailureCode = dominantFailureCode;
|
|
52
|
-
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
53
49
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
54
50
|
const node_path_1 = __importDefault(require("node:path"));
|
|
51
|
+
const hash_1 = require("./reclamation/hash");
|
|
52
|
+
Object.defineProperty(exports, "dirBytes", { enumerable: true, get: function () { return hash_1.dirBytes; } });
|
|
53
|
+
Object.defineProperty(exports, "sha256OfFile", { enumerable: true, get: function () { return hash_1.sha256OfFile; } });
|
|
54
|
+
Object.defineProperty(exports, "sha256OfString", { enumerable: true, get: function () { return hash_1.sha256OfString; } });
|
|
55
55
|
const multi_agent_eval_1 = require("./multi-agent-eval");
|
|
56
56
|
const node_projection_1 = require("./node-projection");
|
|
57
57
|
const node_snapshot_1 = require("./node-snapshot");
|
|
@@ -95,59 +95,6 @@ class ReclamationError extends Error {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
exports.ReclamationError = ReclamationError;
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
// Content addressing + byte measurement (NO `du` — in-process only).
|
|
100
|
-
// ---------------------------------------------------------------------------
|
|
101
|
-
function sha256Hex(value) {
|
|
102
|
-
return node_crypto_1.default.createHash("sha256").update(value).digest("hex");
|
|
103
|
-
}
|
|
104
|
-
function sha256OfString(value) {
|
|
105
|
-
return `sha256:${sha256Hex(value)}`;
|
|
106
|
-
}
|
|
107
|
-
function sha256OfFile(file) {
|
|
108
|
-
return `sha256:${sha256Hex(node_fs_1.default.readFileSync(file))}`;
|
|
109
|
-
}
|
|
110
|
-
/** Walk a path and sum file sizes IN-PROCESS (no `du`). Returns 0 if absent. A
|
|
111
|
-
* file returns its own size; a dir returns the recursive sum. */
|
|
112
|
-
function dirBytes(p) {
|
|
113
|
-
let total = 0;
|
|
114
|
-
let stat;
|
|
115
|
-
try {
|
|
116
|
-
stat = node_fs_1.default.statSync(p);
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
return 0;
|
|
120
|
-
}
|
|
121
|
-
if (stat.isFile())
|
|
122
|
-
return stat.size;
|
|
123
|
-
if (!stat.isDirectory())
|
|
124
|
-
return 0;
|
|
125
|
-
for (const entry of node_fs_1.default.readdirSync(p, { withFileTypes: true })) {
|
|
126
|
-
total += dirBytes(node_path_1.default.join(p, entry.name));
|
|
127
|
-
}
|
|
128
|
-
return total;
|
|
129
|
-
}
|
|
130
|
-
/** Stable content digest of a path (file = its bytes; dir = digest over each
|
|
131
|
-
* member's relative path + bytes, sorted). Lets the freed-manifest record a
|
|
132
|
-
* single sha per freed dir. */
|
|
133
|
-
function contentDigest(p) {
|
|
134
|
-
const stat = node_fs_1.default.statSync(p);
|
|
135
|
-
if (stat.isFile())
|
|
136
|
-
return sha256OfFile(p);
|
|
137
|
-
const parts = [];
|
|
138
|
-
const walk = (dir, rel) => {
|
|
139
|
-
for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true }).sort((a, b) => (0, compare_1.compareBytes)(a.name, b.name))) {
|
|
140
|
-
const abs = node_path_1.default.join(dir, entry.name);
|
|
141
|
-
const r = node_path_1.default.join(rel, entry.name);
|
|
142
|
-
if (entry.isDirectory())
|
|
143
|
-
walk(abs, r);
|
|
144
|
-
else
|
|
145
|
-
parts.push(`${r}:${sha256OfFile(abs)}`);
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
walk(p, "");
|
|
149
|
-
return sha256OfString(parts.join("\n"));
|
|
150
|
-
}
|
|
151
98
|
/** Persist a run's authoritative state.json DURABLY (atomic temp → fsync →
|
|
152
99
|
* rename). The re-point that scratch reclamation depends on MUST be persisted
|
|
153
100
|
* this way BEFORE any byte is freed — see prepareFree(). */
|
|
@@ -220,13 +167,13 @@ function digestEvidenceEntry(entry) {
|
|
|
220
167
|
try {
|
|
221
168
|
const stat = node_fs_1.default.statSync(candidatePath);
|
|
222
169
|
if (stat.isFile())
|
|
223
|
-
return { ref, digest: sha256OfFile(candidatePath) };
|
|
170
|
+
return { ref, digest: (0, hash_1.sha256OfFile)(candidatePath) };
|
|
224
171
|
}
|
|
225
172
|
catch {
|
|
226
173
|
/* fall through to locator digest */
|
|
227
174
|
}
|
|
228
175
|
}
|
|
229
|
-
return { ref, digest: sha256OfString(ref) };
|
|
176
|
+
return { ref, digest: (0, hash_1.sha256OfString)(ref) };
|
|
230
177
|
}
|
|
231
178
|
/** STEP 1: extract + seal the skeleton. Pure read over the run; never mutates. */
|
|
232
179
|
function extractSkeleton(run) {
|
|
@@ -264,7 +211,7 @@ function extractSkeleton(run) {
|
|
|
264
211
|
.map(([ref, digest]) => ({ ref, digest }))
|
|
265
212
|
.sort((a, b) => (0, compare_1.compareBytes)(a.ref, b.ref));
|
|
266
213
|
const eventLog = auditEventLogPath(run);
|
|
267
|
-
const auditLogDigest = node_fs_1.default.existsSync(eventLog) ? sha256OfFile(eventLog) : sha256OfString("");
|
|
214
|
+
const auditLogDigest = node_fs_1.default.existsSync(eventLog) ? (0, hash_1.sha256OfFile)(eventLog) : (0, hash_1.sha256OfString)("");
|
|
268
215
|
const events = node_fs_1.default.existsSync(eventLog)
|
|
269
216
|
? node_fs_1.default
|
|
270
217
|
.readFileSync(eventLog, "utf8")
|
|
@@ -284,18 +231,18 @@ function extractSkeleton(run) {
|
|
|
284
231
|
const metricsReport = node_path_1.default.join(run.paths.runDir, "metrics", "metrics-report.json");
|
|
285
232
|
const costRecord = {
|
|
286
233
|
tasks: (run.tasks || []).map((task) => ({ taskId: task.id, model: task.usage?.model, source: task.usage?.source })),
|
|
287
|
-
metricsDigest: node_fs_1.default.existsSync(metricsReport) ? sha256OfFile(metricsReport) : undefined
|
|
234
|
+
metricsDigest: node_fs_1.default.existsSync(metricsReport) ? (0, hash_1.sha256OfFile)(metricsReport) : undefined
|
|
288
235
|
};
|
|
289
236
|
const collaboration = run.collaboration;
|
|
290
237
|
const collaborationLog = {
|
|
291
|
-
digest: sha256OfString((0, multi_agent_eval_1.replayStableStringify)(collaboration || {})),
|
|
238
|
+
digest: (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)(collaboration || {})),
|
|
292
239
|
approvals: collaboration?.approvals?.length || 0,
|
|
293
240
|
comments: collaboration?.comments?.length || 0,
|
|
294
241
|
handoffs: collaboration?.handoffs?.length || 0
|
|
295
242
|
};
|
|
296
243
|
// Empty (not a hash-of-empty) when state.json is absent, so the skeleton fails
|
|
297
244
|
// closed — you cannot seal a run whose authoritative state is gone.
|
|
298
|
-
const stateDigest = node_fs_1.default.existsSync(run.paths.state) ? sha256OfFile(run.paths.state) : "";
|
|
245
|
+
const stateDigest = node_fs_1.default.existsSync(run.paths.state) ? (0, hash_1.sha256OfFile)(run.paths.state) : "";
|
|
299
246
|
return {
|
|
300
247
|
schemaVersion: 1,
|
|
301
248
|
runId: run.id,
|
|
@@ -402,13 +349,13 @@ function buildReferenceGraph(run) {
|
|
|
402
349
|
* node-projection field set (node-projection.ts) so reconstruction matches
|
|
403
350
|
* node-snapshot.ts's body byte-for-byte — the projection can no longer drift. */
|
|
404
351
|
function snapshotProjectionDigest(node) {
|
|
405
|
-
return sha256OfString((0, node_projection_1.nodeProjectionDigestInput)(node));
|
|
352
|
+
return (0, hash_1.sha256OfString)((0, node_projection_1.nodeProjectionDigestInput)(node));
|
|
406
353
|
}
|
|
407
354
|
/** Body digest of the RETAINED node (lives in state.json). The reconstruction
|
|
408
355
|
* verifier re-derives the projection from this retained input. Same shared field
|
|
409
356
|
* set / canonical bytes as snapshotProjectionDigest. */
|
|
410
357
|
function nodeBodyDigest(node) {
|
|
411
|
-
return sha256OfString((0, node_projection_1.nodeProjectionDigestInput)(node));
|
|
358
|
+
return (0, hash_1.sha256OfString)((0, node_projection_1.nodeProjectionDigestInput)(node));
|
|
412
359
|
}
|
|
413
360
|
/** Build the retention plan: which paths are freeable under `policy`, of what
|
|
414
361
|
* kind, how many bytes, and the resulting capability downgrade. */
|
|
@@ -431,7 +378,7 @@ function planReclamation(run, policy = {}) {
|
|
|
431
378
|
const resultsCopy = task?.resultPath;
|
|
432
379
|
if (!resultNodeId || !resultsCopy || !node_fs_1.default.existsSync(resultsCopy))
|
|
433
380
|
continue;
|
|
434
|
-
const bytes = dirBytes(workerDir);
|
|
381
|
+
const bytes = (0, hash_1.dirBytes)(workerDir);
|
|
435
382
|
if (bytes <= 0)
|
|
436
383
|
continue;
|
|
437
384
|
freeable.push({
|
|
@@ -478,14 +425,14 @@ function planReclamation(run, policy = {}) {
|
|
|
478
425
|
continue; // source node gone → cannot reconstruct → retain
|
|
479
426
|
if (repointNodeIds.has(node.id))
|
|
480
427
|
continue; // body will be re-pointed → retain
|
|
481
|
-
const bytes = dirBytes(snapFile);
|
|
428
|
+
const bytes = (0, hash_1.dirBytes)(snapFile);
|
|
482
429
|
if (bytes <= 0)
|
|
483
430
|
continue;
|
|
484
431
|
const inputDigest = nodeBodyDigest(node);
|
|
485
432
|
const recipe = {
|
|
486
433
|
recipeKind: "node-snapshot-projection",
|
|
487
434
|
inputDigests: [inputDigest],
|
|
488
|
-
inputsDigest: sha256OfString((0, multi_agent_eval_1.replayStableStringify)([inputDigest])),
|
|
435
|
+
inputsDigest: (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)([inputDigest])),
|
|
489
436
|
expectDigest: snapshotProjectionDigest(node),
|
|
490
437
|
sourceRef: node.id
|
|
491
438
|
};
|
|
@@ -534,11 +481,11 @@ function planReclamation(run, policy = {}) {
|
|
|
534
481
|
return { freeable, bytesToFree, byKind, capability, capabilityReason };
|
|
535
482
|
}
|
|
536
483
|
function policyDigestOf(policy) {
|
|
537
|
-
return sha256OfString((0, multi_agent_eval_1.replayStableStringify)(policy));
|
|
484
|
+
return (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)(policy));
|
|
538
485
|
}
|
|
539
486
|
/** genesis prevTombstoneHash = sha256 of the sealed skeleton. */
|
|
540
487
|
function genesisPrevHash(skeleton) {
|
|
541
|
-
return sha256OfString((0, multi_agent_eval_1.replayStableStringify)(skeleton));
|
|
488
|
+
return (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)(skeleton));
|
|
542
489
|
}
|
|
543
490
|
/** The canonical bytes a tombstoneHash binds: freed-manifest + sealed skeleton +
|
|
544
491
|
* prevTombstoneHash + capability. Recomputed independently by `gc verify`. */
|
|
@@ -551,14 +498,14 @@ function tombstoneHashInput(t) {
|
|
|
551
498
|
policyDigest: t.policyDigest,
|
|
552
499
|
freed: t.freed.map((f) => ({ path: f.path, kind: f.kind, bytes: f.bytes, sha256: f.sha256, recipe: f.recipe || null })),
|
|
553
500
|
bytesFreed: t.bytesFreed,
|
|
554
|
-
skeletonDigest: sha256OfString((0, multi_agent_eval_1.replayStableStringify)(t.skeleton)),
|
|
501
|
+
skeletonDigest: (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)(t.skeleton)),
|
|
555
502
|
capability: t.capability,
|
|
556
503
|
capabilityReason: t.capabilityReason,
|
|
557
504
|
prevTombstoneHash: t.prevTombstoneHash
|
|
558
505
|
});
|
|
559
506
|
}
|
|
560
507
|
function computeTombstoneHash(t) {
|
|
561
|
-
return sha256OfString(tombstoneHashInput(t));
|
|
508
|
+
return (0, hash_1.sha256OfString)(tombstoneHashInput(t));
|
|
562
509
|
}
|
|
563
510
|
function tombstoneId(seq) {
|
|
564
511
|
// Deterministic (FreeBSD-audit L13): the chain POSITION, not a process-global
|
|
@@ -576,7 +523,7 @@ function buildTombstone(run, skeleton, plan, options = {}) {
|
|
|
576
523
|
path: entry.path,
|
|
577
524
|
kind: entry.kind,
|
|
578
525
|
bytes: entry.bytes,
|
|
579
|
-
sha256: contentDigest(entry.absPath),
|
|
526
|
+
sha256: (0, hash_1.contentDigest)(entry.absPath),
|
|
580
527
|
recipe: entry.recipe
|
|
581
528
|
}));
|
|
582
529
|
const base = {
|
|
@@ -685,7 +632,7 @@ function freeBulk(run, tombstone) {
|
|
|
685
632
|
let freedBytes = 0;
|
|
686
633
|
for (const entry of tombstone.freed) {
|
|
687
634
|
const abs = node_path_1.default.join(runDir, entry.path);
|
|
688
|
-
const before = dirBytes(abs);
|
|
635
|
+
const before = (0, hash_1.dirBytes)(abs);
|
|
689
636
|
node_fs_1.default.rmSync(abs, { recursive: true, force: true });
|
|
690
637
|
freedBytes += before;
|
|
691
638
|
}
|
|
@@ -774,15 +721,15 @@ function reconstructArtifact(run, recipe) {
|
|
|
774
721
|
if (recipe.recipeKind === "node-snapshot-projection") {
|
|
775
722
|
const node = (run.nodes || []).find((n) => n.id === recipe.sourceRef);
|
|
776
723
|
if (!node) {
|
|
777
|
-
return { inputsDigest: sha256OfString("absent"), expectDigest: sha256OfString("absent") };
|
|
724
|
+
return { inputsDigest: (0, hash_1.sha256OfString)("absent"), expectDigest: (0, hash_1.sha256OfString)("absent") };
|
|
778
725
|
}
|
|
779
726
|
const inputDigest = nodeBodyDigest(node);
|
|
780
|
-
const inputsDigest = sha256OfString((0, multi_agent_eval_1.replayStableStringify)([inputDigest]));
|
|
727
|
+
const inputsDigest = (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)([inputDigest]));
|
|
781
728
|
const expectDigest = snapshotProjectionDigest(node);
|
|
782
729
|
return { inputsDigest, expectDigest };
|
|
783
730
|
}
|
|
784
731
|
// Unknown recipe kind → fail closed (digest can't match expectDigest).
|
|
785
|
-
return { inputsDigest: sha256OfString("unknown-recipe"), expectDigest: sha256OfString("unknown-recipe") };
|
|
732
|
+
return { inputsDigest: (0, hash_1.sha256OfString)("unknown-recipe"), expectDigest: (0, hash_1.sha256OfString)("unknown-recipe") };
|
|
786
733
|
}
|
|
787
734
|
/** Re-prove the whole reclamation chain for a run: skeleton schema-complete,
|
|
788
735
|
* tombstoneHash/prevTombstoneHash chain recomputed-and-untampered, and each
|