cool-workflow 0.2.1 → 0.2.2
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/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/cli/dispatch.js +18 -86
- package/dist/cli/parseargv.js +5 -2
- package/dist/core/capability-data.js +270 -0
- package/dist/core/capability-table.js +16 -3504
- package/dist/core/format/help.js +28 -5
- package/dist/core/multi-agent/collaboration.js +3 -2
- package/dist/core/multi-agent/coordinator.js +4 -3
- package/dist/core/pipeline/commit-gate.js +2 -1
- package/dist/core/state/state-explosion/digest.js +3 -2
- package/dist/core/state/state-explosion/graph.js +9 -8
- package/dist/core/state/state-explosion/helpers.js +2 -1
- package/dist/core/trust/ledger.js +2 -1
- package/dist/core/types/execution-backend.js +18 -0
- package/dist/core/types/observability.js +7 -0
- package/dist/core/util/collate.js +23 -0
- package/dist/core/version.js +1 -1
- package/dist/shell/audit-cli.js +46 -2
- package/dist/shell/commit-summary.js +2 -1
- package/dist/shell/drive.js +26 -10
- package/dist/shell/evidence-reasoning.js +4 -3
- package/dist/shell/execution-backend/registry.js +2 -1
- package/dist/shell/execution-backend/types.js +0 -9
- package/dist/shell/fs-atomic.js +14 -1
- package/dist/shell/multi-agent-operator-ux.js +4 -3
- package/dist/shell/observability.js +12 -11
- package/dist/shell/onramp.js +17 -1
- package/dist/shell/operator-ux-text.js +2 -1
- package/dist/shell/operator-ux.js +7 -6
- package/dist/shell/pipeline-cli.js +82 -72
- package/dist/shell/reclamation-io.js +2 -1
- package/dist/shell/report.js +2 -1
- package/dist/shell/run-store.js +17 -0
- package/dist/shell/state-explosion-cli.js +2 -1
- package/dist/shell/topology-io.js +2 -1
- package/dist/shell/trust-audit.js +41 -2
- package/dist/shell/worker-cli.js +9 -1
- package/dist/shell/worker-isolation.js +34 -9
- package/dist/shell/workflow-app-loader.js +3 -2
- package/dist/wiring/capability-table/basics.js +66 -0
- package/dist/wiring/capability-table/exec-backend.js +162 -0
- package/dist/wiring/capability-table/index.js +42 -0
- package/dist/wiring/capability-table/multi-agent.js +623 -0
- package/dist/wiring/capability-table/parity.js +466 -0
- package/dist/wiring/capability-table/pipeline.js +280 -0
- package/dist/wiring/capability-table/registry-core.js +189 -0
- package/dist/wiring/capability-table/reporting.js +394 -0
- package/dist/wiring/capability-table/scheduling-registry.js +558 -0
- package/dist/wiring/capability-table/state.js +173 -0
- package/dist/wiring/capability-table/trust-ledger.js +134 -0
- package/dist/wiring/capability-table/workflow-apps.js +366 -0
- package/docs/agent-delegation-drive.7.md +2 -0
- package/docs/cli-mcp-parity.7.md +7 -2
- package/docs/contract-migration-tooling.7.md +2 -0
- package/docs/control-plane-scheduling.7.md +2 -0
- package/docs/durable-state-and-locking.7.md +22 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
- package/docs/execution-backends.7.md +2 -0
- package/docs/index.md +1 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
- package/docs/multi-agent-eval-replay-harness.7.md +2 -0
- package/docs/multi-agent-operator-ux.7.md +2 -0
- package/docs/node-snapshot-diff-replay.7.md +2 -0
- package/docs/observability-cost-accounting.7.md +2 -0
- package/docs/project-index.md +25 -5
- package/docs/real-execution-backends.7.md +2 -0
- package/docs/release-and-migration.7.md +2 -0
- package/docs/release-tooling.7.md +2 -0
- package/docs/run-registry-control-plane.7.md +2 -0
- package/docs/run-retention-reclamation.7.md +2 -0
- package/docs/security-trust-hardening.7.md +30 -0
- package/docs/state-explosion-management.7.md +2 -0
- package/docs/team-collaboration.7.md +2 -0
- package/docs/trust-audit-anchor.7.md +69 -0
- package/docs/web-desktop-workbench.7.md +2 -0
- package/manifest/plugin.manifest.json +1 -1
- package/package.json +3 -1
- package/scripts/bump-version.js +24 -2
- package/scripts/canonical-apps.js +4 -4
- package/scripts/dogfood-release.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/purity-baseline.json +68 -0
- package/scripts/purity-gate.js +239 -0
- package/scripts/release-check.js +8 -1
- package/scripts/version-sync-check.js +33 -12
- package/workflows/README.md +19 -0
|
@@ -66,6 +66,7 @@ const fs = __importStar(require("node:fs"));
|
|
|
66
66
|
const path = __importStar(require("node:path"));
|
|
67
67
|
const fs_atomic_1 = require("./fs-atomic");
|
|
68
68
|
const telemetry_ledger_io_1 = require("./telemetry-ledger-io");
|
|
69
|
+
const collate_1 = require("../core/util/collate");
|
|
69
70
|
exports.METRICS_SCHEMA_VERSION = 1;
|
|
70
71
|
const VERIFIER_PASS_STATUSES = new Set(["verified", "completed", "committed"]);
|
|
71
72
|
const VERIFIER_FAIL_STATUSES = new Set(["failed", "rejected", "blocked"]);
|
|
@@ -105,22 +106,22 @@ function usageKey(usage) {
|
|
|
105
106
|
}
|
|
106
107
|
function fingerprintMetricsSource(run) {
|
|
107
108
|
const parts = [`id:${run.id}`, `createdAt:${run.createdAt}`, `updatedAt:${run.updatedAt}`, `app:${run.workflow.app?.id || run.workflow.id}`];
|
|
108
|
-
for (const task of [...run.tasks].sort((a, b) => a.id
|
|
109
|
+
for (const task of [...run.tasks].sort((a, b) => (0, collate_1.stableCompare)(a.id, b.id))) {
|
|
109
110
|
parts.push(`task:${task.id}:${task.status}:${task.dispatchedAt || "-"}:${task.completedAt || "-"}:${usageKey(task.usage)}:${task.backendId || "-"}`);
|
|
110
111
|
}
|
|
111
|
-
for (const worker of [...(run.workers || [])].sort((a, b) => a.id
|
|
112
|
+
for (const worker of [...(run.workers || [])].sort((a, b) => (0, collate_1.stableCompare)(a.id, b.id))) {
|
|
112
113
|
parts.push(`worker:${worker.id}:${worker.status}:${worker.output?.recordedAt || "-"}:${usageKey(worker.usage)}:${worker.backendId || "-"}`);
|
|
113
114
|
}
|
|
114
|
-
for (const node of [...(run.nodes || [])].filter((n) => n.kind === "verifier").sort((a, b) => a.id
|
|
115
|
+
for (const node of [...(run.nodes || [])].filter((n) => n.kind === "verifier").sort((a, b) => (0, collate_1.stableCompare)(a.id, b.id))) {
|
|
115
116
|
parts.push(`verifier:${node.id}:${node.status}`);
|
|
116
117
|
}
|
|
117
|
-
for (const cand of [...(run.candidates || [])].sort((a, b) => a.id
|
|
118
|
+
for (const cand of [...(run.candidates || [])].sort((a, b) => (0, collate_1.stableCompare)(a.id, b.id))) {
|
|
118
119
|
parts.push(`candidate:${cand.id}:${cand.status}`);
|
|
119
120
|
}
|
|
120
|
-
for (const fb of [...(run.feedback || [])].sort((a, b) => a.id
|
|
121
|
+
for (const fb of [...(run.feedback || [])].sort((a, b) => (0, collate_1.stableCompare)(a.id, b.id))) {
|
|
121
122
|
parts.push(`feedback:${fb.id}:${fb.status}`);
|
|
122
123
|
}
|
|
123
|
-
for (const m of [...(run.multiAgent?.memberships || [])].sort((a, b) => a.id
|
|
124
|
+
for (const m of [...(run.multiAgent?.memberships || [])].sort((a, b) => (0, collate_1.stableCompare)(a.id, b.id))) {
|
|
124
125
|
parts.push(`membership:${m.id}:${m.status}`);
|
|
125
126
|
}
|
|
126
127
|
return fingerprintStrings(parts);
|
|
@@ -148,7 +149,7 @@ function usageUnits(run) {
|
|
|
148
149
|
units.push({ unit: task.id, kind: "task", usage: task.usage });
|
|
149
150
|
}
|
|
150
151
|
}
|
|
151
|
-
return units.sort((a, b) => a.unit
|
|
152
|
+
return units.sort((a, b) => (0, collate_1.stableCompare)(a.unit, b.unit));
|
|
152
153
|
}
|
|
153
154
|
function tokenTotal(usage) {
|
|
154
155
|
if (typeof usage.totalTokens === "number")
|
|
@@ -349,12 +350,12 @@ function deriveCandidateAcceptanceRate(run) {
|
|
|
349
350
|
function taskRows(run) {
|
|
350
351
|
return run.tasks
|
|
351
352
|
.map((task) => ({ id: task.id, kind: "task", status: task.status, duration: duration(task.dispatchedAt, task.completedAt) }))
|
|
352
|
-
.sort((a, b) => a.id
|
|
353
|
+
.sort((a, b) => (0, collate_1.stableCompare)(a.id, b.id));
|
|
353
354
|
}
|
|
354
355
|
function workerRows(run) {
|
|
355
356
|
return (run.workers || [])
|
|
356
357
|
.map((worker) => ({ id: worker.id, kind: "worker", status: worker.status, duration: duration(worker.createdAt, workerEndAt(worker)) }))
|
|
357
|
-
.sort((a, b) => a.id
|
|
358
|
+
.sort((a, b) => (0, collate_1.stableCompare)(a.id, b.id));
|
|
358
359
|
}
|
|
359
360
|
function targetCreatedAt(run, target) {
|
|
360
361
|
if (target.kind === "candidate")
|
|
@@ -541,7 +542,7 @@ function deriveMetricsSummary(inputs, options) {
|
|
|
541
542
|
const report = deriveMetricsReport(input.run, { now: options.now, policy: options.policy, persistedFingerprint: input.persistedFingerprint });
|
|
542
543
|
perRun.push({ report, ref: { runId: report.runId, repo: input.repo, app: report.scope.app, backendIds: report.scope.backendIds, freshness: report.freshness.status, rates: report.rates, usage: report.usage, cost: report.cost } });
|
|
543
544
|
}
|
|
544
|
-
perRun.sort((a, b) => a.report.runId
|
|
545
|
+
perRun.sort((a, b) => (0, collate_1.stableCompare)(a.report.runId, b.report.runId));
|
|
545
546
|
const groupBy = (keyOf) => {
|
|
546
547
|
const map = new Map();
|
|
547
548
|
for (const { report } of perRun) {
|
|
@@ -552,7 +553,7 @@ function deriveMetricsSummary(inputs, options) {
|
|
|
552
553
|
}
|
|
553
554
|
}
|
|
554
555
|
return [...map.entries()]
|
|
555
|
-
.sort((a, b) => a[0]
|
|
556
|
+
.sort((a, b) => (0, collate_1.stableCompare)(a[0], b[0]))
|
|
556
557
|
.map(([key, reports]) => ({
|
|
557
558
|
key,
|
|
558
559
|
runCount: reports.length,
|
package/dist/shell/onramp.js
CHANGED
|
@@ -294,10 +294,26 @@ function evaluateOnrampContract(files, options = {}) {
|
|
|
294
294
|
const scriptFiles = normalized.filter((file) => file.startsWith("plugins/cool-workflow/scripts/"));
|
|
295
295
|
const surfaceFiles = normalized.filter(isSurfaceFile);
|
|
296
296
|
const smokeFiles = normalized.filter((file) => /^plugins\/cool-workflow\/test\/.+-smoke\.js$/.test(file));
|
|
297
|
+
// WP1.1 (#360) restored a second, parallel test layer: pure `core/`
|
|
298
|
+
// logic proven by `test/*.test.js` under `npm run test:unit`, run and
|
|
299
|
+
// gated separately from the black-box `test/*-smoke.js` suite. A cycle
|
|
300
|
+
// that proves its fix with a unit test only (no smoke touched) is a
|
|
301
|
+
// real, complete cycle — the gate must accept either kind, not just
|
|
302
|
+
// the one that existed before the unit-test layer came back.
|
|
303
|
+
const unitTestFiles = normalized.filter((file) => /^plugins\/cool-workflow\/test\/.+\.test\.js$/.test(file));
|
|
304
|
+
// The black-box conformance suite (v2/conformance/cases/*.case.js) is a
|
|
305
|
+
// third, equally real proof layer — CI-gated on every push, and the one
|
|
306
|
+
// North Star Track C leans on. A cycle proven end to end by a new or
|
|
307
|
+
// changed conformance case (with no test/*-smoke.js or test/*.test.js
|
|
308
|
+
// touched) is a real, complete cycle too.
|
|
309
|
+
const conformanceCaseFiles = normalized.filter((file) => /^v2\/conformance\/cases\/.+\.case\.js$/.test(file));
|
|
297
310
|
const docFiles = normalized.filter(isDocFile);
|
|
298
311
|
const iterationFiles = normalized.filter((file) => file === "ITERATION_LOG.md");
|
|
299
312
|
const sourceAppOrScript = runtimeFiles.length > 0 || typeFiles.length > 0 || appFiles.length > 0 || scriptFiles.length > 0;
|
|
300
|
-
if ((runtimeFiles.length > 0 || appFiles.length > 0) &&
|
|
313
|
+
if ((runtimeFiles.length > 0 || appFiles.length > 0) &&
|
|
314
|
+
smokeFiles.length === 0 &&
|
|
315
|
+
unitTestFiles.length === 0 &&
|
|
316
|
+
conformanceCaseFiles.length === 0) {
|
|
301
317
|
issues.push({
|
|
302
318
|
code: "runtime-smoke-required",
|
|
303
319
|
detail: "Runtime or app changes must include at least one smoke test change.",
|
|
@@ -24,8 +24,9 @@ exports.formatCandidateSummaryText = formatCandidateSummaryText;
|
|
|
24
24
|
exports.formatFeedbackSummaryText = formatFeedbackSummaryText;
|
|
25
25
|
const term_1 = require("./term");
|
|
26
26
|
const multi_agent_operator_ux_1 = require("./multi-agent-operator-ux");
|
|
27
|
+
const collate_1 = require("../core/util/collate");
|
|
27
28
|
function formatCounts(counts) {
|
|
28
|
-
const entries = Object.entries(counts).sort(([a], [b]) =>
|
|
29
|
+
const entries = Object.entries(counts).sort(([a], [b]) => (0, collate_1.stableCompare)(a, b));
|
|
29
30
|
if (!entries.length)
|
|
30
31
|
return "none";
|
|
31
32
|
return entries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
@@ -66,6 +66,7 @@ const coordinator_io_1 = require("./coordinator-io");
|
|
|
66
66
|
const topology_io_1 = require("./topology-io");
|
|
67
67
|
const multi_agent_operator_ux_1 = require("./multi-agent-operator-ux");
|
|
68
68
|
const trust_policy_io_1 = require("./trust-policy-io");
|
|
69
|
+
const collate_1 = require("../core/util/collate");
|
|
69
70
|
function countBy(values, key) {
|
|
70
71
|
const counts = {};
|
|
71
72
|
for (const value of values)
|
|
@@ -143,8 +144,8 @@ function summarizeTasks(tasks) {
|
|
|
143
144
|
* verb and operator panel both read. */
|
|
144
145
|
function summarizeOperatorCandidates(run) {
|
|
145
146
|
const counts = (0, candidate_scoring_io_1.summarizeCandidates)(run);
|
|
146
|
-
const candidates = [...(run.candidates || [])].sort((left, right) => left.id
|
|
147
|
-
const selections = [...(run.candidateSelections || [])].sort((left, right) => left.id
|
|
147
|
+
const candidates = [...(run.candidates || [])].sort((left, right) => (0, collate_1.stableCompare)(left.id, right.id));
|
|
148
|
+
const selections = [...(run.candidateSelections || [])].sort((left, right) => (0, collate_1.stableCompare)(left.id, right.id));
|
|
148
149
|
const commits = run.commits || [];
|
|
149
150
|
const selectedIds = new Set(selections.map((selection) => selection.candidateId));
|
|
150
151
|
const readyForCommit = selections
|
|
@@ -190,7 +191,7 @@ function summarizeOperatorFeedback(run) {
|
|
|
190
191
|
};
|
|
191
192
|
}
|
|
192
193
|
function summarizeOperatorCommits(run) {
|
|
193
|
-
const commits = [...(run.commits || [])].sort((left, right) => left.createdAt
|
|
194
|
+
const commits = [...(run.commits || [])].sort((left, right) => (0, collate_1.stableCompare)(left.createdAt, right.createdAt) || (0, collate_1.stableCompare)(left.id, right.id));
|
|
194
195
|
return {
|
|
195
196
|
total: commits.length,
|
|
196
197
|
verifierGated: commits.filter((c) => c.verifierGated).length,
|
|
@@ -200,7 +201,7 @@ function summarizeOperatorCommits(run) {
|
|
|
200
201
|
};
|
|
201
202
|
}
|
|
202
203
|
function summarizeOperatorWorkers(run) {
|
|
203
|
-
const workers = (run.workers || []).slice().sort((a, b) => a.id
|
|
204
|
+
const workers = (run.workers || []).slice().sort((a, b) => (0, collate_1.stableCompare)(a.id, b.id));
|
|
204
205
|
return {
|
|
205
206
|
total: workers.length,
|
|
206
207
|
byStatus: countBy(workers, (w) => w.status),
|
|
@@ -425,7 +426,7 @@ function buildOperatorGraph(run) {
|
|
|
425
426
|
addNode(node.id, node.kind, node.status, node.label, node.path);
|
|
426
427
|
for (const edge of blackboardGraph.edges)
|
|
427
428
|
addEdge(edge.from, edge.to, edge.label);
|
|
428
|
-
const sortedNodes = [...nodes.values()].sort((a, b) => a.kind
|
|
429
|
-
const sortedEdges = edges.slice().sort((a, b) => a.from
|
|
429
|
+
const sortedNodes = [...nodes.values()].sort((a, b) => (0, collate_1.stableCompare)(a.kind, b.kind) || (0, collate_1.stableCompare)(a.id, b.id));
|
|
430
|
+
const sortedEdges = edges.slice().sort((a, b) => (0, collate_1.stableCompare)(a.from, b.from) || (0, collate_1.stableCompare)(a.to, b.to) || (0, collate_1.stableCompare)(a.label || "", b.label || ""));
|
|
430
431
|
return { runId: run.id, nodes: sortedNodes, edges: sortedEdges };
|
|
431
432
|
}
|
|
@@ -552,84 +552,94 @@ function quickstartRun(args) {
|
|
|
552
552
|
}
|
|
553
553
|
function dispatchRun(args) {
|
|
554
554
|
const runId = String(args.runId);
|
|
555
|
-
|
|
556
|
-
//
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
555
|
+
// The whole load -> change -> save cycle holds the state.json lock so a
|
|
556
|
+
// concurrent dispatch/result on the same run cannot drop this update.
|
|
557
|
+
return (0, run_store_1.withRunStateLock)(runId, invocationCwd(args), (run) => {
|
|
558
|
+
// parseArgv keys long flags in kebab-case; accept camelCase as a fallback.
|
|
559
|
+
const flag = (kebab, camel) => {
|
|
560
|
+
const v = args[kebab] ?? args[camel];
|
|
561
|
+
return typeof v === "string" && v.trim() ? v : undefined;
|
|
562
|
+
};
|
|
563
|
+
const manifest = (0, dispatch_1.createDispatchManifest)(run, args.limit !== undefined ? Number(args.limit) : undefined, {
|
|
564
|
+
sandboxProfileId: typeof args.sandbox === "string" ? args.sandbox : undefined,
|
|
565
|
+
sandbox: typeof args.sandbox === "string" ? args.sandbox : undefined,
|
|
566
|
+
backendId: typeof args.backend === "string" ? args.backend : undefined,
|
|
567
|
+
multiAgentRunId: flag("multi-agent-run", "multiAgentRun"),
|
|
568
|
+
multiAgentGroupId: flag("multi-agent-group", "multiAgentGroup"),
|
|
569
|
+
multiAgentRoleId: flag("multi-agent-role", "multiAgentRole"),
|
|
570
|
+
multiAgentFanoutId: flag("multi-agent-fanout", "multiAgentFanout"),
|
|
571
|
+
});
|
|
572
|
+
if (manifest.dispatchId) {
|
|
573
|
+
(0, commit_1.commitState)(run, `dispatch:${manifest.dispatchId}`);
|
|
574
|
+
(0, run_store_1.saveCheckpoint)(run);
|
|
575
|
+
(0, report_1.writeReport)(run);
|
|
576
|
+
}
|
|
577
|
+
return manifest;
|
|
569
578
|
});
|
|
570
|
-
if (manifest.dispatchId) {
|
|
571
|
-
(0, commit_1.commitState)(run, `dispatch:${manifest.dispatchId}`);
|
|
572
|
-
(0, run_store_1.saveCheckpoint)(run);
|
|
573
|
-
(0, report_1.writeReport)(run);
|
|
574
|
-
}
|
|
575
|
-
return manifest;
|
|
576
579
|
}
|
|
577
580
|
function recordResultRun(args) {
|
|
578
581
|
const runId = String(args.runId);
|
|
579
582
|
const taskId = String(args.taskId);
|
|
580
583
|
const resultPath = String(args.resultPath);
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
584
|
+
// Two processes recording results for two tasks of the SAME run used to
|
|
585
|
+
// race: both loaded, and the later saveCheckpoint dropped the earlier
|
|
586
|
+
// task's completion. The lock now covers the whole cycle.
|
|
587
|
+
return (0, run_store_1.withRunStateLock)(runId, invocationCwd(args), (run) => {
|
|
588
|
+
const task = run.tasks.find((t) => t.id === taskId);
|
|
589
|
+
if (!task || !task.workerId)
|
|
590
|
+
throw new Error(`Unknown task id for run ${runId}: ${taskId}`);
|
|
591
|
+
const absolute = path.resolve(resultPath);
|
|
592
|
+
// A result path inside a system directory is never accepted (POLA): the
|
|
593
|
+
// operator file gets copied into the worker's result.md, so a /etc/passwd
|
|
594
|
+
// source would smuggle system content into a run. Byte-behavior port of the
|
|
595
|
+
// old build's recordResult system-directory blacklist.
|
|
596
|
+
if (/^\/(etc|bin|sbin|usr|Library|System|Applications|boot|dev|proc|sys|root|var\/log|var\/run)\//.test(absolute)) {
|
|
597
|
+
throw new Error(`Result path must not be a system directory: ${resultPath}`);
|
|
598
|
+
}
|
|
599
|
+
if (!fs.existsSync(absolute))
|
|
600
|
+
throw new Error(`Result file does not exist: ${resultPath}`);
|
|
601
|
+
const workerId = String(task.workerId);
|
|
602
|
+
// Host-attested `cw result <run> <task> <file>` intake: the operator hands CW
|
|
603
|
+
// an EXTERNAL result file that lives OUTSIDE the worker's read-only write
|
|
604
|
+
// boundary. The old task-level recordResult (lifecycle-operations.ts:279-280)
|
|
605
|
+
// COPIED that external file into the run's results area and recorded the
|
|
606
|
+
// internal path — it never ran the external path through validateSandboxWrite.
|
|
607
|
+
// v2 collapsed the two intakes into recordWorkerOutput, which sandbox-validates
|
|
608
|
+
// its input against the worker boundary, so a bare external path is rejected
|
|
609
|
+
// ("write path is outside sandbox profile <id>"). Restore the copy-in: stage
|
|
610
|
+
// the operator file at the worker's OWN result.md (which IS inside the write
|
|
611
|
+
// boundary), then record that internal path exactly like a driven worker.
|
|
612
|
+
const manifest = (0, worker_isolation_1.showWorkerManifest)(run, workerId);
|
|
613
|
+
fs.mkdirSync(path.dirname(manifest.resultPath), { recursive: true });
|
|
614
|
+
fs.copyFileSync(absolute, manifest.resultPath);
|
|
615
|
+
const output = (0, worker_isolation_1.recordWorkerOutput)(run, workerId, manifest.resultPath, {
|
|
616
|
+
requireAttestedTelemetry: (0, agent_config_1.resolveAgentConfig)(args).requireAttestedTelemetry,
|
|
617
|
+
allowUnattested: Boolean(args.allowUnattested ?? args["allow-unattested"]),
|
|
618
|
+
});
|
|
619
|
+
// Host-attested token usage (v0.1.31): record it verbatim as provenance when
|
|
620
|
+
// the operator supplied `--usage-*` flags; CW never synthesizes usage. The old
|
|
621
|
+
// task-level recordResult set `task.usage = usage` (lifecycle-operations.ts:286)
|
|
622
|
+
// and its unit was the TASK. v2 records through recordWorkerOutput, which gives
|
|
623
|
+
// the worker an `output` record — so the observability usage UNIT becomes the
|
|
624
|
+
// WORKER (deriveUsageTotals reads worker.usage for workers with output, and
|
|
625
|
+
// EXCLUDES that task). Attach the usage to the worker scope so the report counts
|
|
626
|
+
// it as an attested unit; also stamp task.usage for byte-parity with the old
|
|
627
|
+
// task-level record.
|
|
628
|
+
const usage = (0, observability_1.parseUsageFromArgs)(args, new Date().toISOString());
|
|
629
|
+
if (usage) {
|
|
630
|
+
task.usage = usage;
|
|
631
|
+
const scope = (0, worker_isolation_1.getWorkerScope)(run, workerId);
|
|
632
|
+
if (scope)
|
|
633
|
+
scope.usage = usage;
|
|
634
|
+
}
|
|
635
|
+
// Byte-exact to the old build's orchestrator recordWorkerOutput()
|
|
636
|
+
// wrapper: an accepted result is its own checkpoint commit, not just a
|
|
637
|
+
// bare saveCheckpoint (SPEC/pipeline-run.md's persist-ordering rule).
|
|
638
|
+
(0, commit_1.commitState)(run, `worker:${workerId}:result`);
|
|
639
|
+
(0, run_store_1.saveCheckpoint)(run);
|
|
640
|
+
(0, report_1.writeReport)(run);
|
|
641
|
+
return output;
|
|
642
|
+
});
|
|
633
643
|
}
|
|
634
644
|
/** `cw commit <run-id>` — byte-exact port of the old build's
|
|
635
645
|
* `orchestrator/lifecycle-operations.ts`'s `commit()`: the CLI/MCP
|
|
@@ -106,6 +106,7 @@ const node_store_1 = require("./node-store");
|
|
|
106
106
|
const node_snapshot_1 = require("../core/state/node-snapshot");
|
|
107
107
|
const node_projection_1 = require("../core/state/node-projection");
|
|
108
108
|
const hash_1 = require("../core/hash");
|
|
109
|
+
const collate_1 = require("../core/util/collate");
|
|
109
110
|
const run_registry_io_1 = require("./run-registry-io");
|
|
110
111
|
// ---------------------------------------------------------------------------
|
|
111
112
|
// Content addressing + byte measurement (in-process, no `du`) — carried
|
|
@@ -1287,7 +1288,7 @@ function readCloneEntries(root) {
|
|
|
1287
1288
|
bytes: dirSize(dir),
|
|
1288
1289
|
});
|
|
1289
1290
|
}
|
|
1290
|
-
entries.sort((a, b) => (a.fetchedAt || ""
|
|
1291
|
+
entries.sort((a, b) => (0, collate_1.stableCompare)(a.fetchedAt || "", b.fetchedAt || ""));
|
|
1291
1292
|
return entries;
|
|
1292
1293
|
}
|
|
1293
1294
|
/** `cw clones list` — every cached remote checkout with its origin,
|
package/dist/shell/report.js
CHANGED
|
@@ -59,13 +59,14 @@ const telemetry_ledger_io_1 = require("./telemetry-ledger-io");
|
|
|
59
59
|
const multi_agent_io_1 = require("./multi-agent-io");
|
|
60
60
|
const coordinator_io_1 = require("./coordinator-io");
|
|
61
61
|
const multi_agent_operator_ux_1 = require("./multi-agent-operator-ux");
|
|
62
|
+
const collate_1 = require("../core/util/collate");
|
|
62
63
|
function formatInputList(value) {
|
|
63
64
|
if (Array.isArray(value))
|
|
64
65
|
return value.join("; ");
|
|
65
66
|
return value ? String(value) : "";
|
|
66
67
|
}
|
|
67
68
|
function formatCounts(counts) {
|
|
68
|
-
const entries = Object.entries(counts).sort(([a], [b]) =>
|
|
69
|
+
const entries = Object.entries(counts).sort(([a], [b]) => (0, collate_1.stableCompare)(a, b));
|
|
69
70
|
if (!entries.length)
|
|
70
71
|
return "none";
|
|
71
72
|
return entries.map(([k, v]) => `${k}=${v}`).join(", ");
|
package/dist/shell/run-store.js
CHANGED
|
@@ -50,6 +50,7 @@ exports.loadRunStateFile = loadRunStateFile;
|
|
|
50
50
|
exports.checkRunStateFile = checkRunStateFile;
|
|
51
51
|
exports.migrateRunStateFile = migrateRunStateFile;
|
|
52
52
|
exports.loadRunFromCwd = loadRunFromCwd;
|
|
53
|
+
exports.withRunStateLock = withRunStateLock;
|
|
53
54
|
exports.saveCheckpoint = saveCheckpoint;
|
|
54
55
|
exports.compactCheckpoint = compactCheckpoint;
|
|
55
56
|
exports.createRun = createRun;
|
|
@@ -112,6 +113,22 @@ function loadRunFromCwd(runId, cwd = process.cwd()) {
|
|
|
112
113
|
}
|
|
113
114
|
return result.run;
|
|
114
115
|
}
|
|
116
|
+
/** Hold the state.json lock over a WHOLE load -> change -> save cycle.
|
|
117
|
+
* A bare loadRunFromCwd + saveCheckpoint pair leaves a window where two
|
|
118
|
+
* processes both load the same state and the later save silently drops
|
|
119
|
+
* the earlier change (the same lost-update class PR #339 fixed for
|
|
120
|
+
* queue.json / triggers.json). `fn` gets the run loaded UNDER the lock;
|
|
121
|
+
* saveCheckpoint calls inside `fn` re-enter the same lock (withFileLock
|
|
122
|
+
* is re-entrant in-process) and write exactly as before. The probe load
|
|
123
|
+
* runs BEFORE the lock so an unknown run id throws the exact
|
|
124
|
+
* loadRunFromCwd error without first creating the run directory as a
|
|
125
|
+
* lock-file side effect — and it supplies `paths.state`, the same lock
|
|
126
|
+
* target saveCheckpoint uses. Keep `fn` short: a critical section past
|
|
127
|
+
* 30s can be stolen as a stale lock. */
|
|
128
|
+
function withRunStateLock(runId, cwd, fn) {
|
|
129
|
+
const probe = loadRunFromCwd(runId, cwd);
|
|
130
|
+
return (0, fs_atomic_1.withFileLock)(probe.paths.state, () => fn(loadRunFromCwd(runId, cwd)));
|
|
131
|
+
}
|
|
115
132
|
/** state.json is the single source of truth — set `updatedAt`, then write
|
|
116
133
|
* it DURABLY with a lock so concurrent processes never lose an update. */
|
|
117
134
|
function saveCheckpoint(run) {
|
|
@@ -66,6 +66,7 @@ const report_1 = require("../core/state/state-explosion/report");
|
|
|
66
66
|
Object.defineProperty(exports, "buildStateExplosionReport", { enumerable: true, get: function () { return report_1.buildStateExplosionReport; } });
|
|
67
67
|
const size_2 = require("../core/state/state-explosion/size");
|
|
68
68
|
const multi_agent_operator_ux_1 = require("./multi-agent-operator-ux");
|
|
69
|
+
const collate_1 = require("../core/util/collate");
|
|
69
70
|
function summariesDir(run) {
|
|
70
71
|
return path.join(run.paths.runDir, "summaries");
|
|
71
72
|
}
|
|
@@ -134,7 +135,7 @@ function refreshStateExplosionSummaries(run, options = {}) {
|
|
|
134
135
|
status: "valid",
|
|
135
136
|
deterministic: true,
|
|
136
137
|
nextAction: `node scripts/cw.js summary show ${run.id}`,
|
|
137
|
-
entries: entries.sort((a, b) => a.id
|
|
138
|
+
entries: entries.sort((a, b) => (0, collate_1.stableCompare)(a.id, b.id)),
|
|
138
139
|
views,
|
|
139
140
|
paths: { summariesDir: dir, indexPath: path.join(dir, "index.json"), reportPath },
|
|
140
141
|
};
|
|
@@ -63,6 +63,7 @@ const hash_2 = require("../core/hash");
|
|
|
63
63
|
const topo = __importStar(require("../core/multi-agent/topology"));
|
|
64
64
|
const multi_agent_io_1 = require("./multi-agent-io");
|
|
65
65
|
const coordinator_io_1 = require("./coordinator-io");
|
|
66
|
+
const collate_1 = require("../core/util/collate");
|
|
66
67
|
function topologyRoot(run) {
|
|
67
68
|
return run.paths.topologiesDir || path.join(run.paths.runDir, "topologies");
|
|
68
69
|
}
|
|
@@ -307,7 +308,7 @@ function buildTopologyGraph(run) {
|
|
|
307
308
|
return topo.buildTopologyGraphFromRuns(run.id, state.runs, (id) => topologyRunPath(run, id));
|
|
308
309
|
}
|
|
309
310
|
function formatTopologyCounts(counts) {
|
|
310
|
-
const entries = Object.entries(counts).sort(([a], [b]) =>
|
|
311
|
+
const entries = Object.entries(counts).sort(([a], [b]) => (0, collate_1.stableCompare)(a, b));
|
|
311
312
|
if (!entries.length)
|
|
312
313
|
return "none";
|
|
313
314
|
return entries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
@@ -58,6 +58,7 @@ exports.TRUST_AUDIT_SCHEMA_VERSION = void 0;
|
|
|
58
58
|
exports.ensureTrustAudit = ensureTrustAudit;
|
|
59
59
|
exports.trustAuditGenesis = trustAuditGenesis;
|
|
60
60
|
exports.listTrustAuditEvents = listTrustAuditEvents;
|
|
61
|
+
exports.trustAuditHead = trustAuditHead;
|
|
61
62
|
exports.verifyTrustAudit = verifyTrustAudit;
|
|
62
63
|
exports.recordTrustAuditEvent = recordTrustAuditEvent;
|
|
63
64
|
exports.recordSandboxPathDecision = recordSandboxPathDecision;
|
|
@@ -136,6 +137,21 @@ function readEventsRaw(eventLogPath) {
|
|
|
136
137
|
function listTrustAuditEvents(run) {
|
|
137
138
|
return readEventsRaw(ensureTrustAudit(run).eventLogPath);
|
|
138
139
|
}
|
|
140
|
+
/** The current head of a run's trust-audit chain: the hash the NEXT
|
|
141
|
+
* appended event will link from (genesis when the log is empty), plus
|
|
142
|
+
* the event count. Read-only projection over existing data. Capture it
|
|
143
|
+
* (e.g. right after a run, or at export time) and later hand it to
|
|
144
|
+
* `verifyTrustAudit`'s anchor / `cw audit verify --expect-head` to
|
|
145
|
+
* re-prove the log was not shortened since the capture. */
|
|
146
|
+
function trustAuditHead(run) {
|
|
147
|
+
const audit = ensureTrustAudit(run);
|
|
148
|
+
const events = readEventsRaw(audit.eventLogPath);
|
|
149
|
+
let head = trustAuditGenesis(run.id);
|
|
150
|
+
for (const event of events) {
|
|
151
|
+
head = event.eventHash !== undefined ? event.eventHash : computeEventHash(event);
|
|
152
|
+
}
|
|
153
|
+
return { eventCount: events.length, headHash: head };
|
|
154
|
+
}
|
|
139
155
|
/** Re-prove the run's trust-audit chain: prevEventHash linkage (append
|
|
140
156
|
* order) + per-event hash recompute. A corrupt line, an edited event,
|
|
141
157
|
* or a removed event flips verified=false. Legacy events without a
|
|
@@ -147,8 +163,14 @@ function listTrustAuditEvents(run) {
|
|
|
147
163
|
* era) or all-legacy (pre-chain). An unchained (eventHash-less) line
|
|
148
164
|
* mixed into an otherwise-chained log is a forgery attempt — dropping
|
|
149
165
|
* the hash to be waved through as "legacy" — so it fails with
|
|
150
|
-
* `trust-audit-unchained-event`, never silently accepted.
|
|
151
|
-
|
|
166
|
+
* `trust-audit-unchained-event`, never silently accepted.
|
|
167
|
+
*
|
|
168
|
+
* ANCHOR (optional): the walk alone cannot see tail truncation — see
|
|
169
|
+
* TrustAuditAnchor. With an anchor, the head-hash trail (genesis plus
|
|
170
|
+
* the hash after each event) must contain `expectHead`, and the log
|
|
171
|
+
* must reach `expectCount` events; a shortfall fails closed with
|
|
172
|
+
* `trust-audit-truncated`. Without an anchor, behavior is unchanged. */
|
|
173
|
+
function verifyTrustAudit(run, anchor) {
|
|
152
174
|
const audit = ensureTrustAudit(run);
|
|
153
175
|
const { events, corruptLines } = readEventsRawCounted(audit.eventLogPath);
|
|
154
176
|
const checks = [];
|
|
@@ -158,12 +180,14 @@ function verifyTrustAudit(run) {
|
|
|
158
180
|
let chained = 0;
|
|
159
181
|
let unchained = 0;
|
|
160
182
|
let expectedPrev = trustAuditGenesis(run.id);
|
|
183
|
+
const headTrail = new Set([expectedPrev]);
|
|
161
184
|
for (let i = 0; i < events.length; i++) {
|
|
162
185
|
const event = events[i];
|
|
163
186
|
const recomputed = computeEventHash(event);
|
|
164
187
|
if (event.eventHash === undefined) {
|
|
165
188
|
unchained += 1;
|
|
166
189
|
expectedPrev = recomputed; // advance the chain over legacy events
|
|
190
|
+
headTrail.add(expectedPrev);
|
|
167
191
|
continue;
|
|
168
192
|
}
|
|
169
193
|
chained += 1;
|
|
@@ -176,12 +200,27 @@ function verifyTrustAudit(run) {
|
|
|
176
200
|
checks.push({ name: `chain-link[${i}]`, pass: false, code: "trust-audit-chain-broken" });
|
|
177
201
|
}
|
|
178
202
|
expectedPrev = event.eventHash;
|
|
203
|
+
headTrail.add(expectedPrev);
|
|
179
204
|
}
|
|
180
205
|
// Era rule: a log with ANY chained event must have EVERY event chained.
|
|
181
206
|
if (chained > 0 && unchained > 0) {
|
|
182
207
|
verified = false;
|
|
183
208
|
checks.push({ name: "unchained-events", pass: false, code: "trust-audit-unchained-event" });
|
|
184
209
|
}
|
|
210
|
+
// Anchor rule: the captured head must still be ON the chain, and the log
|
|
211
|
+
// must be at least as long as it was at capture time. A truncated-then-
|
|
212
|
+
// appended log fails the head check (new events link from an earlier
|
|
213
|
+
// point, so the old head is no longer in the trail).
|
|
214
|
+
if (anchor) {
|
|
215
|
+
if (anchor.expectCount !== undefined && events.length < anchor.expectCount) {
|
|
216
|
+
verified = false;
|
|
217
|
+
checks.push({ name: "anchor-count", pass: false, code: "trust-audit-truncated" });
|
|
218
|
+
}
|
|
219
|
+
if (anchor.expectHead !== undefined && !headTrail.has(anchor.expectHead)) {
|
|
220
|
+
verified = false;
|
|
221
|
+
checks.push({ name: "anchor-head", pass: false, code: "trust-audit-truncated" });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
185
224
|
return { present: events.length > 0, verified, eventCount: events.length, chained, unchained, corruptLines, checks };
|
|
186
225
|
}
|
|
187
226
|
function unique(values) {
|
package/dist/shell/worker-cli.js
CHANGED
|
@@ -51,6 +51,7 @@ const drive_1 = require("./drive");
|
|
|
51
51
|
const commit_1 = require("./commit");
|
|
52
52
|
const report_1 = require("./report");
|
|
53
53
|
const operator_ux_1 = require("./operator-ux");
|
|
54
|
+
const agent_config_1 = require("./agent-config");
|
|
54
55
|
function cwdFor(args) {
|
|
55
56
|
return typeof args.cwd === "string" && args.cwd.trim() ? path.resolve(args.cwd) : process.cwd();
|
|
56
57
|
}
|
|
@@ -60,6 +61,10 @@ function req(value, label) {
|
|
|
60
61
|
throw new Error(`Missing ${label}`);
|
|
61
62
|
return s;
|
|
62
63
|
}
|
|
64
|
+
/** `--allow-unattested` (CLI: dashed key; MCP: allowUnattested). */
|
|
65
|
+
function allowUnattestedOption(args) {
|
|
66
|
+
return Boolean(args.allowUnattested ?? args["allow-unattested"]);
|
|
67
|
+
}
|
|
63
68
|
function workerListCli(args) {
|
|
64
69
|
const run = (0, run_store_1.loadRunFromCwd)(req(args.runId, "run id"), cwdFor(args));
|
|
65
70
|
return (0, worker_isolation_1.listWorkerScopes)(run, { status: typeof args.status === "string" ? args.status : undefined });
|
|
@@ -92,7 +97,10 @@ function workerManifestCli(args) {
|
|
|
92
97
|
* same steps itself around the bare accept, so it never routes through here. */
|
|
93
98
|
function workerOutputCli(args) {
|
|
94
99
|
const run = (0, run_store_1.loadRunFromCwd)(req(args.runId, "run id"), cwdFor(args));
|
|
95
|
-
(0, worker_isolation_1.recordWorkerOutput)(run, req(args.workerId, "worker id"), req(args.resultPath, "result file"), {
|
|
100
|
+
(0, worker_isolation_1.recordWorkerOutput)(run, req(args.workerId, "worker id"), req(args.resultPath, "result file"), {
|
|
101
|
+
requireAttestedTelemetry: (0, agent_config_1.resolveAgentConfig)(args).requireAttestedTelemetry,
|
|
102
|
+
allowUnattested: allowUnattestedOption(args),
|
|
103
|
+
});
|
|
96
104
|
run.loopStage = "observe";
|
|
97
105
|
(0, dispatch_1.updatePhaseStatuses)(run);
|
|
98
106
|
(0, drive_1.maybeExpandLoop)(run);
|