cool-workflow 0.2.0 → 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/README.md +7 -5
- package/apps/architecture-review/app.json +2 -2
- package/apps/architecture-review/workflow.js +12 -12
- 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 +20 -88
- package/dist/cli/parseargv.js +6 -3
- package/dist/core/capability-data.js +270 -0
- package/dist/core/capability-table.js +15 -3519
- package/dist/core/format/help.js +30 -7
- package/dist/core/hash.js +1 -1
- package/dist/core/multi-agent/candidate-scoring.js +1 -1
- package/dist/core/multi-agent/collaboration.js +3 -2
- package/dist/core/multi-agent/coordinator.js +4 -3
- package/dist/core/multi-agent/eval-replay.js +1 -1
- package/dist/core/multi-agent/runtime.js +2 -2
- package/dist/core/multi-agent/trust-policy.js +1 -1
- package/dist/core/pipeline/commit-gate.js +2 -1
- package/dist/core/pipeline/contract.js +1 -1
- package/dist/core/pipeline/drive-decide.js +1 -1
- package/dist/core/state/contract-migration.js +1 -1
- package/dist/core/state/migrations.js +2 -2
- package/dist/core/state/node-snapshot.js +1 -1
- package/dist/core/state/state-explosion/digest.js +3 -2
- package/dist/core/state/state-explosion/graph.js +13 -12
- package/dist/core/state/state-explosion/helpers.js +5 -4
- package/dist/core/state/state-explosion/report.js +1 -1
- package/dist/core/state/state-explosion/size.js +1 -1
- package/dist/core/state/state-node.js +2 -2
- package/dist/core/state/types.js +1 -1
- package/dist/core/trust/ledger.js +3 -2
- package/dist/core/trust/telemetry-attestation.js +3 -3
- package/dist/core/trust/telemetry-ledger.js +2 -2
- 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/core/workflow-apps/app-schema.js +1 -1
- package/dist/mcp/dispatch.js +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/shell/agent-config.js +1 -1
- package/dist/shell/audit-cli.js +46 -2
- package/dist/shell/commit-summary.js +2 -1
- package/dist/shell/doctor.js +17 -4
- package/dist/shell/drive.js +47 -19
- package/dist/shell/evidence-reasoning.js +4 -3
- package/dist/shell/execution-backend/agent.js +42 -10
- package/dist/shell/execution-backend/ci.js +1 -1
- package/dist/shell/execution-backend/container.js +1 -1
- package/dist/shell/execution-backend/envelopes.js +1 -1
- package/dist/shell/execution-backend/local.js +1 -1
- package/dist/shell/execution-backend/probes.js +1 -1
- package/dist/shell/execution-backend/registry.js +3 -2
- package/dist/shell/execution-backend/remote.js +1 -1
- package/dist/shell/execution-backend/types.js +0 -9
- package/dist/shell/fs-atomic.js +15 -2
- package/dist/shell/ledger-io.js +1 -1
- package/dist/shell/metrics-cli.js +4 -2
- package/dist/shell/multi-agent-host.js +1 -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 +3 -2
- package/dist/shell/registry-cli.js +15 -0
- package/dist/shell/report.js +2 -1
- package/dist/shell/run-registry-io.js +1 -1
- package/dist/shell/run-store.js +17 -0
- package/dist/shell/sandbox-profile.js +1 -1
- package/dist/shell/scheduler-io.js +46 -37
- package/dist/shell/scheduling-io.js +32 -22
- package/dist/shell/state-explosion-cli.js +2 -1
- package/dist/shell/telemetry-ledger-io.js +1 -1
- package/dist/shell/term.js +1 -1
- package/dist/shell/topology-io.js +2 -1
- package/dist/shell/trust-audit.js +45 -5
- package/dist/shell/workbench-host.js +9 -1
- package/dist/shell/workbench.js +6 -11
- 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 +4 -0
- package/docs/cli-mcp-parity.7.md +9 -2
- package/docs/contract-migration-tooling.7.md +4 -0
- package/docs/control-plane-scheduling.7.md +4 -0
- package/docs/durable-state-and-locking.7.md +24 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
- package/docs/execution-backends.7.md +4 -0
- package/docs/index.md +1 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
- package/docs/multi-agent-eval-replay-harness.7.md +4 -0
- package/docs/multi-agent-operator-ux.7.md +4 -0
- package/docs/node-snapshot-diff-replay.7.md +4 -0
- package/docs/observability-cost-accounting.7.md +6 -1
- package/docs/project-index.md +30 -5
- package/docs/real-execution-backends.7.md +4 -0
- package/docs/release-and-migration.7.md +4 -0
- package/docs/release-tooling.7.md +4 -0
- package/docs/remote-source-review.7.md +4 -4
- package/docs/report-verifiable-bundle.7.md +1 -1
- package/docs/run-registry-control-plane.7.md +4 -0
- package/docs/run-retention-reclamation.7.md +4 -0
- package/docs/security-trust-hardening.7.md +33 -1
- package/docs/state-explosion-management.7.md +4 -0
- package/docs/team-collaboration.7.md +4 -0
- package/docs/trust-audit-anchor.7.md +69 -0
- package/docs/web-desktop-workbench.7.md +15 -1
- package/manifest/plugin.manifest.json +1 -1
- package/package.json +3 -1
- package/scripts/agents/agent-adapter-core.js +22 -2
- package/scripts/agents/claude-p-agent.js +8 -3
- package/scripts/agents/gemini-agent.js +5 -1
- package/scripts/agents/opencode-agent.js +5 -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
|
@@ -62,6 +62,7 @@ const coordinator_io_1 = require("./coordinator-io");
|
|
|
62
62
|
const multi_agent_io_1 = require("./multi-agent-io");
|
|
63
63
|
const topology_io_1 = require("./topology-io");
|
|
64
64
|
const trust_audit_1 = require("./trust-audit");
|
|
65
|
+
const collate_1 = require("../core/util/collate");
|
|
65
66
|
function maOf(run) {
|
|
66
67
|
return run.multiAgent || {};
|
|
67
68
|
}
|
|
@@ -275,7 +276,7 @@ function deriveDependencies(run) {
|
|
|
275
276
|
for (const commit of commitsOf(run)) {
|
|
276
277
|
add(commit.selectionId ? `${run.id}:selection:${commit.selectionId}` : undefined, String(commit.stateNodeId || `${run.id}:commit:${commit.id}`), "commits", commit.verifierGated ? "committed" : "checkpoint");
|
|
277
278
|
}
|
|
278
|
-
return rows.filter(uniqueById).sort((left, right) => left.from
|
|
279
|
+
return rows.filter(uniqueById).sort((left, right) => (0, collate_1.stableCompare)(left.from, right.from) || (0, collate_1.stableCompare)(left.to, right.to));
|
|
279
280
|
}
|
|
280
281
|
function deriveFailures(run, dependencies) {
|
|
281
282
|
const rows = [];
|
|
@@ -337,7 +338,7 @@ function deriveFailures(run, dependencies) {
|
|
|
337
338
|
const readySelection = selectionsOf(run).find((selection) => !commitsOf(run).some((commit) => commit.selectionId === selection.id && commit.verifierGated));
|
|
338
339
|
if (readySelection)
|
|
339
340
|
add(String(readySelection.id), "commit-gate", "not-ready", `selection ${readySelection.id} has no verifier-gated commit`, `node scripts/cw.js commit ${run.id} --selection ${readySelection.id} --reason "<verified rationale>"`, readySelection.candidateId);
|
|
340
|
-
return rows.filter(uniqueByFailure).sort((left, right) => left.kind
|
|
341
|
+
return rows.filter(uniqueByFailure).sort((left, right) => (0, collate_1.stableCompare)(left.kind, right.kind) || (0, collate_1.stableCompare)(left.id, right.id));
|
|
341
342
|
}
|
|
342
343
|
function deriveEvidence(run) {
|
|
343
344
|
const rows = new Map();
|
|
@@ -442,7 +443,7 @@ function deriveEvidence(run) {
|
|
|
442
443
|
return [...rows.values()]
|
|
443
444
|
.map(normalizeEvidenceStatus)
|
|
444
445
|
.map(withDisposition)
|
|
445
|
-
.sort((left, right) => statusRank(left.status) - statusRank(right.status) || left.id
|
|
446
|
+
.sort((left, right) => statusRank(left.status) - statusRank(right.status) || (0, collate_1.stableCompare)(left.id, right.id));
|
|
446
447
|
}
|
|
447
448
|
function formatDependencies(rows) {
|
|
448
449
|
const lines = ["Dependencies"];
|
|
@@ -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
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// shell/reclamation-io.ts — gc plan/run/verify's write-ahead reclamation
|
|
3
3
|
// transaction, plus the orphan-run sweep and the clone cache gc.
|
|
4
4
|
//
|
|
5
|
-
// MILESTONE 10 (
|
|
5
|
+
// MILESTONE 10 (docs/rebuild/PLAN.md build order, step 10). Byte-exact port of the
|
|
6
6
|
// old build's src/reclamation.ts + src/reclamation/hash.ts +
|
|
7
7
|
// src/run-registry/{gc,orphans}.ts + src/clones.ts. Reuses
|
|
8
8
|
// shell/fs-atomic.ts's `withFileLock` directly (no reimplementation, per
|
|
@@ -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,
|
|
@@ -58,6 +58,7 @@ exports.routineCreateCli = routineCreateCli;
|
|
|
58
58
|
exports.routineListCli = routineListCli;
|
|
59
59
|
exports.routineDeleteCli = routineDeleteCli;
|
|
60
60
|
exports.routineFireCli = routineFireCli;
|
|
61
|
+
exports.resolveRoutineFirePayload = resolveRoutineFirePayload;
|
|
61
62
|
exports.routineEventsCli = routineEventsCli;
|
|
62
63
|
exports.registryRefreshCli = registryRefreshCli;
|
|
63
64
|
exports.registryShowCli = registryShowCli;
|
|
@@ -79,6 +80,7 @@ exports.orphansListCli = orphansListCli;
|
|
|
79
80
|
exports.orphansGcCli = orphansGcCli;
|
|
80
81
|
exports.clonesListCli = clonesListCli;
|
|
81
82
|
exports.clonesGcCli = clonesGcCli;
|
|
83
|
+
const fs = __importStar(require("node:fs"));
|
|
82
84
|
const path = __importStar(require("node:path"));
|
|
83
85
|
const run_registry_io_1 = require("./run-registry-io");
|
|
84
86
|
const pipeline_1 = require("./pipeline");
|
|
@@ -149,6 +151,19 @@ function routineDeleteCli(id, options = {}) {
|
|
|
149
151
|
function routineFireCli(kind, payload, options = {}) {
|
|
150
152
|
return new scheduler_io_1.RoutineTriggerBridge(resolveCwd(options)).fire(kind, payload);
|
|
151
153
|
}
|
|
154
|
+
/** Resolves a `routine fire` payload: a `--payload-path`/positional file wins
|
|
155
|
+
* (parsed as JSON) over the raw CLI/MCP options bag. The file read lives
|
|
156
|
+
* here, in the shell layer, not in core/capability-table.ts. */
|
|
157
|
+
function resolveRoutineFirePayload(payloadPath, options) {
|
|
158
|
+
if (!payloadPath)
|
|
159
|
+
return options;
|
|
160
|
+
try {
|
|
161
|
+
return JSON.parse(fs.readFileSync(payloadPath, "utf8"));
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
throw new Error(`Failed to parse payload file "${payloadPath}": ${String((e && e.message) || e)}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
152
167
|
function routineEventsCli(id, options = {}) {
|
|
153
168
|
return new scheduler_io_1.RoutineTriggerBridge(resolveCwd(options)).events(id);
|
|
154
169
|
}
|
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(", ");
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// shell/run-registry-io.ts — the on-disk run registry: index/queue/history
|
|
3
3
|
// persistence, lifecycle derivation, search/resume/archive/rerun.
|
|
4
4
|
//
|
|
5
|
-
// MILESTONE 10 (
|
|
5
|
+
// MILESTONE 10 (docs/rebuild/PLAN.md build order, step 10). Byte-exact port of the
|
|
6
6
|
// old build's src/run-registry.ts + src/run-registry/{derive,policy,
|
|
7
7
|
// queue}.ts. The registry is a DERIVED, rebuildable index over each repo's
|
|
8
8
|
// `.cw/runs/<id>/state.json` (the single source of truth, never mutated
|
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) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// shell/sandbox-profile.ts — the 4 bundled sandbox profiles, resolution,
|
|
3
3
|
// validation, and the runtime read/write/command/network checks.
|
|
4
4
|
//
|
|
5
|
-
// MILESTONE 5 (
|
|
5
|
+
// MILESTONE 5 (docs/rebuild/PLAN.md build order, step 5). Byte-exact port of
|
|
6
6
|
// plugins/cool-workflow/src/sandbox-profile.ts.
|
|
7
7
|
//
|
|
8
8
|
// Evidence: SPEC/execution-backend.md "Sandbox profiles".
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// shell/scheduler-io.ts — Scheduler (cw schedule/loop), the daemon tick
|
|
3
3
|
// (cw schedule daemon), and routine triggers (cw routine).
|
|
4
4
|
//
|
|
5
|
-
// MILESTONE 10 (
|
|
5
|
+
// MILESTONE 10 (docs/rebuild/PLAN.md build order, step 10). Byte-exact port of the
|
|
6
6
|
// old build's src/scheduler.ts + src/daemon.ts + src/triggers.ts. Reuses
|
|
7
7
|
// shell/fs-atomic.ts's `withFileLock`/`readJson`/`writeJson`/`safeFileName`
|
|
8
8
|
// directly (no reimplementation).
|
|
@@ -387,51 +387,60 @@ class RoutineTriggerBridge {
|
|
|
387
387
|
this.storePath = path.join(this.cwd, ".cw", "routines", "triggers.json");
|
|
388
388
|
this.payloadsDir = path.join(this.cwd, ".cw", "routines", "payloads");
|
|
389
389
|
}
|
|
390
|
+
locked(fn) {
|
|
391
|
+
return (0, fs_atomic_1.withFileLock)(this.storePath, fn);
|
|
392
|
+
}
|
|
390
393
|
create(options) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
394
|
+
return this.locked(() => {
|
|
395
|
+
const now = new Date().toISOString();
|
|
396
|
+
const store = this.load();
|
|
397
|
+
// Monotonic id, NOT triggers.length: delete shrinks the collection, so
|
|
398
|
+
// a length-based seq would reuse a live id after delete+create.
|
|
399
|
+
const seq = (store.nextTriggerSeq || 0) + 1;
|
|
400
|
+
store.nextTriggerSeq = seq;
|
|
401
|
+
const trigger = {
|
|
402
|
+
id: createTriggerId(normalizeTriggerKind(options.kind), seq),
|
|
403
|
+
kind: normalizeTriggerKind(options.kind),
|
|
404
|
+
createdAt: now,
|
|
405
|
+
updatedAt: now,
|
|
406
|
+
source: String(options.source || options.kind || "api"),
|
|
407
|
+
prompt: requiredString(options.prompt, "prompt"),
|
|
408
|
+
workflowId: stringOption(options.workflowId),
|
|
409
|
+
runId: stringOption(options.runId),
|
|
410
|
+
match: parseJsonObject(options.match),
|
|
411
|
+
metadata: parseJsonObject(options.metadata),
|
|
412
|
+
};
|
|
413
|
+
store.triggers.push(trigger);
|
|
414
|
+
this.save(store);
|
|
415
|
+
return trigger;
|
|
416
|
+
});
|
|
412
417
|
}
|
|
413
418
|
list(kind) {
|
|
414
419
|
const store = this.load();
|
|
415
420
|
return kind ? store.triggers.filter((trigger) => trigger.kind === kind) : store.triggers;
|
|
416
421
|
}
|
|
417
422
|
delete(id) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
+
return this.locked(() => {
|
|
424
|
+
const store = this.load();
|
|
425
|
+
const before = store.triggers.length;
|
|
426
|
+
store.triggers = store.triggers.filter((trigger) => trigger.id !== id);
|
|
427
|
+
this.save(store);
|
|
428
|
+
return { deleted: store.triggers.length !== before, id };
|
|
429
|
+
});
|
|
423
430
|
}
|
|
424
431
|
fire(kind, payload) {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
432
|
+
return this.locked(() => {
|
|
433
|
+
const normalizedKind = normalizeTriggerKind(kind);
|
|
434
|
+
const store = this.load();
|
|
435
|
+
const now = new Date().toISOString();
|
|
436
|
+
const base = store.events.length;
|
|
437
|
+
const events = store.triggers
|
|
438
|
+
.filter((trigger) => trigger.kind === normalizedKind)
|
|
439
|
+
.map((trigger, index) => this.createEvent(trigger, payload, now, base + index + 1));
|
|
440
|
+
store.events.push(...events);
|
|
441
|
+
this.save(store);
|
|
442
|
+
return events;
|
|
443
|
+
});
|
|
435
444
|
}
|
|
436
445
|
events(triggerId) {
|
|
437
446
|
const store = this.load();
|