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.
Files changed (156) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/apps/architecture-review/app.json +1 -1
  4. package/apps/architecture-review-fast/app.json +1 -1
  5. package/apps/end-to-end-golden-path/app.json +1 -1
  6. package/apps/pr-review-fix-ci/app.json +1 -1
  7. package/apps/release-cut/app.json +1 -1
  8. package/apps/research-synthesis/app.json +1 -1
  9. package/dist/cli/dispatch.js +29 -91
  10. package/dist/cli/entry.js +41 -1
  11. package/dist/cli/io.js +6 -2
  12. package/dist/cli/parseargv.js +6 -2
  13. package/dist/core/capability-data.js +271 -0
  14. package/dist/core/capability-table.js +16 -3504
  15. package/dist/core/format/completion.js +68 -0
  16. package/dist/core/format/help.js +50 -8
  17. package/dist/core/format/safe-json.js +73 -0
  18. package/dist/core/format/state-explosion-text.js +1 -1
  19. package/dist/core/multi-agent/candidate-scoring.js +5 -1
  20. package/dist/core/multi-agent/collaboration.js +6 -5
  21. package/dist/core/multi-agent/coordinator.js +9 -8
  22. package/dist/core/multi-agent/runtime.js +4 -4
  23. package/dist/core/multi-agent/topology.js +3 -3
  24. package/dist/core/pipeline/commit-gate.js +2 -1
  25. package/dist/core/pipeline/dispatch.js +18 -4
  26. package/dist/core/pipeline/drive-decide.js +2 -1
  27. package/dist/core/state/migrations.js +16 -1
  28. package/dist/core/state/state-explosion/digest.js +16 -15
  29. package/dist/core/state/state-explosion/graph.js +26 -19
  30. package/dist/core/state/state-explosion/helpers.js +2 -1
  31. package/dist/core/state/state-explosion/report.js +6 -6
  32. package/dist/core/trust/ledger.js +2 -1
  33. package/dist/core/types/execution-backend.js +18 -0
  34. package/dist/core/types/observability.js +7 -0
  35. package/dist/core/util/collate.js +23 -0
  36. package/dist/core/util/numeric-flag.js +40 -0
  37. package/dist/core/version.js +1 -1
  38. package/dist/mcp/server.js +99 -11
  39. package/dist/shell/audit-cli.js +99 -23
  40. package/dist/shell/commit-summary.js +2 -1
  41. package/dist/shell/coordinator-io.js +73 -13
  42. package/dist/shell/dispatch.js +1 -1
  43. package/dist/shell/doctor.js +80 -1
  44. package/dist/shell/drive.js +269 -59
  45. package/dist/shell/eval-text.js +2 -2
  46. package/dist/shell/evidence-reasoning.js +8 -7
  47. package/dist/shell/execution-backend/agent.js +20 -1
  48. package/dist/shell/execution-backend/container.js +4 -1
  49. package/dist/shell/execution-backend/local.js +4 -1
  50. package/dist/shell/execution-backend/registry.js +2 -1
  51. package/dist/shell/execution-backend/types.js +0 -9
  52. package/dist/shell/feedback-cli.js +6 -6
  53. package/dist/shell/fs-atomic.js +232 -30
  54. package/dist/shell/man-cli.js +6 -0
  55. package/dist/shell/metrics-cli.js +2 -1
  56. package/dist/shell/multi-agent-cli.js +367 -323
  57. package/dist/shell/multi-agent-host.js +9 -9
  58. package/dist/shell/multi-agent-operator-ux.js +82 -39
  59. package/dist/shell/node-store.js +10 -4
  60. package/dist/shell/observability.js +13 -12
  61. package/dist/shell/onramp.js +17 -1
  62. package/dist/shell/operator-ux-text.js +24 -23
  63. package/dist/shell/operator-ux.js +22 -20
  64. package/dist/shell/orchestrator.js +49 -38
  65. package/dist/shell/pipeline-cli.js +168 -111
  66. package/dist/shell/reclamation-io.js +16 -5
  67. package/dist/shell/registry-cli.js +19 -17
  68. package/dist/shell/remote-source.js +13 -8
  69. package/dist/shell/report-cli.js +45 -0
  70. package/dist/shell/report.js +4 -2
  71. package/dist/shell/run-registry-io.js +77 -19
  72. package/dist/shell/run-store.js +91 -2
  73. package/dist/shell/scheduling-io.js +12 -13
  74. package/dist/shell/state-cli.js +2 -7
  75. package/dist/shell/state-explosion-cli.js +19 -10
  76. package/dist/shell/topology-io.js +38 -6
  77. package/dist/shell/trust-audit.js +264 -23
  78. package/dist/shell/trust-policy-io.js +1 -1
  79. package/dist/shell/worker-cli.js +41 -29
  80. package/dist/shell/worker-isolation.js +34 -9
  81. package/dist/shell/workflow-app-loader.js +3 -2
  82. package/dist/wiring/capability-table/basics.js +91 -0
  83. package/dist/wiring/capability-table/exec-backend.js +171 -0
  84. package/dist/wiring/capability-table/index.js +42 -0
  85. package/dist/wiring/capability-table/multi-agent.js +630 -0
  86. package/dist/wiring/capability-table/parity.js +467 -0
  87. package/dist/wiring/capability-table/pipeline.js +292 -0
  88. package/dist/wiring/capability-table/registry-core.js +208 -0
  89. package/dist/wiring/capability-table/reporting.js +435 -0
  90. package/dist/wiring/capability-table/scheduling-registry.js +592 -0
  91. package/dist/wiring/capability-table/state.js +181 -0
  92. package/dist/wiring/capability-table/trust-ledger.js +158 -0
  93. package/dist/wiring/capability-table/workflow-apps.js +381 -0
  94. package/docs/agent-delegation-drive.7.md +4 -0
  95. package/docs/candidate-scoring.7.md +1 -1
  96. package/docs/canonical-workflow-apps.7.md +7 -7
  97. package/docs/cli-mcp-parity.7.md +13 -3
  98. package/docs/contract-migration-tooling.7.md +4 -0
  99. package/docs/control-plane-scheduling.7.md +4 -0
  100. package/docs/coordinator-blackboard.7.md +17 -17
  101. package/docs/dogfood-one-real-repo.7.md +11 -11
  102. package/docs/durable-state-and-locking.7.md +24 -0
  103. package/docs/end-to-end-golden-path.7.md +14 -14
  104. package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
  105. package/docs/execution-backends.7.md +4 -0
  106. package/docs/getting-started.md +37 -37
  107. package/docs/index.md +1 -0
  108. package/docs/multi-agent-cli-mcp-surface.7.md +21 -17
  109. package/docs/multi-agent-eval-replay-harness.7.md +17 -13
  110. package/docs/multi-agent-operator-ux.7.md +23 -19
  111. package/docs/multi-agent-runtime-core.7.md +22 -22
  112. package/docs/multi-agent-topologies.7.md +6 -6
  113. package/docs/multi-agent-trust-policy-audit.7.md +11 -11
  114. package/docs/node-snapshot-diff-replay.7.md +4 -0
  115. package/docs/observability-cost-accounting.7.md +4 -0
  116. package/docs/operator-ux.7.md +34 -34
  117. package/docs/pipeline-runner.7.md +4 -4
  118. package/docs/project-index.md +48 -5
  119. package/docs/real-execution-backends.7.md +4 -0
  120. package/docs/release-and-migration.7.md +5 -1
  121. package/docs/release-tooling.7.md +4 -0
  122. package/docs/routines.md +4 -4
  123. package/docs/run-registry-control-plane.7.md +23 -19
  124. package/docs/run-retention-reclamation.7.md +4 -0
  125. package/docs/scheduled-tasks.md +14 -14
  126. package/docs/security-trust-hardening.7.md +43 -13
  127. package/docs/state-explosion-management.7.md +14 -10
  128. package/docs/team-collaboration.7.md +4 -0
  129. package/docs/trust-audit-anchor.7.md +71 -0
  130. package/docs/unix-principles.md +3 -1
  131. package/docs/verifier-gated-commit.7.md +1 -1
  132. package/docs/web-desktop-workbench.7.md +4 -0
  133. package/docs/workflow-app-framework.7.md +13 -13
  134. package/manifest/plugin.manifest.json +1 -1
  135. package/package.json +4 -2
  136. package/scripts/agents/claude-p-agent.js +2 -2
  137. package/scripts/block-unapproved-tag.sh +23 -2
  138. package/scripts/bump-version.js +24 -2
  139. package/scripts/canonical-apps.js +4 -4
  140. package/scripts/children/batch-delegate-child.js +52 -2
  141. package/scripts/dogfood-release.js +1 -1
  142. package/scripts/fake-date-for-reproduction.js +44 -0
  143. package/scripts/golden-path.js +4 -4
  144. package/scripts/purity-baseline.json +71 -0
  145. package/scripts/purity-gate.js +239 -0
  146. package/scripts/release-check.js +8 -1
  147. package/scripts/release-flow.js +57 -1
  148. package/scripts/verdict-keygen.js +83 -0
  149. package/scripts/verify-bump-reproduction.sh +148 -0
  150. package/scripts/verify-verdict-signature.js +61 -0
  151. package/scripts/version-sync-check.js +33 -12
  152. package/skills/cool-workflow/SKILL.md +9 -9
  153. package/skills/cool-workflow/references/commands.md +89 -88
  154. package/ui/workbench/app.css +37 -1
  155. package/ui/workbench/app.js +124 -6
  156. 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: `node scripts/cw.js multi-agent run ${runId} --topology map-reduce`, reason: "Materialize a host-facing multi-agent topology.", priority: "high" }];
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: `node scripts/cw.js multi-agent step ${runId}`, reason: "Create the next dispatch manifest without spawning workers.", priority: "high" }];
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: `node scripts/cw.js worker output ${runId} <worker-id> <result.md>`, reason: "A host-executed worker must report result evidence.", priority: "high" }];
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: `node scripts/cw.js multi-agent step ${runId}`, reason: "Collect fanin once required worker evidence is present.", priority: "high" }];
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: `node scripts/cw.js multi-agent score ${runId} --candidate <candidate-id> --criterion correctness=1 --evidence <path-or-ref>`, reason: "Score a candidate with explicit evidence.", priority: "high" }];
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: `node scripts/cw.js multi-agent select ${runId} --candidate <candidate-id> --reason "<rationale>"`, reason: "Select a scored candidate after verifier gates pass.", priority: "high" }];
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: `node scripts/cw.js commit ${runId} --selection ${ready.selectionId} --reason "<verified rationale>"`, reason: "Create a verifier-gated CW state commit.", priority: "high" }];
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: `node scripts/cw.js report ${runId} --show`, reason: "Review the completed run report.", priority: "normal" }];
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: `node scripts/cw.js multi-agent status ${runId}`, reason: active.length > 1 ? "Resolve ambiguous active topology state." : "Inspect specific blocked reasons.", priority: "high" }];
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
- `node scripts/cw.js multi-agent status ${run.id} --json`;
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))].filter(uniqueFilter),
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).filter(uniqueFilter),
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", `node scripts/cw.js worker manifest ${run.id} ${coverage.workerId || "<worker-id>"}`);
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.filter(uniqueById).sort((left, right) => left.from.localeCompare(right.from) || left.to.localeCompare(right.to));
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 = (state.memberships || []).filter((entry) => entry.roleId === role.id);
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`, `node scripts/cw.js multi-agent step ${run.id}`, role.id);
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}`, `node scripts/cw.js multi-agent status ${run.id} --json`, role.id);
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 ? workersOf(run).find((entry) => entry.id === membership.workerId) : undefined;
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}`, `node scripts/cw.js multi-agent membership ${run.id} ${membership.id}`, membership.roleId, membership.workerId);
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`, `node scripts/cw.js multi-agent step ${run.id}`, membership.roleId, membership.taskId);
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}`, `node scripts/cw.js worker show ${run.id} ${worker.id}`, membership.roleId, membership.id);
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`, `node scripts/cw.js worker manifest ${run.id} ${worker.id}`, membership.roleId, membership.id);
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, `node scripts/cw.js multi-agent failures ${run.id}`, fanin.groupId, fanin.fanoutId);
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}`, `node scripts/cw.js multi-agent step ${run.id}`, roleId, fanin.id);
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}`, `node scripts/cw.js multi-agent membership ${run.id} ${membershipId}`, membershipId, fanin.id);
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] || `node scripts/cw.js topology summary ${run.id}`, String(topology.id));
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}`, `node scripts/cw.js topology summary ${run.id}`, String(topology.id));
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), `node scripts/cw.js feedback show ${run.id} ${feedback.id}`, feedback.taskId, feedback.nodeId);
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}`, `node scripts/cw.js candidate show ${run.id} ${candidate.id}`, candidate.workerId, candidate.taskId);
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`, `node scripts/cw.js multi-agent score ${run.id} --candidate ${candidate.id} --evidence <path-or-ref>`, candidate.workerId, candidate.taskId);
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`, `node scripts/cw.js candidate show ${run.id} ${candidate.id}`, candidate.workerId, candidate.taskId);
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", `node scripts/cw.js multi-agent select ${run.id} --candidate <candidate-id> --reason "<rationale>"`);
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 || `node scripts/cw.js multi-agent status ${run.id} --json`);
337
- const readySelection = selectionsOf(run).find((selection) => !commitsOf(run).some((commit) => commit.selectionId === selection.id && commit.verifierGated));
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`, `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.localeCompare(right.kind) || left.id.localeCompare(right.id));
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.localeCompare(right.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 = selectionsOf(run).find((entry) => !commitsOf(run).some((commit) => commit.selectionId === entry.id && commit.verifierGated));
497
- return selection ? `node scripts/cw.js commit ${run.id} --selection ${selection.id} --reason "<verified rationale>"` : undefined;
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).filter(uniqueFilter).sort();
562
- }
563
- function uniqueFilter(value, index, values) {
564
- return values.indexOf(value) === index;
565
- }
566
- function uniqueById(value, index, values) {
567
- return values.findIndex((entry) => entry.id === value.id) === index;
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(value, index, values) {
570
- return values.findIndex((entry) => entry.id === value.id && entry.kind === value.kind) === index;
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();
@@ -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, `${snapshotId}.json`);
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 `<replayId>.json`. */
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", `${replayId}.json`);
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.localeCompare(b.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.localeCompare(b.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.localeCompare(b.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.localeCompare(b.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.localeCompare(b.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.localeCompare(b.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.localeCompare(b.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.localeCompare(b.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.localeCompare(b.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
- : `node scripts/cw.js metrics show ${run.id} --json`,
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.localeCompare(b.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].localeCompare(b[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,
@@ -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) && smokeFiles.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]) => a.localeCompare(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 || `node scripts/cw.js audit multi-agent ${String(view.runId || "<run-id>")} --json`)}`,
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
- ` node scripts/cw.js graph ${summary.runId}`,
302
- ` node scripts/cw.js worker summary ${summary.runId}`,
303
- ` node scripts/cw.js topology summary ${summary.runId}`,
304
- ` node scripts/cw.js topology graph ${summary.runId}`,
305
- ` node scripts/cw.js multi-agent summary ${summary.runId}`,
306
- ` node scripts/cw.js multi-agent graph ${summary.runId}`,
307
- ` node scripts/cw.js multi-agent dependencies ${summary.runId}`,
308
- ` node scripts/cw.js multi-agent failures ${summary.runId}`,
309
- ` node scripts/cw.js multi-agent evidence ${summary.runId}`,
310
- ` node scripts/cw.js blackboard summary ${summary.runId}`,
311
- ` node scripts/cw.js blackboard graph ${summary.runId}`,
312
- ` node scripts/cw.js coordinator summary ${summary.runId}`,
313
- ` node scripts/cw.js candidate summary ${summary.runId}`,
314
- ` node scripts/cw.js feedback summary ${summary.runId}`,
315
- ` node scripts/cw.js commit summary ${summary.runId}`,
316
- ` node scripts/cw.js audit summary ${summary.runId}`,
317
- ` node scripts/cw.js audit provenance ${summary.runId}`,
318
- ` node scripts/cw.js audit multi-agent ${summary.runId}`,
319
- ` node scripts/cw.js audit policy ${summary.runId}`,
320
- ` node scripts/cw.js audit blackboard ${summary.runId}`,
321
- ` node scripts/cw.js audit judge ${summary.runId}`,
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: "node scripts/cw.js plan <workflow-id> --repo <path>",
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 phaseTasks = run.tasks.filter((t) => phase.taskIds.includes(t.id));
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.localeCompare(right.id));
147
- const selections = [...(run.candidateSelections || [])].sort((left, right) => left.id.localeCompare(right.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.localeCompare(right.createdAt) || left.id.localeCompare(right.id));
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.localeCompare(b.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: `node scripts/cw.js feedback show ${run.id} ${feedback.id}`, reason: `Open ${feedback.severity} ${feedback.classification} feedback should be inspected before more dispatch.`, priority: "high" });
228
- actions.push({ command: `node scripts/cw.js feedback task ${run.id} ${feedback.id}`, reason: "Create a correction task if the feedback is actionable.", priority: "normal" });
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: `node scripts/cw.js feedback list ${run.id}`, reason: `Worker ${failedWorker.id} is ${failedWorker.status}; inspect linked feedback before retrying.`, priority: "high" });
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 ? `node scripts/cw.js worker manifest ${run.id} ${worker.id}` : `node scripts/cw.js worker list ${run.id}`, reason: "Running workers need their manifests inspected and final output recorded.", priority: "high" });
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: `node scripts/cw.js worker output ${run.id} ${worker.id} ${worker.resultPath}`, reason: "Record the worker result after its result.md is ready.", priority: "normal" });
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: `node scripts/cw.js dispatch ${run.id} --limit ${limit}`, reason: `${ctx.tasks.pending.length} pending task(s) are ready for the active phase.`, priority: "high" });
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: `node scripts/cw.js candidate register ${run.id} --worker ${completedWithoutCandidate.workerId}`, reason: "A completed worker result is available but has not been registered as a candidate.", priority: "high" });
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: `node scripts/cw.js 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" });
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: `node scripts/cw.js candidate rank ${run.id}`, reason: "Scored candidates can be ranked before selection.", priority: "high" });
264
- actions.push({ command: `node scripts/cw.js candidate select ${run.id} ${scoredWithoutSelection.id}`, reason: "Select the candidate once the ranking supports it.", priority: "normal" });
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: `node scripts/cw.js commit ${run.id} --selection ${ready.selectionId}`, reason: "A verified selected candidate is ready for a verifier-gated commit.", priority: "high" });
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: `node scripts/cw.js report ${run.id} --show`, reason: "All tracked phases are complete or no further operator action is currently available.", priority: "normal" });
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.localeCompare(b.kind) || a.id.localeCompare(b.id));
429
- const sortedEdges = edges.slice().sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || (a.label || "").localeCompare(b.label || ""));
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
  }