cool-workflow 0.1.81 → 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 +125 -121
- 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/candidate-scoring.js +20 -26
- package/dist/capability-core.js +80 -93
- package/dist/capability-registry.js +30 -3
- package/dist/cli.js +12 -1
- package/dist/commit.js +217 -204
- package/dist/coordinator/util.js +6 -9
- package/dist/dispatch.js +11 -3
- package/dist/doctor.js +153 -0
- package/dist/evidence-reasoning.js +4 -1
- package/dist/execution-backend/agent.js +11 -48
- package/dist/execution-backend.js +11 -31
- package/dist/gates.js +48 -0
- package/dist/mcp-server.js +11 -0
- package/dist/multi-agent/helpers.js +6 -10
- package/dist/multi-agent/ids.js +20 -0
- package/dist/multi-agent-eval.js +27 -1
- package/dist/multi-agent-host.js +53 -21
- package/dist/multi-agent-operator-ux.js +2 -1
- package/dist/multi-agent-trust.js +5 -5
- package/dist/node-projection.js +59 -0
- package/dist/node-snapshot.js +8 -18
- package/dist/orchestrator/lifecycle-operations.js +22 -1
- package/dist/orchestrator.js +29 -2
- package/dist/reclamation/hash.js +72 -0
- package/dist/reclamation.js +31 -112
- package/dist/run-registry/queue.js +6 -7
- package/dist/run-registry.js +35 -24
- package/dist/scheduler.js +112 -57
- package/dist/topology.js +25 -4
- package/dist/trust-audit.js +70 -38
- package/dist/validation.js +328 -0
- package/dist/version.js +1 -1
- package/dist/worker-isolation.js +163 -58
- package/docs/agent-delegation-drive.7.md +90 -85
- 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 +227 -154
- package/docs/contract-migration-tooling.7.md +49 -41
- package/docs/control-plane-scheduling.7.md +45 -40
- 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 +35 -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 +63 -58
- package/docs/execution-backends.7.md +84 -79
- 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 +83 -77
- package/docs/multi-agent-eval-replay-harness.7.md +59 -54
- package/docs/multi-agent-operator-ux.7.md +70 -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 -21
- package/docs/observability-cost-accounting.7.md +50 -45
- package/docs/operator-ux.7.md +30 -30
- package/docs/pipeline-runner.7.md +31 -31
- package/docs/project-index.md +15 -6
- package/docs/real-execution-backends.7.md +47 -42
- package/docs/release-and-migration.7.md +43 -38
- package/docs/release-tooling.7.md +74 -39
- package/docs/routines.md +16 -16
- package/docs/run-registry-control-plane.7.md +120 -115
- package/docs/run-retention-reclamation.7.md +46 -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 +64 -59
- package/docs/state-node.7.md +8 -8
- package/docs/team-collaboration.7.md +63 -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 +70 -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/manifest/source-context-profiles.json +1 -1
- package/package.json +3 -2
- package/scripts/canonical-apps.js +4 -4
- package/scripts/children/batch-delegate-child.js +58 -0
- package/scripts/children/http-delegate-child.js +39 -0
- package/scripts/dogfood-release.js +1 -1
- package/scripts/gen-parity-doc.js +106 -0
- package/scripts/golden-path.js +4 -4
- package/scripts/release-flow.js +181 -5
- package/dist/verifier-registry.js +0 -46
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
|
+
}
|
|
@@ -19,6 +19,7 @@ const state_1 = require("./state");
|
|
|
19
19
|
const multi_agent_operator_ux_1 = require("./multi-agent-operator-ux");
|
|
20
20
|
const trust_audit_1 = require("./trust-audit");
|
|
21
21
|
const multi_agent_trust_1 = require("./multi-agent-trust");
|
|
22
|
+
const validation_1 = require("./validation");
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
23
24
|
// Evidence Adoption Reasoning Chain (v0.1.26)
|
|
24
25
|
//
|
|
@@ -616,7 +617,9 @@ function readAllScores(run) {
|
|
|
616
617
|
continue;
|
|
617
618
|
for (const file of node_fs_1.default.readdirSync(dir).filter((entry) => entry.endsWith(".json")).sort()) {
|
|
618
619
|
try {
|
|
619
|
-
const score = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(dir, file), "utf8"));
|
|
620
|
+
const score = (0, validation_1.tryValidateCandidateScore)(JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(dir, file), "utf8")));
|
|
621
|
+
if (!score)
|
|
622
|
+
continue; // Malformed/forged score shape: skip; the score gate fails closed.
|
|
620
623
|
scores.set(score.id, score);
|
|
621
624
|
}
|
|
622
625
|
catch {
|
|
@@ -255,53 +255,16 @@ function prepareAgentSpawn(request) {
|
|
|
255
255
|
timeoutMs: resolved.timeoutMs || 600000
|
|
256
256
|
};
|
|
257
257
|
}
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
process.stdin.on("end", () => {
|
|
269
|
-
const jobs = JSON.parse(raw);
|
|
270
|
-
if (!jobs.length) { process.stdout.write("[]"); return; }
|
|
271
|
-
const out = new Array(jobs.length);
|
|
272
|
-
let pending = jobs.length;
|
|
273
|
-
const CAP = 32 * 1024 * 1024;
|
|
274
|
-
jobs.forEach((job, i) => {
|
|
275
|
-
let stdout = "";
|
|
276
|
-
let settled = false;
|
|
277
|
-
const settle = (o) => {
|
|
278
|
-
if (settled) return;
|
|
279
|
-
settled = true;
|
|
280
|
-
out[i] = o;
|
|
281
|
-
if (--pending === 0) process.stdout.write(JSON.stringify(out));
|
|
282
|
-
};
|
|
283
|
-
let child;
|
|
284
|
-
try {
|
|
285
|
-
child = spawn(job.binary, job.args, { cwd: job.cwd, env: process.env, shell: false });
|
|
286
|
-
} catch (error) {
|
|
287
|
-
settle({ spawnError: String((error && error.message) || error), exitCode: null, stdout: "" });
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
const term = setTimeout(() => { try { child.kill("SIGTERM"); } catch {} }, job.timeoutMs);
|
|
291
|
-
const kill = setTimeout(() => { try { child.kill("SIGKILL"); } catch {} }, job.timeoutMs + 5000);
|
|
292
|
-
child.stdout.on("data", (d) => { if (stdout.length < CAP) stdout += d; });
|
|
293
|
-
child.stderr.on("data", () => {});
|
|
294
|
-
child.on("error", (error) => {
|
|
295
|
-
clearTimeout(term); clearTimeout(kill);
|
|
296
|
-
settle({ spawnError: String((error && error.message) || error), exitCode: null, stdout });
|
|
297
|
-
});
|
|
298
|
-
child.on("close", (code) => {
|
|
299
|
-
clearTimeout(term); clearTimeout(kill);
|
|
300
|
-
settle({ exitCode: typeof code === "number" ? code : null, stdout });
|
|
301
|
-
});
|
|
302
|
-
});
|
|
303
|
-
});
|
|
304
|
-
`;
|
|
258
|
+
// The batch delegate child is a real, packaged Node script (not an embedded
|
|
259
|
+
// `node -e` string — F11). It reads jobs JSON on stdin, spawns ALL concurrently
|
|
260
|
+
// (shell:false, inherited env — the agent's own credentials resolve; CW never
|
|
261
|
+
// reads them), per-job SIGTERM at timeoutMs + SIGKILL at +5s, caps each captured
|
|
262
|
+
// stdout at 32MB, and prints the outcome array when every job has settled. stderr
|
|
263
|
+
// is drained (a full pipe must never wedge a child). A kill yields exitCode null —
|
|
264
|
+
// the no-exit-code refusal. We spawn it BY PATH (shell:false); the path is
|
|
265
|
+
// resolved from this compiled module (dist/execution-backend/agent.js) up to the
|
|
266
|
+
// package's `scripts/children/` dir, which package.json ships in "files".
|
|
267
|
+
const BATCH_DELEGATE_CHILD_SCRIPT = node_path_1.default.resolve(__dirname, "..", "..", "scripts", "children", "batch-delegate-child.js");
|
|
305
268
|
/** Run a batch of agent spawns concurrently; outcomes index-align with jobs. The
|
|
306
269
|
* parent backstop timeout (max job timeout + 30s) means even a wedged delegate
|
|
307
270
|
* child cannot deadlock the drive: on any batch-level failure EVERY job settles
|
|
@@ -310,7 +273,7 @@ function runAgentBatchOutcomes(jobs) {
|
|
|
310
273
|
if (!jobs.length)
|
|
311
274
|
return [];
|
|
312
275
|
const maxTimeout = Math.max(...jobs.map((job) => job.timeoutMs));
|
|
313
|
-
const child = (0, node_child_process_1.spawnSync)(process.execPath, [
|
|
276
|
+
const child = (0, node_child_process_1.spawnSync)(process.execPath, [BATCH_DELEGATE_CHILD_SCRIPT], {
|
|
314
277
|
input: JSON.stringify(jobs),
|
|
315
278
|
encoding: "utf8",
|
|
316
279
|
maxBuffer: 33 * 1024 * 1024 * jobs.length,
|
|
@@ -47,6 +47,7 @@ exports.backendProbePayload = backendProbePayload;
|
|
|
47
47
|
exports.buildChildEnv = buildChildEnv;
|
|
48
48
|
exports.clearProbeCache = clearProbeCache;
|
|
49
49
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
50
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
50
51
|
const node_child_process_1 = require("node:child_process");
|
|
51
52
|
// Pure leaf helpers — carved into ./execution-backend/util.ts (god-module carve).
|
|
52
53
|
// `sha256` is re-exported below to keep the public surface byte-identical.
|
|
@@ -578,35 +579,14 @@ function runContainer(descriptor, policy, request, label, handle, attestation) {
|
|
|
578
579
|
}
|
|
579
580
|
return delegatedEnvelope(descriptor, label, handle, attestation, command, args, exitCode, String(result.stdout || ""));
|
|
580
581
|
}
|
|
581
|
-
//
|
|
582
|
-
// JSON job on stdin, POSTs it to the endpoint,
|
|
583
|
-
// and prints `{ exitCode, stdout }` (or
|
|
584
|
-
// fetch, node >=18), so the driver stays
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const job = JSON.parse((await read()) || "{}");
|
|
590
|
-
const endpoint = process.env.CW_DELEGATE_ENDPOINT;
|
|
591
|
-
if (!endpoint) throw new Error("no endpoint");
|
|
592
|
-
const post = await fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(job) });
|
|
593
|
-
if (!post.ok) throw new Error("runner responded " + post.status);
|
|
594
|
-
let data = await post.json();
|
|
595
|
-
// Poll a returned jobId until the runner reports done.
|
|
596
|
-
let guard = 0;
|
|
597
|
-
while (data && data.jobId && data.done !== true && guard++ < 600) {
|
|
598
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
599
|
-
const poll = await fetch(endpoint + (endpoint.includes("?") ? "&" : "?") + "jobId=" + encodeURIComponent(data.jobId));
|
|
600
|
-
if (!poll.ok) throw new Error("poll responded " + poll.status);
|
|
601
|
-
data = await poll.json();
|
|
602
|
-
}
|
|
603
|
-
if (typeof data.exitCode !== "number") throw new Error("runner did not report an exitCode");
|
|
604
|
-
process.stdout.write(JSON.stringify({ exitCode: data.exitCode, stdout: String(data.stdout || "") }));
|
|
605
|
-
} catch (e) {
|
|
606
|
-
process.stdout.write(JSON.stringify({ error: e && e.message ? e.message : String(e) }));
|
|
607
|
-
}
|
|
608
|
-
})();
|
|
609
|
-
`;
|
|
582
|
+
// The remote/CI delegation child is a real, packaged Node script (not an embedded
|
|
583
|
+
// `node -e` string — F11): it reads a JSON job on stdin, POSTs it to the endpoint,
|
|
584
|
+
// optionally polls a returned jobId, and prints `{ exitCode, stdout }` (or
|
|
585
|
+
// `{ error }`) on stdout. Node-only (global fetch, node >=18), so the driver stays
|
|
586
|
+
// portable and synchronous from CW's view. We spawn it BY PATH (shell:false). The
|
|
587
|
+
// path is resolved from this compiled module (dist/execution-backend.js) up to the
|
|
588
|
+
// package's `scripts/children/` dir, which package.json ships in "files".
|
|
589
|
+
const HTTP_DELEGATE_CHILD_SCRIPT = node_path_1.default.resolve(__dirname, "..", "scripts", "children", "http-delegate-child.js");
|
|
610
590
|
/** remote / ci — real HTTP delegation. POSTs the job to the configured endpoint
|
|
611
591
|
* (and polls a returned jobId) via a Node child, then records the runner's exit +
|
|
612
592
|
* stdout digest as canonical evidence. Fails closed when the endpoint is missing,
|
|
@@ -628,7 +608,7 @@ function runHttpDelegation(descriptor, policy, request, label, handle, attestati
|
|
|
628
608
|
sandboxProfileId: policy.id,
|
|
629
609
|
jobId: handle.jobId
|
|
630
610
|
});
|
|
631
|
-
const child = (0, node_child_process_1.spawnSync)(process.execPath, [
|
|
611
|
+
const child = (0, node_child_process_1.spawnSync)(process.execPath, [HTTP_DELEGATE_CHILD_SCRIPT], {
|
|
632
612
|
input: job,
|
|
633
613
|
env: { ...process.env, CW_DELEGATE_ENDPOINT: endpoint },
|
|
634
614
|
encoding: "utf8",
|
|
@@ -752,7 +732,7 @@ function runAgentEndpoint(descriptor, policy, request, label, resolved, attestat
|
|
|
752
732
|
resultPath: manifest?.resultPath,
|
|
753
733
|
sandboxProfileId: policy.id
|
|
754
734
|
});
|
|
755
|
-
const child = (0, node_child_process_1.spawnSync)(process.execPath, [
|
|
735
|
+
const child = (0, node_child_process_1.spawnSync)(process.execPath, [HTTP_DELEGATE_CHILD_SCRIPT], {
|
|
756
736
|
input: job,
|
|
757
737
|
env: { ...process.env, CW_DELEGATE_ENDPOINT: endpoint },
|
|
758
738
|
encoding: "utf8",
|
package/dist/gates.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Shared, drift-proof gate primitives (F4). These two helpers were previously
|
|
3
|
+
// duplicated "in sync by comment" in commit.ts and candidate-scoring.ts — the
|
|
4
|
+
// no-false-green gate and the sandbox-profile lookup. A by-comment sync is a
|
|
5
|
+
// structural drift hazard: nothing prevents one copy from changing without the
|
|
6
|
+
// other, and the two gates MUST agree (selection + commit must reach the same
|
|
7
|
+
// empty-capture verdict). Extracting ONE implementation each, imported by both
|
|
8
|
+
// call sites, makes that drift impossible.
|
|
9
|
+
//
|
|
10
|
+
// Pure functions over PERSISTED state only — no clock, no randomness, no fs —
|
|
11
|
+
// so both selection and commit replays reach the same gate decision. Keep them
|
|
12
|
+
// fail-closed: when in doubt the caller must BLOCK, never silently pass.
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.emptyCaptureWarning = emptyCaptureWarning;
|
|
15
|
+
exports.sandboxProfileForCandidate = sandboxProfileForCandidate;
|
|
16
|
+
/** The HARD no-false-green gate (DIRECTION.md "ambiguity is a visible state").
|
|
17
|
+
* A verifier node is built FROM a result node; when that result captured no
|
|
18
|
+
* structured signal at all the result node carries a `metadata.captureWarning`
|
|
19
|
+
* marker (set in worker-isolation / lifecycle ingest via isEmptyCapture). The
|
|
20
|
+
* worker output is still ACCEPTED (a recorded warning, never a silent pass), but
|
|
21
|
+
* a verifier-GATED commit — and candidate SELECTION — must NOT be able to
|
|
22
|
+
* present that zero-evidence result as clean/green. We detect it here, reading
|
|
23
|
+
* ONLY persisted state (the source result node's metadata) — purely functional,
|
|
24
|
+
* no clock/ordering — so snapshot replay reaches the same gate decision.
|
|
25
|
+
* Returns the marker string, or undefined.
|
|
26
|
+
*
|
|
27
|
+
* Resolution trail: verifier node -> its input/parent result node. We look at
|
|
28
|
+
* `inputs.inputNodeId` (set by runPipelineStage) first, then fall back to the
|
|
29
|
+
* first parent, so it works regardless of which ingest path produced the node. */
|
|
30
|
+
function emptyCaptureWarning(run, verifierNode) {
|
|
31
|
+
const resultNodeId = (typeof verifierNode.inputs?.inputNodeId === "string" ? verifierNode.inputs.inputNodeId : undefined) ||
|
|
32
|
+
verifierNode.parents[0];
|
|
33
|
+
const resultNode = resultNodeId ? (run.nodes || []).find((node) => node.id === resultNodeId) : undefined;
|
|
34
|
+
const warning = resultNode?.metadata?.captureWarning;
|
|
35
|
+
return typeof warning === "string" && warning ? warning : undefined;
|
|
36
|
+
}
|
|
37
|
+
/** Resolve the sandbox profile that backed a candidate's acceptance: the
|
|
38
|
+
* worker's profile if the candidate has a worker, else the originating task's.
|
|
39
|
+
* Read by both the commit gate's acceptance rationale and selection. Accepts an
|
|
40
|
+
* optional candidate so the commit-side caller (which may not have resolved a
|
|
41
|
+
* candidate) can share the same lookup. Pure over persisted run state. */
|
|
42
|
+
function sandboxProfileForCandidate(run, candidate) {
|
|
43
|
+
const worker = candidate?.workerId ? (run.workers || []).find((entry) => entry.id === candidate.workerId) : undefined;
|
|
44
|
+
if (worker?.sandboxProfileId)
|
|
45
|
+
return worker.sandboxProfileId;
|
|
46
|
+
const task = candidate?.taskId ? (run.tasks || []).find((entry) => entry.id === candidate.taskId) : undefined;
|
|
47
|
+
return task?.sandboxProfileId;
|
|
48
|
+
}
|
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;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MULTI_AGENT_SCHEMA_VERSION = void 0;
|
|
3
|
+
exports.createId = exports.MULTI_AGENT_SCHEMA_VERSION = void 0;
|
|
4
4
|
exports.indexRow = indexRow;
|
|
5
5
|
exports.assertNoRecordPathCollisions = assertNoRecordPathCollisions;
|
|
6
6
|
exports.pluralKind = pluralKind;
|
|
@@ -9,7 +9,6 @@ exports.assertLifecycleTransition = assertLifecycleTransition;
|
|
|
9
9
|
exports.lifecycleEvent = lifecycleEvent;
|
|
10
10
|
exports.isMembershipReported = isMembershipReported;
|
|
11
11
|
exports.touch = touch;
|
|
12
|
-
exports.createId = createId;
|
|
13
12
|
exports.compact = compact;
|
|
14
13
|
exports.unique = unique;
|
|
15
14
|
exports.countBy = countBy;
|
|
@@ -106,14 +105,11 @@ function touch(record) {
|
|
|
106
105
|
record.updatedAt = new Date().toISOString();
|
|
107
106
|
return record;
|
|
108
107
|
}
|
|
109
|
-
// Deterministic record id
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
function createId(prefix, seq) {
|
|
115
|
-
return `${prefix}-${String(seq).padStart(4, "0")}`;
|
|
116
|
-
}
|
|
108
|
+
// Deterministic record id — single source of truth in ./ids. Re-exported here so
|
|
109
|
+
// multi-agent.ts importers stay byte-unchanged (F10 dedup: the coordinator layer
|
|
110
|
+
// shares the exact same helper).
|
|
111
|
+
var ids_1 = require("./ids");
|
|
112
|
+
Object.defineProperty(exports, "createId", { enumerable: true, get: function () { return ids_1.createId; } });
|
|
117
113
|
function compact(value) {
|
|
118
114
|
if (!value)
|
|
119
115
|
return undefined;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createId = createId;
|
|
4
|
+
// Single source of truth for the deterministic record id scheme shared by the
|
|
5
|
+
// multi-agent kernel (multi-agent/helpers.ts) and the coordinator/blackboard
|
|
6
|
+
// layer (coordinator/util.ts). Both previously copy-pasted byte-identical
|
|
7
|
+
// zero-padded-seq logic; this dedup (F10) collapses them onto one helper.
|
|
8
|
+
//
|
|
9
|
+
// Deterministic record id (FreeBSD-audit L12/L13): the record's POSITION in its
|
|
10
|
+
// per-run collection, threaded from the call site. No wall-clock stamp, no PRNG
|
|
11
|
+
// suffix — re-running the same multi-agent topology / coordination mints
|
|
12
|
+
// byte-identical ids, so snapshot/replay digests match. Each call site already
|
|
13
|
+
// asserts the minted id is unique within its collection, and these collections
|
|
14
|
+
// only ever append.
|
|
15
|
+
//
|
|
16
|
+
// REPLAY DETERMINISM: pure function of its arguments — no Date, no random.
|
|
17
|
+
// The output MUST stay byte-identical: `${prefix}-${4-digit-zero-padded-seq}`.
|
|
18
|
+
function createId(prefix, seq) {
|
|
19
|
+
return `${prefix}-${String(seq).padStart(4, "0")}`;
|
|
20
|
+
}
|
package/dist/multi-agent-eval.js
CHANGED
|
@@ -124,6 +124,14 @@ function replayMultiAgentSnapshot(target, options = {}) {
|
|
|
124
124
|
const replayDir = node_path_1.default.join(suiteDir, "replay");
|
|
125
125
|
const replayRunPath = node_path_1.default.join(suiteDir, "replay-run.json");
|
|
126
126
|
node_fs_1.default.mkdirSync(replayDir, { recursive: true });
|
|
127
|
+
// RE-DERIVE the projection from the raw captured state instead of copying
|
|
128
|
+
// snapshot.normalized. Copying the baseline would make compareMultiAgentReplay
|
|
129
|
+
// diff the baseline against a byte-copy of itself, so a projection-determinism
|
|
130
|
+
// regression in normalizeRun could never be caught (the determinism moat would
|
|
131
|
+
// be false-green). We reconstruct the run from the captured raw state and run
|
|
132
|
+
// the SAME normalizeRun pipeline used at capture time, producing an INDEPENDENT
|
|
133
|
+
// re-projection the comparison can pit against the baseline.
|
|
134
|
+
const replayed = rederiveNormalizedFromSnapshot(snapshot);
|
|
127
135
|
const replay = {
|
|
128
136
|
schemaVersion: 1,
|
|
129
137
|
kind: "multi-agent-replay-run",
|
|
@@ -139,7 +147,7 @@ function replayMultiAgentSnapshot(target, options = {}) {
|
|
|
139
147
|
replayRunPath,
|
|
140
148
|
snapshotPath: snapshot.paths.snapshotPath
|
|
141
149
|
},
|
|
142
|
-
replay:
|
|
150
|
+
replay: replayed,
|
|
143
151
|
errors: []
|
|
144
152
|
};
|
|
145
153
|
(0, state_1.writeJson)(replayRunPath, replay);
|
|
@@ -398,6 +406,24 @@ function loadScoreForTarget(target, scorePath) {
|
|
|
398
406
|
}
|
|
399
407
|
return scoreMultiAgentReplay(target);
|
|
400
408
|
}
|
|
409
|
+
// Re-derive snapshot.normalized INDEPENDENTLY at replay time. The raw captured
|
|
410
|
+
// state is the baseline run state file (snapshot.paths.baselineStatePath) — the
|
|
411
|
+
// same WorkflowRun captureRun/normalizeRun projected at capture time. We re-load
|
|
412
|
+
// it and re-run the IDENTICAL normalizeRun pipeline, so the result is a genuine
|
|
413
|
+
// re-projection rather than a copy of the baseline. Fail-closed: if the raw
|
|
414
|
+
// state cannot be reconstructed we throw (never silently fall back to the
|
|
415
|
+
// baseline copy — that is exactly the false-green this guards against).
|
|
416
|
+
function rederiveNormalizedFromSnapshot(snapshot) {
|
|
417
|
+
const statePath = snapshot.paths.baselineStatePath;
|
|
418
|
+
if (!statePath || !node_fs_1.default.existsSync(statePath)) {
|
|
419
|
+
throw new Error(`Cannot re-derive replay projection: baseline run state missing at ${statePath || "<unset>"}; re-snapshot from a live run before replaying.`);
|
|
420
|
+
}
|
|
421
|
+
const result = (0, state_1.loadRunStateFile)(statePath, { dryRun: true });
|
|
422
|
+
if (result.report.status === "unsupported") {
|
|
423
|
+
throw new Error(`Cannot re-derive replay projection: baseline run state at ${statePath} is unsupported: ${result.report.errors.join("; ")}`);
|
|
424
|
+
}
|
|
425
|
+
return normalizeRun(result.run);
|
|
426
|
+
}
|
|
401
427
|
function captureRun(run) {
|
|
402
428
|
return {
|
|
403
429
|
topology: run.topologies || { schemaVersion: 1, runs: [] },
|
package/dist/multi-agent-host.js
CHANGED
|
@@ -384,22 +384,54 @@ function hostSelect(run, options = {}) {
|
|
|
384
384
|
}
|
|
385
385
|
return envelope(run, "select", { performed: "selected-candidate", data: selection });
|
|
386
386
|
}
|
|
387
|
+
function memoize(compute) {
|
|
388
|
+
let cached;
|
|
389
|
+
return (run) => (cached ??= { value: compute(run) }).value;
|
|
390
|
+
}
|
|
391
|
+
function createHostSummaryCache(run) {
|
|
392
|
+
const topologies = memoize(topology_1.summarizeTopologies);
|
|
393
|
+
const multiAgent = memoize(multi_agent_1.summarizeMultiAgent);
|
|
394
|
+
const blackboard = memoize(coordinator_1.summarizeBlackboard);
|
|
395
|
+
const workers = memoize(operator_ux_1.summarizeOperatorWorkers);
|
|
396
|
+
const candidates = memoize(operator_ux_1.summarizeOperatorCandidates);
|
|
397
|
+
const feedback = memoize(operator_ux_1.summarizeOperatorFeedback);
|
|
398
|
+
const commits = memoize(operator_ux_1.summarizeOperatorCommits);
|
|
399
|
+
const trust = memoize(trust_audit_1.summarizeTrustAudit);
|
|
400
|
+
const operator = memoize(operator_ux_1.summarizeOperatorRun);
|
|
401
|
+
const multiAgentOperator = memoize(multi_agent_operator_ux_1.summarizeMultiAgentOperator);
|
|
402
|
+
const active = memoize(activeTopologies);
|
|
403
|
+
return {
|
|
404
|
+
run,
|
|
405
|
+
topologies: () => topologies(run),
|
|
406
|
+
multiAgent: () => multiAgent(run),
|
|
407
|
+
blackboard: () => blackboard(run),
|
|
408
|
+
workers: () => workers(run),
|
|
409
|
+
candidates: () => candidates(run),
|
|
410
|
+
feedback: () => feedback(run),
|
|
411
|
+
commits: () => commits(run),
|
|
412
|
+
trust: () => trust(run),
|
|
413
|
+
operator: () => operator(run),
|
|
414
|
+
multiAgentOperator: () => multiAgentOperator(run),
|
|
415
|
+
active: () => active(run)
|
|
416
|
+
};
|
|
417
|
+
}
|
|
387
418
|
function envelope(run, command, options = {}) {
|
|
388
|
-
const
|
|
389
|
-
const
|
|
390
|
-
const
|
|
391
|
-
const
|
|
392
|
-
const
|
|
393
|
-
const
|
|
394
|
-
const
|
|
395
|
-
const
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
const
|
|
419
|
+
const cache = createHostSummaryCache(run);
|
|
420
|
+
const topologies = cache.topologies();
|
|
421
|
+
const multiAgent = cache.multiAgent();
|
|
422
|
+
const blackboard = cache.blackboard();
|
|
423
|
+
const workers = cache.workers();
|
|
424
|
+
const candidates = cache.candidates();
|
|
425
|
+
const feedback = cache.feedback();
|
|
426
|
+
const commits = cache.commits();
|
|
427
|
+
const trust = cache.trust();
|
|
428
|
+
const operator = cache.operator();
|
|
429
|
+
const multiAgentOperator = cache.multiAgentOperator();
|
|
430
|
+
const active = cache.active();
|
|
399
431
|
const blockedReasons = unique([...operator.blockedReasons, ...(options.extraBlockedReasons || [])]);
|
|
400
|
-
const state = blockedReasons.length ? "blocked" : classifyHostState(run);
|
|
432
|
+
const state = blockedReasons.length ? "blocked" : classifyHostState(run, cache);
|
|
401
433
|
const ids = activeIds(run, active);
|
|
402
|
-
const nextActions = hostNextActions(run, state, active, options.requiredHostAction);
|
|
434
|
+
const nextActions = hostNextActions(run, state, active, options.requiredHostAction, cache);
|
|
403
435
|
return {
|
|
404
436
|
schemaVersion: 1,
|
|
405
437
|
surface: "multi-agent-host",
|
|
@@ -427,12 +459,12 @@ function envelope(run, command, options = {}) {
|
|
|
427
459
|
data: options.data
|
|
428
460
|
};
|
|
429
461
|
}
|
|
430
|
-
function classifyHostState(run) {
|
|
431
|
-
const active =
|
|
432
|
-
const feedback =
|
|
433
|
-
const workers =
|
|
434
|
-
const candidates =
|
|
435
|
-
const commits =
|
|
462
|
+
function classifyHostState(run, cache = createHostSummaryCache(run)) {
|
|
463
|
+
const active = cache.active();
|
|
464
|
+
const feedback = cache.feedback();
|
|
465
|
+
const workers = cache.workers();
|
|
466
|
+
const candidates = cache.candidates();
|
|
467
|
+
const commits = cache.commits();
|
|
436
468
|
if (!active.length && !(run.multiAgent?.runs || []).length)
|
|
437
469
|
return "needs-run";
|
|
438
470
|
if (feedback.open.some((entry) => !entry.retryable))
|
|
@@ -475,7 +507,7 @@ function activeIds(run, active) {
|
|
|
475
507
|
auditEventIds: unique(active.flatMap((entry) => entry.links.auditEventIds))
|
|
476
508
|
};
|
|
477
509
|
}
|
|
478
|
-
function hostNextActions(run, state, active, requiredHostAction) {
|
|
510
|
+
function hostNextActions(run, state, active, requiredHostAction, cache = createHostSummaryCache(run)) {
|
|
479
511
|
if (requiredHostAction)
|
|
480
512
|
return [{ command: "host-action", reason: requiredHostAction, priority: "high" }];
|
|
481
513
|
const runId = run.id;
|
|
@@ -493,7 +525,7 @@ function hostNextActions(run, state, active, requiredHostAction) {
|
|
|
493
525
|
case "ready-for-selection":
|
|
494
526
|
return [{ command: `node scripts/cw.js multi-agent select ${runId} --candidate <candidate-id> --reason "<rationale>"`, reason: "Select a scored candidate after verifier gates pass.", priority: "high" }];
|
|
495
527
|
case "ready-for-commit": {
|
|
496
|
-
const ready =
|
|
528
|
+
const ready = cache.candidates().readyForCommit[0];
|
|
497
529
|
return [{ command: `node scripts/cw.js commit ${runId} --selection ${ready.selectionId} --reason "<verified rationale>"`, reason: "Create a verifier-gated CW state commit.", priority: "high" }];
|
|
498
530
|
}
|
|
499
531
|
case "complete":
|
|
@@ -15,6 +15,7 @@ const coordinator_1 = require("./coordinator");
|
|
|
15
15
|
const multi_agent_1 = require("./multi-agent");
|
|
16
16
|
const topology_1 = require("./topology");
|
|
17
17
|
const trust_audit_1 = require("./trust-audit");
|
|
18
|
+
const validation_1 = require("./validation");
|
|
18
19
|
function summarizeMultiAgentOperator(run) {
|
|
19
20
|
const topologies = (0, topology_1.summarizeTopologies)(run);
|
|
20
21
|
const multiAgent = (0, multi_agent_1.summarizeMultiAgent)(run);
|
|
@@ -447,7 +448,7 @@ function readScores(run, candidateId) {
|
|
|
447
448
|
.readdirSync(dir)
|
|
448
449
|
.filter((file) => file.endsWith(".json"))
|
|
449
450
|
.sort()
|
|
450
|
-
.map((file) => JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(dir, file), "utf8")));
|
|
451
|
+
.map((file) => (0, validation_1.validateCandidateScore)(JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(dir, file), "utf8"))));
|
|
451
452
|
}
|
|
452
453
|
function scorePath(run, candidateId, scoreId) {
|
|
453
454
|
const file = node_path_1.default.join(run.paths.candidatesDir || node_path_1.default.join(run.paths.runDir, "candidates"), safeFileName(candidateId), "scores", `${safeFileName(scoreId)}.json`);
|