cool-workflow 0.1.81 → 0.1.82
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 +1 -1
- 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 +64 -85
- package/dist/capability-registry.js +22 -3
- package/dist/commit.js +212 -203
- package/dist/coordinator/util.js +6 -9
- package/dist/dispatch.js +11 -3
- 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/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 +16 -2
- package/dist/reclamation.js +8 -36
- package/dist/scheduler.js +34 -4
- 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 +143 -58
- package/docs/agent-delegation-drive.7.md +1 -0
- package/docs/cli-mcp-parity.7.md +1 -0
- package/docs/contract-migration-tooling.7.md +1 -0
- package/docs/control-plane-scheduling.7.md +1 -0
- package/docs/durable-state-and-locking.7.md +1 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +1 -0
- package/docs/execution-backends.7.md +1 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +1 -0
- package/docs/multi-agent-eval-replay-harness.7.md +1 -0
- package/docs/multi-agent-operator-ux.7.md +1 -0
- package/docs/node-snapshot-diff-replay.7.md +1 -0
- package/docs/observability-cost-accounting.7.md +1 -0
- package/docs/project-index.md +8 -4
- package/docs/real-execution-backends.7.md +1 -0
- package/docs/release-and-migration.7.md +1 -0
- package/docs/release-tooling.7.md +33 -2
- package/docs/run-registry-control-plane.7.md +1 -0
- package/docs/run-retention-reclamation.7.md +1 -0
- package/docs/state-explosion-management.7.md +1 -0
- package/docs/team-collaboration.7.md +1 -0
- package/docs/web-desktop-workbench.7.md +1 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +1 -1
- package/package.json +1 -1
- 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/golden-path.js +4 -4
- package/scripts/release-flow.js +181 -5
|
@@ -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
|
+
}
|
|
@@ -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`);
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.policyForRole = policyForRole;
|
|
7
4
|
exports.policyForGroup = policyForGroup;
|
|
@@ -16,7 +13,7 @@ exports.summarizeMultiAgentTrust = summarizeMultiAgentTrust;
|
|
|
16
13
|
exports.hasAcceptedJudgeRationale = hasAcceptedJudgeRationale;
|
|
17
14
|
exports.sourceForActor = sourceForActor;
|
|
18
15
|
exports.hashText = hashText;
|
|
19
|
-
const
|
|
16
|
+
const execution_backend_1 = require("./execution-backend");
|
|
20
17
|
const trust_audit_1 = require("./trust-audit");
|
|
21
18
|
function policyForRole(role) {
|
|
22
19
|
const topologyRole = String(role.metadata?.topologyRoleId || role.title || "").toLowerCase();
|
|
@@ -294,8 +291,11 @@ function sourceForActor(actor) {
|
|
|
294
291
|
return "runtime-derived";
|
|
295
292
|
return "cw-validated";
|
|
296
293
|
}
|
|
294
|
+
// Delegates to the shared execution-backend sha256 (F10 dedup). Byte-identical:
|
|
295
|
+
// both emit `sha256:<hex>` and Node's Hash.update(string) defaults to utf8, the
|
|
296
|
+
// same encoding the shared helper passes explicitly.
|
|
297
297
|
function hashText(value) {
|
|
298
|
-
return
|
|
298
|
+
return (0, execution_backend_1.sha256)(value);
|
|
299
299
|
}
|
|
300
300
|
function resolvePolicy(run, input) {
|
|
301
301
|
const membership = input.membershipId ? run.multiAgent?.memberships.find((entry) => entry.id === input.membershipId) : undefined;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Canonical StateNode projection (F9 de-triplication) — the SINGLE source of
|
|
3
|
+
// truth for which StateNode fields make up the derived, fingerprinted body.
|
|
4
|
+
//
|
|
5
|
+
// Previously this 13-field set was hand-copied in three places: node-snapshot.ts's
|
|
6
|
+
// snapshotBody, reclamation.ts's snapshotProjectionDigest, and reclamation.ts's
|
|
7
|
+
// rawNodeBody/nodeBodyDigest (whose comment literally said "Mirror
|
|
8
|
+
// node-snapshot.ts"). Any field added in one place but not the others would drift
|
|
9
|
+
// the projection — and because reclamation hashes are tombstone-chained, a drift
|
|
10
|
+
// would silently break the chain. This module collapses the list to ONE export so
|
|
11
|
+
// the field set can only change in one place.
|
|
12
|
+
//
|
|
13
|
+
// BYTE-IDENTITY [load-bearing]: the projection / digest OUTPUT here is identical to
|
|
14
|
+
// the prior in-line implementations. `normalizeValue` sorts object keys, so the
|
|
15
|
+
// literal field ORDER never affected the bytes — only the field SET ever mattered.
|
|
16
|
+
// `replayStableStringify(value) === JSON.stringify(normalizeValue(value))` and
|
|
17
|
+
// `normalizeValue` is idempotent, so the projected-body digest and the raw-body
|
|
18
|
+
// digest funnel through the same canonical bytes (node-snapshot-diff-replay +
|
|
19
|
+
// reclamation smokes verify this).
|
|
20
|
+
//
|
|
21
|
+
// Pure: no I/O, no wall-clock, no random — safe in core/state/replay logic.
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.rawNodeProjection = rawNodeProjection;
|
|
24
|
+
exports.projectNodeBody = projectNodeBody;
|
|
25
|
+
exports.nodeProjectionDigestInput = nodeProjectionDigestInput;
|
|
26
|
+
const multi_agent_eval_1 = require("./multi-agent-eval");
|
|
27
|
+
/** The raw (un-normalized) projection input: the canonical field set, copied off
|
|
28
|
+
* the StateNode with no scrubbing. `normalizeValue`/`replayStableStringify` apply
|
|
29
|
+
* the timestamp/path scrubbing downstream. This is the ONE place the field list
|
|
30
|
+
* lives — add or drop a projected field here and every consumer follows. */
|
|
31
|
+
function rawNodeProjection(node) {
|
|
32
|
+
return {
|
|
33
|
+
id: node.id,
|
|
34
|
+
kind: node.kind,
|
|
35
|
+
status: node.status,
|
|
36
|
+
loopStage: node.loopStage,
|
|
37
|
+
inputs: node.inputs,
|
|
38
|
+
outputs: node.outputs,
|
|
39
|
+
artifacts: node.artifacts,
|
|
40
|
+
evidence: node.evidence,
|
|
41
|
+
errors: node.errors,
|
|
42
|
+
parents: node.parents,
|
|
43
|
+
children: node.children,
|
|
44
|
+
contractId: node.contractId,
|
|
45
|
+
metadata: node.metadata
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** The normalized, derived projected body of one StateNode — timestamps/paths
|
|
49
|
+
* stripped via the eval normalizer, so it is byte-stable across captures of the
|
|
50
|
+
* same logical state. This is exactly node-snapshot.ts's historical snapshotBody. */
|
|
51
|
+
function projectNodeBody(node) {
|
|
52
|
+
return (0, multi_agent_eval_1.normalizeValue)(rawNodeProjection(node));
|
|
53
|
+
}
|
|
54
|
+
/** Stable digest input for the projected body: the canonical bytes that the
|
|
55
|
+
* snapshot/reconstruction digests bind. `replayStableStringify` re-normalizes, so
|
|
56
|
+
* feeding the raw projection or the already-normalized body yields identical bytes. */
|
|
57
|
+
function nodeProjectionDigestInput(node) {
|
|
58
|
+
return (0, multi_agent_eval_1.replayStableStringify)(rawNodeProjection(node));
|
|
59
|
+
}
|
package/dist/node-snapshot.js
CHANGED
|
@@ -37,7 +37,9 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
37
37
|
const pipeline_runner_1 = require("./pipeline-runner");
|
|
38
38
|
const state_1 = require("./state");
|
|
39
39
|
const multi_agent_eval_1 = require("./multi-agent-eval");
|
|
40
|
+
const node_projection_1 = require("./node-projection");
|
|
40
41
|
const state_explosion_1 = require("./state-explosion");
|
|
42
|
+
const validation_1 = require("./validation");
|
|
41
43
|
exports.NODE_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
42
44
|
/** Structured fail-closed error (mirrors the PipelineContractError shape). */
|
|
43
45
|
class NodeSnapshotError extends Error {
|
|
@@ -64,23 +66,11 @@ const SNAPSHOT_SECTIONS = [
|
|
|
64
66
|
"metadata"
|
|
65
67
|
];
|
|
66
68
|
/** The normalized projection of a node — timestamps/paths stripped by the eval
|
|
67
|
-
* normalizer, so it is byte-stable across captures of the same logical state.
|
|
69
|
+
* normalizer, so it is byte-stable across captures of the same logical state. The
|
|
70
|
+
* canonical field set lives in node-projection.ts (shared with reclamation.ts so
|
|
71
|
+
* the projection can never drift across the two). */
|
|
68
72
|
function snapshotBody(node) {
|
|
69
|
-
return (0,
|
|
70
|
-
id: node.id,
|
|
71
|
-
kind: node.kind,
|
|
72
|
-
status: node.status,
|
|
73
|
-
loopStage: node.loopStage,
|
|
74
|
-
inputs: node.inputs,
|
|
75
|
-
outputs: node.outputs,
|
|
76
|
-
artifacts: node.artifacts,
|
|
77
|
-
evidence: node.evidence,
|
|
78
|
-
errors: node.errors,
|
|
79
|
-
parents: node.parents,
|
|
80
|
-
children: node.children,
|
|
81
|
-
contractId: node.contractId,
|
|
82
|
-
metadata: node.metadata
|
|
83
|
-
});
|
|
73
|
+
return (0, node_projection_1.projectNodeBody)(node);
|
|
84
74
|
}
|
|
85
75
|
/** RAW fingerprint (NOT normalized): any transition (updatedAt/status) or
|
|
86
76
|
* artifact/evidence change flips it, which is how drift is detected. */
|
|
@@ -113,7 +103,7 @@ function readNodeSnapshot(run, snapshotId) {
|
|
|
113
103
|
for (const nodeDir of node_fs_1.default.readdirSync(root)) {
|
|
114
104
|
const file = node_path_1.default.join(root, nodeDir, `${snapshotId}.json`);
|
|
115
105
|
if (node_fs_1.default.existsSync(file))
|
|
116
|
-
return JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
|
|
106
|
+
return (0, validation_1.validateNodeSnapshot)(JSON.parse(node_fs_1.default.readFileSync(file, "utf8")));
|
|
117
107
|
}
|
|
118
108
|
}
|
|
119
109
|
throw new NodeSnapshotError("snapshot-not-found", `Node snapshot ${snapshotId} not found in run ${run.id}`, { freshness: "absent" });
|
|
@@ -125,7 +115,7 @@ function readNodeReplay(run, replayId) {
|
|
|
125
115
|
for (const nodeDir of node_fs_1.default.readdirSync(root)) {
|
|
126
116
|
const file = node_path_1.default.join(root, nodeDir, "replays", `${replayId}.json`);
|
|
127
117
|
if (node_fs_1.default.existsSync(file))
|
|
128
|
-
return JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
|
|
118
|
+
return (0, validation_1.validateNodeReplayRun)(JSON.parse(node_fs_1.default.readFileSync(file, "utf8")));
|
|
129
119
|
}
|
|
130
120
|
}
|
|
131
121
|
throw new NodeSnapshotError("replay-not-found", `Node replay ${replayId} not found in run ${run.id}`, { freshness: "absent" });
|
|
@@ -17,6 +17,7 @@ exports.commit = commit;
|
|
|
17
17
|
// plan() receives an already-resolved workflow app record (the runner still owns
|
|
18
18
|
// app loading, which is instance-stateful). Behavior is identical to the inline
|
|
19
19
|
// implementations; only the location changed.
|
|
20
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
20
21
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
21
22
|
const node_path_1 = __importDefault(require("node:path"));
|
|
22
23
|
const state_1 = require("../state");
|
|
@@ -456,8 +457,28 @@ function renderPrompt(prompt, inputs) {
|
|
|
456
457
|
}
|
|
457
458
|
return rendered;
|
|
458
459
|
}
|
|
460
|
+
// Deterministic run id (replay-determinism self-audit): the wall-clock stamp is an
|
|
461
|
+
// edge timestamp (recorded once and stripped on replay), but the former
|
|
462
|
+
// Math.random() suffix made the run id itself non-reproducible — re-deriving the id
|
|
463
|
+
// for the SAME recorded run would never match. The suffix is now a content hash of
|
|
464
|
+
// the run's deterministic identity (workflowId + the recorded stamp), so the id is a
|
|
465
|
+
// pure function of inputs that already live in state. Distinct plan() invocations
|
|
466
|
+
// still get distinct ids because the per-millisecond stamp differs; replaying a
|
|
467
|
+
// recorded run reproduces the byte-identical id. Mirrors the de-clock done for
|
|
468
|
+
// worker ids in src/worker-isolation/paths.ts.
|
|
469
|
+
let runIdSequence = 0;
|
|
459
470
|
function createRunId(workflowId) {
|
|
460
471
|
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
|
|
461
|
-
|
|
472
|
+
// The stamp is second-resolution, so several runs of the same workflowId minted
|
|
473
|
+
// within one second would otherwise hash to the SAME id. process.pid + a monotonic
|
|
474
|
+
// counter break the tie across BOTH same-process (counter) and concurrent-process
|
|
475
|
+
// (pid) minting — deterministic-by-environment, not a PRNG, so it keeps the
|
|
476
|
+
// replay-determinism intent; the id is an edge stamp stripped on replay anyway.
|
|
477
|
+
runIdSequence += 1;
|
|
478
|
+
const suffix = node_crypto_1.default
|
|
479
|
+
.createHash("sha256")
|
|
480
|
+
.update(`${workflowId}:${stamp}:${process.pid}:${runIdSequence}`)
|
|
481
|
+
.digest("hex")
|
|
482
|
+
.slice(0, 6);
|
|
462
483
|
return `${workflowId}-${stamp}-${suffix}`;
|
|
463
484
|
}
|
package/dist/orchestrator.js
CHANGED
|
@@ -90,10 +90,24 @@ class CoolWorkflowRunner {
|
|
|
90
90
|
pluginRoot;
|
|
91
91
|
workflowsDir;
|
|
92
92
|
appsDir;
|
|
93
|
-
|
|
93
|
+
// F7: the directory a run is resolved against (replaces the former process.chdir
|
|
94
|
+
// bracket in capability-core). undefined => fall back to process.cwd(). The runner
|
|
95
|
+
// reads runs from disk per call (no in-memory run state), so withBaseDir hands back
|
|
96
|
+
// a cheap scoped clone instead of mutating the global process cwd.
|
|
97
|
+
baseDir;
|
|
98
|
+
constructor({ pluginRoot, baseDir }) {
|
|
94
99
|
this.pluginRoot = resolvePluginRoot(pluginRoot);
|
|
95
100
|
this.workflowsDir = node_path_1.default.join(this.pluginRoot, "workflows");
|
|
96
101
|
this.appsDir = node_path_1.default.join(this.pluginRoot, "apps");
|
|
102
|
+
this.baseDir = baseDir ? node_path_1.default.resolve(baseDir) : undefined;
|
|
103
|
+
}
|
|
104
|
+
/** Return a runner that resolves runs against `dir` instead of process.cwd(),
|
|
105
|
+
* WITHOUT chdir-ing the process (F7). Same instance when the dir is unchanged. */
|
|
106
|
+
withBaseDir(dir) {
|
|
107
|
+
const resolved = dir ? node_path_1.default.resolve(dir) : undefined;
|
|
108
|
+
if (resolved === this.baseDir)
|
|
109
|
+
return this;
|
|
110
|
+
return new CoolWorkflowRunner({ pluginRoot: this.pluginRoot, baseDir: resolved });
|
|
97
111
|
}
|
|
98
112
|
listWorkflows() {
|
|
99
113
|
return this.loadWorkflowApps().map((record) => {
|
|
@@ -700,7 +714,7 @@ class CoolWorkflowRunner {
|
|
|
700
714
|
return feedbackOps.resolveFeedback(this.loadRun(runId), feedbackId, options);
|
|
701
715
|
}
|
|
702
716
|
loadRun(runId) {
|
|
703
|
-
return (0, state_1.loadRunFromCwd)(runId);
|
|
717
|
+
return (0, state_1.loadRunFromCwd)(runId, this.baseDir);
|
|
704
718
|
}
|
|
705
719
|
loadWorkflowAppById(appId) {
|
|
706
720
|
const record = this.loadWorkflowApps().find((candidate) => candidate.app.id === appId);
|
package/dist/reclamation.js
CHANGED
|
@@ -53,6 +53,7 @@ const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
|
53
53
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
54
54
|
const node_path_1 = __importDefault(require("node:path"));
|
|
55
55
|
const multi_agent_eval_1 = require("./multi-agent-eval");
|
|
56
|
+
const node_projection_1 = require("./node-projection");
|
|
56
57
|
const node_snapshot_1 = require("./node-snapshot");
|
|
57
58
|
const state_1 = require("./state");
|
|
58
59
|
const trust_audit_1 = require("./trust-audit");
|
|
@@ -397,46 +398,17 @@ function buildReferenceGraph(run) {
|
|
|
397
398
|
add(message.id);
|
|
398
399
|
return refs;
|
|
399
400
|
}
|
|
401
|
+
/** expectDigest of a node's deterministic projection. Re-uses the SHARED
|
|
402
|
+
* node-projection field set (node-projection.ts) so reconstruction matches
|
|
403
|
+
* node-snapshot.ts's body byte-for-byte — the projection can no longer drift. */
|
|
400
404
|
function snapshotProjectionDigest(node) {
|
|
401
|
-
|
|
402
|
-
const body = (0, multi_agent_eval_1.normalizeValue)({
|
|
403
|
-
id: node.id,
|
|
404
|
-
kind: node.kind,
|
|
405
|
-
status: node.status,
|
|
406
|
-
loopStage: node.loopStage,
|
|
407
|
-
inputs: node.inputs,
|
|
408
|
-
outputs: node.outputs,
|
|
409
|
-
artifacts: node.artifacts,
|
|
410
|
-
evidence: node.evidence,
|
|
411
|
-
errors: node.errors,
|
|
412
|
-
parents: node.parents,
|
|
413
|
-
children: node.children,
|
|
414
|
-
contractId: node.contractId,
|
|
415
|
-
metadata: node.metadata
|
|
416
|
-
});
|
|
417
|
-
return sha256OfString((0, multi_agent_eval_1.replayStableStringify)(body));
|
|
405
|
+
return sha256OfString((0, node_projection_1.nodeProjectionDigestInput)(node));
|
|
418
406
|
}
|
|
419
407
|
/** Body digest of the RETAINED node (lives in state.json). The reconstruction
|
|
420
|
-
* verifier re-derives the projection from this retained input.
|
|
408
|
+
* verifier re-derives the projection from this retained input. Same shared field
|
|
409
|
+
* set / canonical bytes as snapshotProjectionDigest. */
|
|
421
410
|
function nodeBodyDigest(node) {
|
|
422
|
-
return sha256OfString((0,
|
|
423
|
-
}
|
|
424
|
-
function rawNodeBody(node) {
|
|
425
|
-
return {
|
|
426
|
-
id: node.id,
|
|
427
|
-
kind: node.kind,
|
|
428
|
-
status: node.status,
|
|
429
|
-
loopStage: node.loopStage,
|
|
430
|
-
inputs: node.inputs,
|
|
431
|
-
outputs: node.outputs,
|
|
432
|
-
artifacts: node.artifacts,
|
|
433
|
-
evidence: node.evidence,
|
|
434
|
-
errors: node.errors,
|
|
435
|
-
parents: node.parents,
|
|
436
|
-
children: node.children,
|
|
437
|
-
contractId: node.contractId,
|
|
438
|
-
metadata: node.metadata
|
|
439
|
-
};
|
|
411
|
+
return sha256OfString((0, node_projection_1.nodeProjectionDigestInput)(node));
|
|
440
412
|
}
|
|
441
413
|
/** Build the retention plan: which paths are freeable under `policy`, of what
|
|
442
414
|
* kind, how many bytes, and the resulting capability downgrade. */
|