cool-workflow 0.2.1 → 0.2.3
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 +29 -91
- package/dist/cli/entry.js +41 -1
- package/dist/cli/io.js +6 -2
- package/dist/cli/parseargv.js +6 -2
- package/dist/core/capability-data.js +271 -0
- package/dist/core/capability-table.js +16 -3504
- package/dist/core/format/completion.js +68 -0
- package/dist/core/format/help.js +50 -8
- package/dist/core/format/safe-json.js +73 -0
- package/dist/core/format/state-explosion-text.js +1 -1
- package/dist/core/multi-agent/candidate-scoring.js +5 -1
- package/dist/core/multi-agent/collaboration.js +6 -5
- package/dist/core/multi-agent/coordinator.js +9 -8
- package/dist/core/multi-agent/runtime.js +4 -4
- package/dist/core/multi-agent/topology.js +3 -3
- package/dist/core/pipeline/commit-gate.js +2 -1
- package/dist/core/pipeline/dispatch.js +18 -4
- package/dist/core/pipeline/drive-decide.js +2 -1
- package/dist/core/state/migrations.js +16 -1
- package/dist/core/state/state-explosion/digest.js +16 -15
- package/dist/core/state/state-explosion/graph.js +26 -19
- package/dist/core/state/state-explosion/helpers.js +2 -1
- package/dist/core/state/state-explosion/report.js +6 -6
- 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/util/numeric-flag.js +40 -0
- package/dist/core/version.js +1 -1
- package/dist/mcp/server.js +99 -11
- package/dist/shell/audit-cli.js +99 -23
- package/dist/shell/commit-summary.js +2 -1
- package/dist/shell/coordinator-io.js +73 -13
- package/dist/shell/dispatch.js +1 -1
- package/dist/shell/doctor.js +80 -1
- package/dist/shell/drive.js +269 -59
- package/dist/shell/eval-text.js +2 -2
- package/dist/shell/evidence-reasoning.js +8 -7
- package/dist/shell/execution-backend/agent.js +20 -1
- package/dist/shell/execution-backend/container.js +4 -1
- package/dist/shell/execution-backend/local.js +4 -1
- package/dist/shell/execution-backend/registry.js +2 -1
- package/dist/shell/execution-backend/types.js +0 -9
- package/dist/shell/feedback-cli.js +6 -6
- package/dist/shell/fs-atomic.js +232 -30
- package/dist/shell/man-cli.js +6 -0
- package/dist/shell/metrics-cli.js +2 -1
- package/dist/shell/multi-agent-cli.js +367 -323
- package/dist/shell/multi-agent-host.js +9 -9
- package/dist/shell/multi-agent-operator-ux.js +82 -39
- package/dist/shell/node-store.js +10 -4
- package/dist/shell/observability.js +13 -12
- package/dist/shell/onramp.js +17 -1
- package/dist/shell/operator-ux-text.js +24 -23
- package/dist/shell/operator-ux.js +22 -20
- package/dist/shell/orchestrator.js +49 -38
- package/dist/shell/pipeline-cli.js +168 -111
- package/dist/shell/reclamation-io.js +16 -5
- package/dist/shell/registry-cli.js +19 -17
- package/dist/shell/remote-source.js +13 -8
- package/dist/shell/report-cli.js +45 -0
- package/dist/shell/report.js +4 -2
- package/dist/shell/run-registry-io.js +77 -19
- package/dist/shell/run-store.js +91 -2
- package/dist/shell/scheduling-io.js +12 -13
- package/dist/shell/state-cli.js +2 -7
- package/dist/shell/state-explosion-cli.js +19 -10
- package/dist/shell/topology-io.js +38 -6
- package/dist/shell/trust-audit.js +264 -23
- package/dist/shell/trust-policy-io.js +1 -1
- package/dist/shell/worker-cli.js +41 -29
- package/dist/shell/worker-isolation.js +34 -9
- package/dist/shell/workflow-app-loader.js +3 -2
- package/dist/wiring/capability-table/basics.js +91 -0
- package/dist/wiring/capability-table/exec-backend.js +171 -0
- package/dist/wiring/capability-table/index.js +42 -0
- package/dist/wiring/capability-table/multi-agent.js +630 -0
- package/dist/wiring/capability-table/parity.js +467 -0
- package/dist/wiring/capability-table/pipeline.js +292 -0
- package/dist/wiring/capability-table/registry-core.js +208 -0
- package/dist/wiring/capability-table/reporting.js +435 -0
- package/dist/wiring/capability-table/scheduling-registry.js +592 -0
- package/dist/wiring/capability-table/state.js +181 -0
- package/dist/wiring/capability-table/trust-ledger.js +158 -0
- package/dist/wiring/capability-table/workflow-apps.js +381 -0
- package/docs/agent-delegation-drive.7.md +4 -0
- package/docs/candidate-scoring.7.md +1 -1
- package/docs/canonical-workflow-apps.7.md +7 -7
- package/docs/cli-mcp-parity.7.md +13 -3
- package/docs/contract-migration-tooling.7.md +4 -0
- package/docs/control-plane-scheduling.7.md +4 -0
- package/docs/coordinator-blackboard.7.md +17 -17
- package/docs/dogfood-one-real-repo.7.md +11 -11
- package/docs/durable-state-and-locking.7.md +24 -0
- package/docs/end-to-end-golden-path.7.md +14 -14
- package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
- package/docs/execution-backends.7.md +4 -0
- package/docs/getting-started.md +37 -37
- package/docs/index.md +1 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +21 -17
- package/docs/multi-agent-eval-replay-harness.7.md +17 -13
- package/docs/multi-agent-operator-ux.7.md +23 -19
- package/docs/multi-agent-runtime-core.7.md +22 -22
- package/docs/multi-agent-topologies.7.md +6 -6
- package/docs/multi-agent-trust-policy-audit.7.md +11 -11
- package/docs/node-snapshot-diff-replay.7.md +4 -0
- package/docs/observability-cost-accounting.7.md +4 -0
- package/docs/operator-ux.7.md +34 -34
- package/docs/pipeline-runner.7.md +4 -4
- package/docs/project-index.md +48 -5
- package/docs/real-execution-backends.7.md +4 -0
- package/docs/release-and-migration.7.md +5 -1
- package/docs/release-tooling.7.md +4 -0
- package/docs/routines.md +4 -4
- package/docs/run-registry-control-plane.7.md +23 -19
- package/docs/run-retention-reclamation.7.md +4 -0
- package/docs/scheduled-tasks.md +14 -14
- package/docs/security-trust-hardening.7.md +43 -13
- package/docs/state-explosion-management.7.md +14 -10
- package/docs/team-collaboration.7.md +4 -0
- package/docs/trust-audit-anchor.7.md +71 -0
- package/docs/unix-principles.md +3 -1
- package/docs/verifier-gated-commit.7.md +1 -1
- package/docs/web-desktop-workbench.7.md +4 -0
- package/docs/workflow-app-framework.7.md +13 -13
- package/manifest/plugin.manifest.json +1 -1
- package/package.json +4 -2
- package/scripts/agents/claude-p-agent.js +2 -2
- package/scripts/block-unapproved-tag.sh +23 -2
- package/scripts/bump-version.js +24 -2
- package/scripts/canonical-apps.js +4 -4
- package/scripts/children/batch-delegate-child.js +52 -2
- package/scripts/dogfood-release.js +1 -1
- package/scripts/fake-date-for-reproduction.js +44 -0
- package/scripts/golden-path.js +4 -4
- package/scripts/purity-baseline.json +71 -0
- package/scripts/purity-gate.js +239 -0
- package/scripts/release-check.js +8 -1
- package/scripts/release-flow.js +57 -1
- package/scripts/verdict-keygen.js +83 -0
- package/scripts/verify-bump-reproduction.sh +148 -0
- package/scripts/verify-verdict-signature.js +61 -0
- package/scripts/version-sync-check.js +33 -12
- package/skills/cool-workflow/SKILL.md +9 -9
- package/skills/cool-workflow/references/commands.md +89 -88
- package/ui/workbench/app.css +37 -1
- package/ui/workbench/app.js +124 -6
- package/workflows/README.md +19 -0
|
@@ -294,27 +294,27 @@ function hostNextActions(run, state, active, requiredHostAction, readyForCommit)
|
|
|
294
294
|
const runId = run.id;
|
|
295
295
|
switch (state) {
|
|
296
296
|
case "needs-run":
|
|
297
|
-
return [{ command: `
|
|
297
|
+
return [{ command: `cw multi-agent run ${runId} --topology map-reduce`, reason: "Materialize a host-facing multi-agent topology.", priority: "high" }];
|
|
298
298
|
case "ready-for-dispatch":
|
|
299
|
-
return [{ command: `
|
|
299
|
+
return [{ command: `cw multi-agent step ${runId}`, reason: "Create the next dispatch manifest without spawning workers.", priority: "high" }];
|
|
300
300
|
case "awaiting-worker-output":
|
|
301
|
-
return [{ command: `
|
|
301
|
+
return [{ command: `cw worker output ${runId} <worker-id> <result.md>`, reason: "A host-executed worker must report result evidence.", priority: "high" }];
|
|
302
302
|
case "ready-for-fanin":
|
|
303
|
-
return [{ command: `
|
|
303
|
+
return [{ command: `cw multi-agent step ${runId}`, reason: "Collect fanin once required worker evidence is present.", priority: "high" }];
|
|
304
304
|
case "ready-for-scoring":
|
|
305
|
-
return [{ command: `
|
|
305
|
+
return [{ command: `cw multi-agent score ${runId} --candidate <candidate-id> --criterion correctness=1 --evidence <path-or-ref>`, reason: "Score a candidate with explicit evidence.", priority: "high" }];
|
|
306
306
|
case "ready-for-selection":
|
|
307
|
-
return [{ command: `
|
|
307
|
+
return [{ command: `cw multi-agent select ${runId} --candidate <candidate-id> --reason "<rationale>"`, reason: "Select a scored candidate after verifier gates pass.", priority: "high" }];
|
|
308
308
|
case "ready-for-commit": {
|
|
309
309
|
const ready = readyForCommit[0];
|
|
310
|
-
return [{ command: `
|
|
310
|
+
return [{ command: `cw commit ${runId} --selection ${ready.selectionId} --reason "<verified rationale>"`, reason: "Create a verifier-gated CW state commit.", priority: "high" }];
|
|
311
311
|
}
|
|
312
312
|
case "complete":
|
|
313
|
-
return [{ command: `
|
|
313
|
+
return [{ command: `cw report ${runId} --show`, reason: "Review the completed run report.", priority: "normal" }];
|
|
314
314
|
case "failed":
|
|
315
315
|
case "blocked":
|
|
316
316
|
default:
|
|
317
|
-
return [{ command: `
|
|
317
|
+
return [{ command: `cw multi-agent status ${runId}`, reason: active.length > 1 ? "Resolve ambiguous active topology state." : "Inspect specific blocked reasons.", priority: "high" }];
|
|
318
318
|
}
|
|
319
319
|
}
|
|
320
320
|
function envelope(run, command, options = {}) {
|
|
@@ -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
|
}
|
|
@@ -105,13 +106,13 @@ function summarizeMultiAgentOperator(run) {
|
|
|
105
106
|
multiAgent.nextAction ||
|
|
106
107
|
blackboard.nextAction ||
|
|
107
108
|
readyCommitCommand(run) ||
|
|
108
|
-
`
|
|
109
|
+
`cw multi-agent status ${run.id} --json`;
|
|
109
110
|
return {
|
|
110
111
|
schemaVersion: 1,
|
|
111
112
|
runId: run.id,
|
|
112
|
-
activeMultiAgentRunIds: [...activeMultiAgentRunIds, ...((state.runs || []).filter((entry) => !isTerminal(entry.status)).map((entry) => entry.id))]
|
|
113
|
+
activeMultiAgentRunIds: [...new Set([...activeMultiAgentRunIds, ...((state.runs || []).filter((entry) => !isTerminal(entry.status)).map((entry) => entry.id))])],
|
|
113
114
|
topologyRunIds: [...activeTopologyIds],
|
|
114
|
-
topologyIds: topologies.active.map((entry) => entry.topologyId)
|
|
115
|
+
topologyIds: [...new Set(topologies.active.map((entry) => entry.topologyId))],
|
|
115
116
|
groups: (state.groups || []).map((entry) => entry.id).sort(),
|
|
116
117
|
roles: (state.roles || []).map((entry) => entry.id).sort(),
|
|
117
118
|
memberships: (state.memberships || []).map((entry) => entry.id).sort(),
|
|
@@ -260,7 +261,7 @@ function deriveDependencies(run) {
|
|
|
260
261
|
for (const fanin of state.fanins || []) {
|
|
261
262
|
add(fanin.fanoutId ? `${run.id}:multi-agent:fanout:${fanin.fanoutId}` : `${run.id}:multi-agent:group:${fanin.groupId}`, `${run.id}:multi-agent:fanin:${fanin.id}`, "fanin");
|
|
262
263
|
for (const coverage of fanin.evidenceCoverage || []) {
|
|
263
|
-
add(`${run.id}:multi-agent:membership:${coverage.membershipId}`, `${run.id}:multi-agent:fanin:${fanin.id}`, coverage.complete ? "adopted-by" : "blocks", coverage.complete ? "ready" : "blocked", coverage.complete ? undefined : "membership has not reported required evidence", `
|
|
264
|
+
add(`${run.id}:multi-agent:membership:${coverage.membershipId}`, `${run.id}:multi-agent:fanin:${fanin.id}`, coverage.complete ? "adopted-by" : "blocks", coverage.complete ? "ready" : "blocked", coverage.complete ? undefined : "membership has not reported required evidence", `cw worker manifest ${run.id} ${coverage.workerId || "<worker-id>"}`);
|
|
264
265
|
}
|
|
265
266
|
}
|
|
266
267
|
for (const candidate of candidatesOf(run)) {
|
|
@@ -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
|
|
279
|
+
return uniqueById(rows).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 = [];
|
|
@@ -283,61 +284,74 @@ function deriveFailures(run, dependencies) {
|
|
|
283
284
|
rows.push({ id, kind, status, owner, linked, reason, nextCommand });
|
|
284
285
|
};
|
|
285
286
|
const state = maOf(run);
|
|
287
|
+
// Grouped/indexed once instead of re-filtering/re-scanning the whole
|
|
288
|
+
// memberships/workers array per role/membership below (O(roles x
|
|
289
|
+
// memberships) and O(memberships x workers) otherwise -- the same
|
|
290
|
+
// array-scan-per-item shape 024b007 fixed for phase/task selection).
|
|
291
|
+
const membershipsByRole = new Map();
|
|
292
|
+
for (const entry of state.memberships || []) {
|
|
293
|
+
const list = membershipsByRole.get(entry.roleId);
|
|
294
|
+
if (list)
|
|
295
|
+
list.push(entry);
|
|
296
|
+
else
|
|
297
|
+
membershipsByRole.set(entry.roleId, [entry]);
|
|
298
|
+
}
|
|
299
|
+
const workersById = new Map(workersOf(run).map((entry) => [entry.id, entry]));
|
|
286
300
|
for (const role of state.roles || []) {
|
|
287
|
-
const memberships = (
|
|
301
|
+
const memberships = membershipsByRole.get(role.id) || [];
|
|
288
302
|
if (!memberships.length && role.status !== "completed" && role.status !== "cancelled") {
|
|
289
|
-
add(role.id, "missing-role-coverage", role.status, `role ${role.id} has no membership`, `
|
|
303
|
+
add(role.id, "missing-role-coverage", role.status, `role ${role.id} has no membership`, `cw multi-agent step ${run.id}`, role.id);
|
|
290
304
|
}
|
|
291
305
|
if (role.status === "blocked" || role.status === "cancelled")
|
|
292
|
-
add(role.id, "agent-role", role.status, `role ${role.id} is ${role.status}`, `
|
|
306
|
+
add(role.id, "agent-role", role.status, `role ${role.id} is ${role.status}`, `cw multi-agent status ${run.id} --json`, role.id);
|
|
293
307
|
}
|
|
294
308
|
for (const membership of state.memberships || []) {
|
|
295
|
-
const worker = membership.workerId ?
|
|
309
|
+
const worker = membership.workerId ? workersById.get(membership.workerId) : undefined;
|
|
296
310
|
if (membership.status === "failed" || membership.status === "cancelled")
|
|
297
|
-
add(membership.id, "agent-membership", membership.status, `membership ${membership.id} is ${membership.status}`, `
|
|
311
|
+
add(membership.id, "agent-membership", membership.status, `membership ${membership.id} is ${membership.status}`, `cw multi-agent membership ${run.id} ${membership.id}`, membership.roleId, membership.workerId);
|
|
298
312
|
if (!membership.workerId)
|
|
299
|
-
add(membership.id, "missing-worker", membership.status, `membership ${membership.id} has no worker`, `
|
|
313
|
+
add(membership.id, "missing-worker", membership.status, `membership ${membership.id} has no worker`, `cw multi-agent step ${run.id}`, membership.roleId, membership.taskId);
|
|
300
314
|
if (worker && (worker.status === "failed" || worker.status === "rejected"))
|
|
301
|
-
add(String(worker.id), "worker", String(worker.status), worker.errors?.[0]?.message || `worker ${worker.id} is ${worker.status}`, `
|
|
315
|
+
add(String(worker.id), "worker", String(worker.status), worker.errors?.[0]?.message || `worker ${worker.id} is ${worker.status}`, `cw worker show ${run.id} ${worker.id}`, membership.roleId, membership.id);
|
|
302
316
|
if (worker && (worker.status === "allocated" || worker.status === "running"))
|
|
303
|
-
add(String(worker.id), "worker-output", String(worker.status), `worker ${worker.id} has not reported output`, `
|
|
317
|
+
add(String(worker.id), "worker-output", String(worker.status), `worker ${worker.id} has not reported output`, `cw worker manifest ${run.id} ${worker.id}`, membership.roleId, membership.id);
|
|
304
318
|
}
|
|
305
319
|
for (const fanin of state.fanins || []) {
|
|
306
320
|
for (const reason of fanin.blockedReasons || [])
|
|
307
|
-
add(fanin.id, "fanin", fanin.status, reason, `
|
|
321
|
+
add(fanin.id, "fanin", fanin.status, reason, `cw multi-agent failures ${run.id}`, fanin.groupId, fanin.fanoutId);
|
|
308
322
|
for (const roleId of fanin.missingRoleIds || [])
|
|
309
|
-
add(`${fanin.id}:${roleId}`, "missing-role-evidence", "missing", `fanin ${fanin.id} is missing role ${roleId}`, `
|
|
323
|
+
add(`${fanin.id}:${roleId}`, "missing-role-evidence", "missing", `fanin ${fanin.id} is missing role ${roleId}`, `cw multi-agent step ${run.id}`, roleId, fanin.id);
|
|
310
324
|
for (const membershipId of fanin.missingMembershipIds || [])
|
|
311
|
-
add(`${fanin.id}:${membershipId}`, "missing-membership-evidence", "missing", `fanin ${fanin.id} is missing membership ${membershipId}`, `
|
|
325
|
+
add(`${fanin.id}:${membershipId}`, "missing-membership-evidence", "missing", `fanin ${fanin.id} is missing membership ${membershipId}`, `cw multi-agent membership ${run.id} ${membershipId}`, membershipId, fanin.id);
|
|
312
326
|
}
|
|
313
327
|
for (const topology of topologyRunsOf(run)) {
|
|
314
328
|
for (const missing of topology.missingEvidence || [])
|
|
315
|
-
add(`${topology.id}:${missing}`, "missing-topology-evidence", "missing", missing, topology.nextActions?.[0] || `
|
|
329
|
+
add(`${topology.id}:${missing}`, "missing-topology-evidence", "missing", missing, topology.nextActions?.[0] || `cw topology summary ${run.id}`, String(topology.id));
|
|
316
330
|
if (topology.status === "blocked" || topology.status === "failed")
|
|
317
|
-
add(String(topology.id), "topology", String(topology.status), `topology ${topology.id} is ${topology.status}`, `
|
|
331
|
+
add(String(topology.id), "topology", String(topology.status), `topology ${topology.id} is ${topology.status}`, `cw topology summary ${run.id}`, String(topology.id));
|
|
318
332
|
}
|
|
319
333
|
for (const feedback of feedbackOf(run)) {
|
|
320
334
|
if (feedback.status === "open" || feedback.status === "tasked")
|
|
321
|
-
add(String(feedback.id), String(feedback.classification), String(feedback.status), String(feedback.message), `
|
|
335
|
+
add(String(feedback.id), String(feedback.classification), String(feedback.status), String(feedback.message), `cw feedback show ${run.id} ${feedback.id}`, feedback.taskId, feedback.nodeId);
|
|
322
336
|
}
|
|
323
337
|
for (const candidate of candidatesOf(run)) {
|
|
324
338
|
const scores = candidate.scores || [];
|
|
325
339
|
if (candidate.status === "rejected" || candidate.status === "failed")
|
|
326
|
-
add(String(candidate.id), "candidate", String(candidate.status), candidate.feedbackIds?.[0] || `candidate ${candidate.id} is ${candidate.status}`, `
|
|
340
|
+
add(String(candidate.id), "candidate", String(candidate.status), candidate.feedbackIds?.[0] || `candidate ${candidate.id} is ${candidate.status}`, `cw candidate show ${run.id} ${candidate.id}`, candidate.workerId, candidate.taskId);
|
|
327
341
|
if (!scores.length && candidate.status !== "rejected" && candidate.status !== "failed")
|
|
328
|
-
add(String(candidate.id), "candidate-score-gap", String(candidate.status), `candidate ${candidate.id} has no score`, `
|
|
342
|
+
add(String(candidate.id), "candidate-score-gap", String(candidate.status), `candidate ${candidate.id} has no score`, `cw multi-agent score ${run.id} --candidate ${candidate.id} --evidence <path-or-ref>`, candidate.workerId, candidate.taskId);
|
|
329
343
|
if (!candidate.verifierNodeId)
|
|
330
|
-
add(`${candidate.id}:verifier`, "candidate-verifier-gap", String(candidate.status), `candidate ${candidate.id} has no verifier gate`, `
|
|
344
|
+
add(`${candidate.id}:verifier`, "candidate-verifier-gap", String(candidate.status), `candidate ${candidate.id} has no verifier gate`, `cw candidate show ${run.id} ${candidate.id}`, candidate.workerId, candidate.taskId);
|
|
331
345
|
}
|
|
332
346
|
if (candidatesOf(run).some((candidate) => (candidate.scores || []).length) && !selectionsOf(run).length) {
|
|
333
|
-
add("selection-gap", "selection", "missing", "scored candidates exist but no selection is recorded", `
|
|
347
|
+
add("selection-gap", "selection", "missing", "scored candidates exist but no selection is recorded", `cw multi-agent select ${run.id} --candidate <candidate-id> --reason "<rationale>"`);
|
|
334
348
|
}
|
|
335
349
|
for (const dep of dependencies.filter((entry) => entry.status === "blocked"))
|
|
336
|
-
add(dep.id, "ambiguous-dependency", dep.status, dep.reason || "dependency is blocked", dep.nextCommand || `
|
|
337
|
-
const readySelection =
|
|
350
|
+
add(dep.id, "ambiguous-dependency", dep.status, dep.reason || "dependency is blocked", dep.nextCommand || `cw multi-agent status ${run.id} --json`);
|
|
351
|
+
const readySelection = firstUngatedSelection(run);
|
|
338
352
|
if (readySelection)
|
|
339
|
-
add(String(readySelection.id), "commit-gate", "not-ready", `selection ${readySelection.id} has no verifier-gated commit`, `
|
|
340
|
-
return rows
|
|
353
|
+
add(String(readySelection.id), "commit-gate", "not-ready", `selection ${readySelection.id} has no verifier-gated commit`, `cw commit ${run.id} --selection ${readySelection.id} --reason "<verified rationale>"`, readySelection.candidateId);
|
|
354
|
+
return uniqueByFailure(rows).sort((left, right) => (0, collate_1.stableCompare)(left.kind, right.kind) || (0, collate_1.stableCompare)(left.id, right.id));
|
|
341
355
|
}
|
|
342
356
|
function deriveEvidence(run) {
|
|
343
357
|
const rows = new Map();
|
|
@@ -442,7 +456,7 @@ function deriveEvidence(run) {
|
|
|
442
456
|
return [...rows.values()]
|
|
443
457
|
.map(normalizeEvidenceStatus)
|
|
444
458
|
.map(withDisposition)
|
|
445
|
-
.sort((left, right) => statusRank(left.status) - statusRank(right.status) || left.id
|
|
459
|
+
.sort((left, right) => statusRank(left.status) - statusRank(right.status) || (0, collate_1.stableCompare)(left.id, right.id));
|
|
446
460
|
}
|
|
447
461
|
function formatDependencies(rows) {
|
|
448
462
|
const lines = ["Dependencies"];
|
|
@@ -492,9 +506,18 @@ function readScores(run, candidateId) {
|
|
|
492
506
|
.sort()
|
|
493
507
|
.map((file) => JSON.parse(fs.readFileSync(path.join(dir, file), "utf8")));
|
|
494
508
|
}
|
|
509
|
+
/** The first selection with no verifier-gated commit yet, if any -- shared
|
|
510
|
+
* by deriveFailures and readyCommitCommand. A Set of already-gated
|
|
511
|
+
* selection ids, built once, replaces re-scanning ALL commits per
|
|
512
|
+
* selection (O(selections x commits) otherwise -- the same array-scan-
|
|
513
|
+
* per-item shape 024b007 fixed for phase/task selection). */
|
|
514
|
+
function firstUngatedSelection(run) {
|
|
515
|
+
const gatedSelectionIds = new Set(commitsOf(run).filter((commit) => commit.verifierGated).map((commit) => commit.selectionId));
|
|
516
|
+
return selectionsOf(run).find((entry) => !gatedSelectionIds.has(entry.id));
|
|
517
|
+
}
|
|
495
518
|
function readyCommitCommand(run) {
|
|
496
|
-
const selection =
|
|
497
|
-
return selection ? `
|
|
519
|
+
const selection = firstUngatedSelection(run);
|
|
520
|
+
return selection ? `cw commit ${run.id} --selection ${selection.id} --reason "<verified rationale>"` : undefined;
|
|
498
521
|
}
|
|
499
522
|
function normalizeEvidenceStatus(row) {
|
|
500
523
|
if (row.rejectedBy.length)
|
|
@@ -558,16 +581,36 @@ function isTerminal(status) {
|
|
|
558
581
|
return status === "completed" || status === "failed" || status === "cancelled";
|
|
559
582
|
}
|
|
560
583
|
function unique(values) {
|
|
561
|
-
return values.filter(Boolean)
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
584
|
+
return [...new Set(values.filter(Boolean))].sort();
|
|
585
|
+
}
|
|
586
|
+
// Each dedup below keeps the FIRST occurrence of a duplicate key, same as
|
|
587
|
+
// the `values.findIndex(...) === index` shape it replaces -- a Set of seen
|
|
588
|
+
// keys does this in one O(N) pass instead of an O(N) findIndex per item
|
|
589
|
+
// (O(N^2) total; this was the dominant cost of deriveDependencies/
|
|
590
|
+
// deriveFailures at large membership counts, found while pinning perf
|
|
591
|
+
// cycle P1-1's review-fix regression test).
|
|
592
|
+
function uniqueById(values) {
|
|
593
|
+
const seen = new Set();
|
|
594
|
+
const result = [];
|
|
595
|
+
for (const value of values) {
|
|
596
|
+
if (seen.has(value.id))
|
|
597
|
+
continue;
|
|
598
|
+
seen.add(value.id);
|
|
599
|
+
result.push(value);
|
|
600
|
+
}
|
|
601
|
+
return result;
|
|
568
602
|
}
|
|
569
|
-
function uniqueByFailure(
|
|
570
|
-
|
|
603
|
+
function uniqueByFailure(values) {
|
|
604
|
+
const seen = new Set();
|
|
605
|
+
const result = [];
|
|
606
|
+
for (const value of values) {
|
|
607
|
+
const key = `${value.id}\0${value.kind}`;
|
|
608
|
+
if (seen.has(key))
|
|
609
|
+
continue;
|
|
610
|
+
seen.add(key);
|
|
611
|
+
result.push(value);
|
|
612
|
+
}
|
|
613
|
+
return result;
|
|
571
614
|
}
|
|
572
615
|
function uniqueEdges(edges) {
|
|
573
616
|
const seen = new Set();
|
package/dist/shell/node-store.js
CHANGED
|
@@ -94,12 +94,16 @@ function replayNodeSnapshot(run, snapshot, options = {}) {
|
|
|
94
94
|
: (replay) => (0, fs_atomic_1.writeJson)(path.join(snapshotDir(run, snapshot.nodeId), "replays", `${replay.replayId}.json`), replay),
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
|
-
/** Scans every dir under `nodes/snapshots/` for `<snapshotId>.json`.
|
|
97
|
+
/** Scans every dir under `nodes/snapshots/` for `<snapshotId>.json`. Runs
|
|
98
|
+
* `snapshotId` through `safeFileName` first (matching every write-side path
|
|
99
|
+
* segment in this file) so a traversal-shaped id can never escape `nodeDir`
|
|
100
|
+
* into an arbitrary file elsewhere on disk. */
|
|
98
101
|
function readNodeSnapshot(run, snapshotId) {
|
|
99
102
|
const root = snapshotsRoot(run);
|
|
103
|
+
const safeId = (0, fs_atomic_1.safeFileName)(snapshotId);
|
|
100
104
|
if (fs.existsSync(root)) {
|
|
101
105
|
for (const nodeDir of fs.readdirSync(root)) {
|
|
102
|
-
const file = path.join(root, nodeDir, `${
|
|
106
|
+
const file = path.join(root, nodeDir, `${safeId}.json`);
|
|
103
107
|
if (fs.existsSync(file))
|
|
104
108
|
return (0, validation_1.validateNodeSnapshot)(JSON.parse(fs.readFileSync(file, "utf8")));
|
|
105
109
|
}
|
|
@@ -108,12 +112,14 @@ function readNodeSnapshot(run, snapshotId) {
|
|
|
108
112
|
freshness: "absent",
|
|
109
113
|
});
|
|
110
114
|
}
|
|
111
|
-
/** Scans every dir under `nodes/snapshots/<nodeDir>/replays/` for
|
|
115
|
+
/** Scans every dir under `nodes/snapshots/<nodeDir>/replays/` for
|
|
116
|
+
* `<replayId>.json`. Same `safeFileName` guard as `readNodeSnapshot`. */
|
|
112
117
|
function readNodeReplay(run, replayId) {
|
|
113
118
|
const root = snapshotsRoot(run);
|
|
119
|
+
const safeId = (0, fs_atomic_1.safeFileName)(replayId);
|
|
114
120
|
if (fs.existsSync(root)) {
|
|
115
121
|
for (const nodeDir of fs.readdirSync(root)) {
|
|
116
|
-
const file = path.join(root, nodeDir, "replays", `${
|
|
122
|
+
const file = path.join(root, nodeDir, "replays", `${safeId}.json`);
|
|
117
123
|
if (fs.existsSync(file))
|
|
118
124
|
return (0, validation_1.validateNodeReplayRun)(JSON.parse(fs.readFileSync(file, "utf8")));
|
|
119
125
|
}
|
|
@@ -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")
|
|
@@ -430,7 +431,7 @@ function deriveMetricsReport(run, options) {
|
|
|
430
431
|
collaboration: deriveCollaborationMetrics(run),
|
|
431
432
|
nextAction: totals.unreportedUnits > 0 && totals.attestedUnits === 0
|
|
432
433
|
? "No attested usage yet — record host usage on result/worker intake (cw result ... --usage-input-tokens N --usage-output-tokens M --usage-model ID)."
|
|
433
|
-
: `
|
|
434
|
+
: `cw metrics show ${run.id} --json`,
|
|
434
435
|
};
|
|
435
436
|
}
|
|
436
437
|
function metricsDir(run) {
|
|
@@ -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(", ");
|
|
@@ -151,7 +152,7 @@ function formatMultiAgentTrustAudit(view) {
|
|
|
151
152
|
...formatAuditEventRows(arrayView(view.policyViolations)),
|
|
152
153
|
"",
|
|
153
154
|
"Next Action",
|
|
154
|
-
` ${String(view.nextAction || `
|
|
155
|
+
` ${String(view.nextAction || `cw audit multi-agent ${String(view.runId || "<run-id>")} --json`)}`,
|
|
155
156
|
].join("\n");
|
|
156
157
|
}
|
|
157
158
|
/** `Topologies` panel — port of the old build's formatTopologyPanel
|
|
@@ -298,27 +299,27 @@ function formatOperatorReport(summary, evidencePaths) {
|
|
|
298
299
|
(0, multi_agent_operator_ux_1.formatMultiAgentEvidence)(summary.multiAgentOperator.evidence),
|
|
299
300
|
"",
|
|
300
301
|
"Resource Commands",
|
|
301
|
-
`
|
|
302
|
-
`
|
|
303
|
-
`
|
|
304
|
-
`
|
|
305
|
-
`
|
|
306
|
-
`
|
|
307
|
-
`
|
|
308
|
-
`
|
|
309
|
-
`
|
|
310
|
-
`
|
|
311
|
-
`
|
|
312
|
-
`
|
|
313
|
-
`
|
|
314
|
-
`
|
|
315
|
-
`
|
|
316
|
-
`
|
|
317
|
-
`
|
|
318
|
-
`
|
|
319
|
-
`
|
|
320
|
-
`
|
|
321
|
-
`
|
|
302
|
+
` cw graph ${summary.runId}`,
|
|
303
|
+
` cw worker summary ${summary.runId}`,
|
|
304
|
+
` cw topology summary ${summary.runId}`,
|
|
305
|
+
` cw topology graph ${summary.runId}`,
|
|
306
|
+
` cw multi-agent summary ${summary.runId}`,
|
|
307
|
+
` cw multi-agent graph ${summary.runId}`,
|
|
308
|
+
` cw multi-agent dependencies ${summary.runId}`,
|
|
309
|
+
` cw multi-agent failures ${summary.runId}`,
|
|
310
|
+
` cw multi-agent evidence ${summary.runId}`,
|
|
311
|
+
` cw blackboard summary ${summary.runId}`,
|
|
312
|
+
` cw blackboard graph ${summary.runId}`,
|
|
313
|
+
` cw coordinator summary ${summary.runId}`,
|
|
314
|
+
` cw candidate summary ${summary.runId}`,
|
|
315
|
+
` cw feedback summary ${summary.runId}`,
|
|
316
|
+
` cw commit summary ${summary.runId}`,
|
|
317
|
+
` cw audit summary ${summary.runId}`,
|
|
318
|
+
` cw audit provenance ${summary.runId}`,
|
|
319
|
+
` cw audit multi-agent ${summary.runId}`,
|
|
320
|
+
` cw audit policy ${summary.runId}`,
|
|
321
|
+
` cw audit blackboard ${summary.runId}`,
|
|
322
|
+
` cw audit judge ${summary.runId}`,
|
|
322
323
|
].join("\n");
|
|
323
324
|
}
|
|
324
325
|
function groupBy(values, key) {
|
|
@@ -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)
|
|
@@ -114,7 +115,7 @@ function summarizeRun(run) {
|
|
|
114
115
|
function adviseNoRun() {
|
|
115
116
|
return [
|
|
116
117
|
{
|
|
117
|
-
command: "
|
|
118
|
+
command: "cw plan <workflow-id> --repo <path>",
|
|
118
119
|
reason: "No run id is available yet; create a workflow run before dispatching or recording evidence.",
|
|
119
120
|
priority: "high",
|
|
120
121
|
},
|
|
@@ -122,7 +123,8 @@ function adviseNoRun() {
|
|
|
122
123
|
}
|
|
123
124
|
function summarizePhases(run) {
|
|
124
125
|
return run.phases.map((phase) => {
|
|
125
|
-
const
|
|
126
|
+
const taskIds = new Set(phase.taskIds);
|
|
127
|
+
const phaseTasks = run.tasks.filter((t) => taskIds.has(t.id));
|
|
126
128
|
const byStatus = countBy(phaseTasks, (t) => t.status);
|
|
127
129
|
return { id: phase.id, name: phase.name, status: phase.status, tasks: { total: phaseTasks.length, ...byStatus } };
|
|
128
130
|
});
|
|
@@ -143,8 +145,8 @@ function summarizeTasks(tasks) {
|
|
|
143
145
|
* verb and operator panel both read. */
|
|
144
146
|
function summarizeOperatorCandidates(run) {
|
|
145
147
|
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
|
|
148
|
+
const candidates = [...(run.candidates || [])].sort((left, right) => (0, collate_1.stableCompare)(left.id, right.id));
|
|
149
|
+
const selections = [...(run.candidateSelections || [])].sort((left, right) => (0, collate_1.stableCompare)(left.id, right.id));
|
|
148
150
|
const commits = run.commits || [];
|
|
149
151
|
const selectedIds = new Set(selections.map((selection) => selection.candidateId));
|
|
150
152
|
const readyForCommit = selections
|
|
@@ -190,7 +192,7 @@ function summarizeOperatorFeedback(run) {
|
|
|
190
192
|
};
|
|
191
193
|
}
|
|
192
194
|
function summarizeOperatorCommits(run) {
|
|
193
|
-
const commits = [...(run.commits || [])].sort((left, right) => left.createdAt
|
|
195
|
+
const commits = [...(run.commits || [])].sort((left, right) => (0, collate_1.stableCompare)(left.createdAt, right.createdAt) || (0, collate_1.stableCompare)(left.id, right.id));
|
|
194
196
|
return {
|
|
195
197
|
total: commits.length,
|
|
196
198
|
verifierGated: commits.filter((c) => c.verifierGated).length,
|
|
@@ -200,7 +202,7 @@ function summarizeOperatorCommits(run) {
|
|
|
200
202
|
};
|
|
201
203
|
}
|
|
202
204
|
function summarizeOperatorWorkers(run) {
|
|
203
|
-
const workers = (run.workers || []).slice().sort((a, b) => a.id
|
|
205
|
+
const workers = (run.workers || []).slice().sort((a, b) => (0, collate_1.stableCompare)(a.id, b.id));
|
|
204
206
|
return {
|
|
205
207
|
total: workers.length,
|
|
206
208
|
byStatus: countBy(workers, (w) => w.status),
|
|
@@ -224,52 +226,52 @@ function adviseNextSteps(run, ctx) {
|
|
|
224
226
|
const openFeedback = feedbackRecords.filter((record) => record.status === "open");
|
|
225
227
|
if (openFeedback.length) {
|
|
226
228
|
const feedback = openFeedback[0];
|
|
227
|
-
actions.push({ command: `
|
|
228
|
-
actions.push({ command: `
|
|
229
|
+
actions.push({ command: `cw feedback show ${run.id} ${feedback.id}`, reason: `Open ${feedback.severity} ${feedback.classification} feedback should be inspected before more dispatch.`, priority: "high" });
|
|
230
|
+
actions.push({ command: `cw feedback task ${run.id} ${feedback.id}`, reason: "Create a correction task if the feedback is actionable.", priority: "normal" });
|
|
229
231
|
return actions;
|
|
230
232
|
}
|
|
231
233
|
const failedWorker = ctx.workers.workers.find((w) => w.status === "failed" || w.status === "rejected");
|
|
232
234
|
if (failedWorker) {
|
|
233
|
-
actions.push({ command: `
|
|
235
|
+
actions.push({ command: `cw feedback list ${run.id}`, reason: `Worker ${failedWorker.id} is ${failedWorker.status}; inspect linked feedback before retrying.`, priority: "high" });
|
|
234
236
|
return actions;
|
|
235
237
|
}
|
|
236
238
|
if (ctx.tasks.running.length) {
|
|
237
239
|
const worker = ctx.workers.workers.find((w) => w.status === "running" || w.status === "allocated");
|
|
238
|
-
actions.push({ command: worker ? `
|
|
240
|
+
actions.push({ command: worker ? `cw worker manifest ${run.id} ${worker.id}` : `cw worker list ${run.id}`, reason: "Running workers need their manifests inspected and final output recorded.", priority: "high" });
|
|
239
241
|
if (worker && worker.resultPath) {
|
|
240
|
-
actions.push({ command: `
|
|
242
|
+
actions.push({ command: `cw worker output ${run.id} ${worker.id} ${worker.resultPath}`, reason: "Record the worker result after its result.md is ready.", priority: "normal" });
|
|
241
243
|
}
|
|
242
244
|
return actions;
|
|
243
245
|
}
|
|
244
246
|
if (ctx.tasks.pending.length) {
|
|
245
247
|
const limit = Math.min(ctx.tasks.pending.length, run.workflow.limits?.maxConcurrentAgents || 4);
|
|
246
|
-
actions.push({ command: `
|
|
248
|
+
actions.push({ command: `cw dispatch ${run.id} --limit ${limit}`, reason: `${ctx.tasks.pending.length} pending task(s) are ready for the active phase.`, priority: "high" });
|
|
247
249
|
return actions;
|
|
248
250
|
}
|
|
249
251
|
const candidateRecords = (run.candidates || []);
|
|
250
252
|
const selectionRecords = (run.candidateSelections || []);
|
|
251
253
|
const completedWithoutCandidate = (run.tasks || []).find((task) => task.status === "completed" && task.workerId && !candidateRecords.some((candidate) => candidate.workerId === task.workerId));
|
|
252
254
|
if (completedWithoutCandidate?.workerId) {
|
|
253
|
-
actions.push({ command: `
|
|
255
|
+
actions.push({ command: `cw candidate register ${run.id} --worker ${completedWithoutCandidate.workerId}`, reason: "A completed worker result is available but has not been registered as a candidate.", priority: "high" });
|
|
254
256
|
return actions;
|
|
255
257
|
}
|
|
256
258
|
const unscored = candidateRecords.find((candidate) => candidate.status === "registered" && !(candidate.scores || []).length);
|
|
257
259
|
if (unscored) {
|
|
258
|
-
actions.push({ command: `
|
|
260
|
+
actions.push({ command: `cw candidate score ${run.id} ${unscored.id} --criterion correctness=1 --evidence <path-or-locator>`, reason: "Registered candidates need score evidence before ranking or selection.", priority: "high" });
|
|
259
261
|
return actions;
|
|
260
262
|
}
|
|
261
263
|
const scoredWithoutSelection = candidateRecords.find((candidate) => candidate.status === "scored" && !selectionRecords.some((selection) => selection.candidateId === candidate.id));
|
|
262
264
|
if (scoredWithoutSelection) {
|
|
263
|
-
actions.push({ command: `
|
|
264
|
-
actions.push({ command: `
|
|
265
|
+
actions.push({ command: `cw candidate rank ${run.id}`, reason: "Scored candidates can be ranked before selection.", priority: "high" });
|
|
266
|
+
actions.push({ command: `cw candidate select ${run.id} ${scoredWithoutSelection.id}`, reason: "Select the candidate once the ranking supports it.", priority: "normal" });
|
|
265
267
|
return actions;
|
|
266
268
|
}
|
|
267
269
|
if (ctx.candidates.readyForCommit.length) {
|
|
268
270
|
const ready = ctx.candidates.readyForCommit[0];
|
|
269
|
-
actions.push({ command: `
|
|
271
|
+
actions.push({ command: `cw commit ${run.id} --selection ${ready.selectionId}`, reason: "A verified selected candidate is ready for a verifier-gated commit.", priority: "high" });
|
|
270
272
|
return actions;
|
|
271
273
|
}
|
|
272
|
-
actions.push({ command: `
|
|
274
|
+
actions.push({ command: `cw report ${run.id} --show`, reason: "All tracked phases are complete or no further operator action is currently available.", priority: "normal" });
|
|
273
275
|
return actions;
|
|
274
276
|
}
|
|
275
277
|
/** `cw operator status <id> --json` — a wider payload than `summarizeRun`
|
|
@@ -425,7 +427,7 @@ function buildOperatorGraph(run) {
|
|
|
425
427
|
addNode(node.id, node.kind, node.status, node.label, node.path);
|
|
426
428
|
for (const edge of blackboardGraph.edges)
|
|
427
429
|
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
|
|
430
|
+
const sortedNodes = [...nodes.values()].sort((a, b) => (0, collate_1.stableCompare)(a.kind, b.kind) || (0, collate_1.stableCompare)(a.id, b.id));
|
|
431
|
+
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
432
|
return { runId: run.id, nodes: sortedNodes, edges: sortedEdges };
|
|
431
433
|
}
|