cool-workflow 0.2.2 → 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 (136) 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 +11 -5
  10. package/dist/cli/entry.js +41 -1
  11. package/dist/cli/io.js +6 -2
  12. package/dist/cli/parseargv.js +1 -0
  13. package/dist/core/capability-data.js +1 -0
  14. package/dist/core/format/completion.js +68 -0
  15. package/dist/core/format/help.js +25 -6
  16. package/dist/core/format/safe-json.js +73 -0
  17. package/dist/core/format/state-explosion-text.js +1 -1
  18. package/dist/core/multi-agent/candidate-scoring.js +5 -1
  19. package/dist/core/multi-agent/collaboration.js +3 -3
  20. package/dist/core/multi-agent/coordinator.js +5 -5
  21. package/dist/core/multi-agent/runtime.js +4 -4
  22. package/dist/core/multi-agent/topology.js +3 -3
  23. package/dist/core/pipeline/dispatch.js +18 -4
  24. package/dist/core/pipeline/drive-decide.js +2 -1
  25. package/dist/core/state/migrations.js +16 -1
  26. package/dist/core/state/state-explosion/digest.js +13 -13
  27. package/dist/core/state/state-explosion/graph.js +17 -11
  28. package/dist/core/state/state-explosion/report.js +6 -6
  29. package/dist/core/util/numeric-flag.js +40 -0
  30. package/dist/core/version.js +1 -1
  31. package/dist/mcp/server.js +99 -11
  32. package/dist/shell/audit-cli.js +57 -25
  33. package/dist/shell/coordinator-io.js +73 -13
  34. package/dist/shell/dispatch.js +1 -1
  35. package/dist/shell/doctor.js +80 -1
  36. package/dist/shell/drive.js +243 -49
  37. package/dist/shell/eval-text.js +2 -2
  38. package/dist/shell/evidence-reasoning.js +4 -4
  39. package/dist/shell/execution-backend/agent.js +20 -1
  40. package/dist/shell/execution-backend/container.js +4 -1
  41. package/dist/shell/execution-backend/local.js +4 -1
  42. package/dist/shell/feedback-cli.js +6 -6
  43. package/dist/shell/fs-atomic.js +218 -29
  44. package/dist/shell/man-cli.js +6 -0
  45. package/dist/shell/metrics-cli.js +2 -1
  46. package/dist/shell/multi-agent-cli.js +367 -323
  47. package/dist/shell/multi-agent-host.js +9 -9
  48. package/dist/shell/multi-agent-operator-ux.js +80 -38
  49. package/dist/shell/node-store.js +10 -4
  50. package/dist/shell/observability.js +1 -1
  51. package/dist/shell/operator-ux-text.js +22 -22
  52. package/dist/shell/operator-ux.js +15 -14
  53. package/dist/shell/orchestrator.js +49 -38
  54. package/dist/shell/pipeline-cli.js +87 -40
  55. package/dist/shell/reclamation-io.js +14 -4
  56. package/dist/shell/registry-cli.js +19 -17
  57. package/dist/shell/remote-source.js +13 -8
  58. package/dist/shell/report-cli.js +45 -0
  59. package/dist/shell/report.js +2 -1
  60. package/dist/shell/run-registry-io.js +77 -19
  61. package/dist/shell/run-store.js +74 -2
  62. package/dist/shell/scheduling-io.js +12 -13
  63. package/dist/shell/state-cli.js +2 -7
  64. package/dist/shell/state-explosion-cli.js +17 -9
  65. package/dist/shell/topology-io.js +36 -5
  66. package/dist/shell/trust-audit.js +224 -22
  67. package/dist/shell/trust-policy-io.js +1 -1
  68. package/dist/shell/worker-cli.js +35 -31
  69. package/dist/wiring/capability-table/basics.js +33 -8
  70. package/dist/wiring/capability-table/exec-backend.js +20 -11
  71. package/dist/wiring/capability-table/multi-agent.js +172 -165
  72. package/dist/wiring/capability-table/parity.js +1 -0
  73. package/dist/wiring/capability-table/pipeline.js +54 -42
  74. package/dist/wiring/capability-table/registry-core.js +29 -10
  75. package/dist/wiring/capability-table/reporting.js +137 -96
  76. package/dist/wiring/capability-table/scheduling-registry.js +195 -161
  77. package/dist/wiring/capability-table/state.js +36 -28
  78. package/dist/wiring/capability-table/trust-ledger.js +46 -22
  79. package/dist/wiring/capability-table/workflow-apps.js +83 -68
  80. package/docs/agent-delegation-drive.7.md +2 -0
  81. package/docs/candidate-scoring.7.md +1 -1
  82. package/docs/canonical-workflow-apps.7.md +7 -7
  83. package/docs/cli-mcp-parity.7.md +8 -3
  84. package/docs/contract-migration-tooling.7.md +2 -0
  85. package/docs/control-plane-scheduling.7.md +2 -0
  86. package/docs/coordinator-blackboard.7.md +17 -17
  87. package/docs/dogfood-one-real-repo.7.md +11 -11
  88. package/docs/durable-state-and-locking.7.md +2 -0
  89. package/docs/end-to-end-golden-path.7.md +14 -14
  90. package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
  91. package/docs/execution-backends.7.md +2 -0
  92. package/docs/getting-started.md +37 -37
  93. package/docs/multi-agent-cli-mcp-surface.7.md +19 -17
  94. package/docs/multi-agent-eval-replay-harness.7.md +15 -13
  95. package/docs/multi-agent-operator-ux.7.md +21 -19
  96. package/docs/multi-agent-runtime-core.7.md +22 -22
  97. package/docs/multi-agent-topologies.7.md +6 -6
  98. package/docs/multi-agent-trust-policy-audit.7.md +11 -11
  99. package/docs/node-snapshot-diff-replay.7.md +2 -0
  100. package/docs/observability-cost-accounting.7.md +2 -0
  101. package/docs/operator-ux.7.md +34 -34
  102. package/docs/pipeline-runner.7.md +4 -4
  103. package/docs/project-index.md +27 -4
  104. package/docs/real-execution-backends.7.md +2 -0
  105. package/docs/release-and-migration.7.md +3 -1
  106. package/docs/release-tooling.7.md +2 -0
  107. package/docs/routines.md +4 -4
  108. package/docs/run-registry-control-plane.7.md +21 -19
  109. package/docs/run-retention-reclamation.7.md +2 -0
  110. package/docs/scheduled-tasks.md +14 -14
  111. package/docs/security-trust-hardening.7.md +15 -15
  112. package/docs/state-explosion-management.7.md +12 -10
  113. package/docs/team-collaboration.7.md +2 -0
  114. package/docs/trust-audit-anchor.7.md +2 -0
  115. package/docs/unix-principles.md +3 -1
  116. package/docs/verifier-gated-commit.7.md +1 -1
  117. package/docs/web-desktop-workbench.7.md +2 -0
  118. package/docs/workflow-app-framework.7.md +13 -13
  119. package/manifest/plugin.manifest.json +1 -1
  120. package/package.json +2 -2
  121. package/scripts/agents/claude-p-agent.js +2 -2
  122. package/scripts/block-unapproved-tag.sh +23 -2
  123. package/scripts/canonical-apps.js +4 -4
  124. package/scripts/children/batch-delegate-child.js +52 -2
  125. package/scripts/dogfood-release.js +1 -1
  126. package/scripts/fake-date-for-reproduction.js +44 -0
  127. package/scripts/golden-path.js +4 -4
  128. package/scripts/purity-baseline.json +3 -0
  129. package/scripts/release-flow.js +57 -1
  130. package/scripts/verdict-keygen.js +83 -0
  131. package/scripts/verify-bump-reproduction.sh +148 -0
  132. package/scripts/verify-verdict-signature.js +61 -0
  133. package/skills/cool-workflow/SKILL.md +9 -9
  134. package/skills/cool-workflow/references/commands.md +89 -88
  135. package/ui/workbench/app.css +37 -1
  136. package/ui/workbench/app.js +124 -6
@@ -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 = {}) {
@@ -106,13 +106,13 @@ function summarizeMultiAgentOperator(run) {
106
106
  multiAgent.nextAction ||
107
107
  blackboard.nextAction ||
108
108
  readyCommitCommand(run) ||
109
- `node scripts/cw.js multi-agent status ${run.id} --json`;
109
+ `cw multi-agent status ${run.id} --json`;
110
110
  return {
111
111
  schemaVersion: 1,
112
112
  runId: run.id,
113
- 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))])],
114
114
  topologyRunIds: [...activeTopologyIds],
115
- topologyIds: topologies.active.map((entry) => entry.topologyId).filter(uniqueFilter),
115
+ topologyIds: [...new Set(topologies.active.map((entry) => entry.topologyId))],
116
116
  groups: (state.groups || []).map((entry) => entry.id).sort(),
117
117
  roles: (state.roles || []).map((entry) => entry.id).sort(),
118
118
  memberships: (state.memberships || []).map((entry) => entry.id).sort(),
@@ -261,7 +261,7 @@ function deriveDependencies(run) {
261
261
  for (const fanin of state.fanins || []) {
262
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");
263
263
  for (const coverage of fanin.evidenceCoverage || []) {
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", `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>"}`);
265
265
  }
266
266
  }
267
267
  for (const candidate of candidatesOf(run)) {
@@ -276,7 +276,7 @@ function deriveDependencies(run) {
276
276
  for (const commit of commitsOf(run)) {
277
277
  add(commit.selectionId ? `${run.id}:selection:${commit.selectionId}` : undefined, String(commit.stateNodeId || `${run.id}:commit:${commit.id}`), "commits", commit.verifierGated ? "committed" : "checkpoint");
278
278
  }
279
- return rows.filter(uniqueById).sort((left, right) => (0, collate_1.stableCompare)(left.from, right.from) || (0, collate_1.stableCompare)(left.to, right.to));
279
+ return uniqueById(rows).sort((left, right) => (0, collate_1.stableCompare)(left.from, right.from) || (0, collate_1.stableCompare)(left.to, right.to));
280
280
  }
281
281
  function deriveFailures(run, dependencies) {
282
282
  const rows = [];
@@ -284,61 +284,74 @@ function deriveFailures(run, dependencies) {
284
284
  rows.push({ id, kind, status, owner, linked, reason, nextCommand });
285
285
  };
286
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]));
287
300
  for (const role of state.roles || []) {
288
- const memberships = (state.memberships || []).filter((entry) => entry.roleId === role.id);
301
+ const memberships = membershipsByRole.get(role.id) || [];
289
302
  if (!memberships.length && role.status !== "completed" && role.status !== "cancelled") {
290
- 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);
291
304
  }
292
305
  if (role.status === "blocked" || role.status === "cancelled")
293
- 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);
294
307
  }
295
308
  for (const membership of state.memberships || []) {
296
- const worker = membership.workerId ? workersOf(run).find((entry) => entry.id === membership.workerId) : undefined;
309
+ const worker = membership.workerId ? workersById.get(membership.workerId) : undefined;
297
310
  if (membership.status === "failed" || membership.status === "cancelled")
298
- 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);
299
312
  if (!membership.workerId)
300
- 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);
301
314
  if (worker && (worker.status === "failed" || worker.status === "rejected"))
302
- 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);
303
316
  if (worker && (worker.status === "allocated" || worker.status === "running"))
304
- 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);
305
318
  }
306
319
  for (const fanin of state.fanins || []) {
307
320
  for (const reason of fanin.blockedReasons || [])
308
- 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);
309
322
  for (const roleId of fanin.missingRoleIds || [])
310
- 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);
311
324
  for (const membershipId of fanin.missingMembershipIds || [])
312
- 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);
313
326
  }
314
327
  for (const topology of topologyRunsOf(run)) {
315
328
  for (const missing of topology.missingEvidence || [])
316
- 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));
317
330
  if (topology.status === "blocked" || topology.status === "failed")
318
- 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));
319
332
  }
320
333
  for (const feedback of feedbackOf(run)) {
321
334
  if (feedback.status === "open" || feedback.status === "tasked")
322
- 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);
323
336
  }
324
337
  for (const candidate of candidatesOf(run)) {
325
338
  const scores = candidate.scores || [];
326
339
  if (candidate.status === "rejected" || candidate.status === "failed")
327
- 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);
328
341
  if (!scores.length && candidate.status !== "rejected" && candidate.status !== "failed")
329
- 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);
330
343
  if (!candidate.verifierNodeId)
331
- 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);
332
345
  }
333
346
  if (candidatesOf(run).some((candidate) => (candidate.scores || []).length) && !selectionsOf(run).length) {
334
- 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>"`);
335
348
  }
336
349
  for (const dep of dependencies.filter((entry) => entry.status === "blocked"))
337
- add(dep.id, "ambiguous-dependency", dep.status, dep.reason || "dependency is blocked", dep.nextCommand || `node scripts/cw.js multi-agent status ${run.id} --json`);
338
- 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);
339
352
  if (readySelection)
340
- add(String(readySelection.id), "commit-gate", "not-ready", `selection ${readySelection.id} has no verifier-gated commit`, `node scripts/cw.js commit ${run.id} --selection ${readySelection.id} --reason "<verified rationale>"`, readySelection.candidateId);
341
- return rows.filter(uniqueByFailure).sort((left, right) => (0, collate_1.stableCompare)(left.kind, right.kind) || (0, collate_1.stableCompare)(left.id, 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));
342
355
  }
343
356
  function deriveEvidence(run) {
344
357
  const rows = new Map();
@@ -493,9 +506,18 @@ function readScores(run, candidateId) {
493
506
  .sort()
494
507
  .map((file) => JSON.parse(fs.readFileSync(path.join(dir, file), "utf8")));
495
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
+ }
496
518
  function readyCommitCommand(run) {
497
- const selection = selectionsOf(run).find((entry) => !commitsOf(run).some((commit) => commit.selectionId === entry.id && commit.verifierGated));
498
- 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;
499
521
  }
500
522
  function normalizeEvidenceStatus(row) {
501
523
  if (row.rejectedBy.length)
@@ -559,16 +581,36 @@ function isTerminal(status) {
559
581
  return status === "completed" || status === "failed" || status === "cancelled";
560
582
  }
561
583
  function unique(values) {
562
- return values.filter(Boolean).filter(uniqueFilter).sort();
563
- }
564
- function uniqueFilter(value, index, values) {
565
- return values.indexOf(value) === index;
566
- }
567
- function uniqueById(value, index, values) {
568
- 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;
569
602
  }
570
- function uniqueByFailure(value, index, values) {
571
- 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;
572
614
  }
573
615
  function uniqueEdges(edges) {
574
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
  }
@@ -431,7 +431,7 @@ function deriveMetricsReport(run, options) {
431
431
  collaboration: deriveCollaborationMetrics(run),
432
432
  nextAction: totals.unreportedUnits > 0 && totals.attestedUnits === 0
433
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)."
434
- : `node scripts/cw.js metrics show ${run.id} --json`,
434
+ : `cw metrics show ${run.id} --json`,
435
435
  };
436
436
  }
437
437
  function metricsDir(run) {
@@ -152,7 +152,7 @@ function formatMultiAgentTrustAudit(view) {
152
152
  ...formatAuditEventRows(arrayView(view.policyViolations)),
153
153
  "",
154
154
  "Next Action",
155
- ` ${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`)}`,
156
156
  ].join("\n");
157
157
  }
158
158
  /** `Topologies` panel — port of the old build's formatTopologyPanel
@@ -299,27 +299,27 @@ function formatOperatorReport(summary, evidencePaths) {
299
299
  (0, multi_agent_operator_ux_1.formatMultiAgentEvidence)(summary.multiAgentOperator.evidence),
300
300
  "",
301
301
  "Resource Commands",
302
- ` node scripts/cw.js graph ${summary.runId}`,
303
- ` node scripts/cw.js worker summary ${summary.runId}`,
304
- ` node scripts/cw.js topology summary ${summary.runId}`,
305
- ` node scripts/cw.js topology graph ${summary.runId}`,
306
- ` node scripts/cw.js multi-agent summary ${summary.runId}`,
307
- ` node scripts/cw.js multi-agent graph ${summary.runId}`,
308
- ` node scripts/cw.js multi-agent dependencies ${summary.runId}`,
309
- ` node scripts/cw.js multi-agent failures ${summary.runId}`,
310
- ` node scripts/cw.js multi-agent evidence ${summary.runId}`,
311
- ` node scripts/cw.js blackboard summary ${summary.runId}`,
312
- ` node scripts/cw.js blackboard graph ${summary.runId}`,
313
- ` node scripts/cw.js coordinator summary ${summary.runId}`,
314
- ` node scripts/cw.js candidate summary ${summary.runId}`,
315
- ` node scripts/cw.js feedback summary ${summary.runId}`,
316
- ` node scripts/cw.js commit summary ${summary.runId}`,
317
- ` node scripts/cw.js audit summary ${summary.runId}`,
318
- ` node scripts/cw.js audit provenance ${summary.runId}`,
319
- ` node scripts/cw.js audit multi-agent ${summary.runId}`,
320
- ` node scripts/cw.js audit policy ${summary.runId}`,
321
- ` node scripts/cw.js audit blackboard ${summary.runId}`,
322
- ` 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}`,
323
323
  ].join("\n");
324
324
  }
325
325
  function groupBy(values, key) {
@@ -115,7 +115,7 @@ function summarizeRun(run) {
115
115
  function adviseNoRun() {
116
116
  return [
117
117
  {
118
- command: "node scripts/cw.js plan <workflow-id> --repo <path>",
118
+ command: "cw plan <workflow-id> --repo <path>",
119
119
  reason: "No run id is available yet; create a workflow run before dispatching or recording evidence.",
120
120
  priority: "high",
121
121
  },
@@ -123,7 +123,8 @@ function adviseNoRun() {
123
123
  }
124
124
  function summarizePhases(run) {
125
125
  return run.phases.map((phase) => {
126
- 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));
127
128
  const byStatus = countBy(phaseTasks, (t) => t.status);
128
129
  return { id: phase.id, name: phase.name, status: phase.status, tasks: { total: phaseTasks.length, ...byStatus } };
129
130
  });
@@ -225,52 +226,52 @@ function adviseNextSteps(run, ctx) {
225
226
  const openFeedback = feedbackRecords.filter((record) => record.status === "open");
226
227
  if (openFeedback.length) {
227
228
  const feedback = openFeedback[0];
228
- 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" });
229
- 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" });
230
231
  return actions;
231
232
  }
232
233
  const failedWorker = ctx.workers.workers.find((w) => w.status === "failed" || w.status === "rejected");
233
234
  if (failedWorker) {
234
- 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" });
235
236
  return actions;
236
237
  }
237
238
  if (ctx.tasks.running.length) {
238
239
  const worker = ctx.workers.workers.find((w) => w.status === "running" || w.status === "allocated");
239
- 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" });
240
241
  if (worker && worker.resultPath) {
241
- 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" });
242
243
  }
243
244
  return actions;
244
245
  }
245
246
  if (ctx.tasks.pending.length) {
246
247
  const limit = Math.min(ctx.tasks.pending.length, run.workflow.limits?.maxConcurrentAgents || 4);
247
- 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" });
248
249
  return actions;
249
250
  }
250
251
  const candidateRecords = (run.candidates || []);
251
252
  const selectionRecords = (run.candidateSelections || []);
252
253
  const completedWithoutCandidate = (run.tasks || []).find((task) => task.status === "completed" && task.workerId && !candidateRecords.some((candidate) => candidate.workerId === task.workerId));
253
254
  if (completedWithoutCandidate?.workerId) {
254
- 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" });
255
256
  return actions;
256
257
  }
257
258
  const unscored = candidateRecords.find((candidate) => candidate.status === "registered" && !(candidate.scores || []).length);
258
259
  if (unscored) {
259
- 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" });
260
261
  return actions;
261
262
  }
262
263
  const scoredWithoutSelection = candidateRecords.find((candidate) => candidate.status === "scored" && !selectionRecords.some((selection) => selection.candidateId === candidate.id));
263
264
  if (scoredWithoutSelection) {
264
- actions.push({ command: `node scripts/cw.js candidate rank ${run.id}`, reason: "Scored candidates can be ranked before selection.", priority: "high" });
265
- 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" });
266
267
  return actions;
267
268
  }
268
269
  if (ctx.candidates.readyForCommit.length) {
269
270
  const ready = ctx.candidates.readyForCommit[0];
270
- 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" });
271
272
  return actions;
272
273
  }
273
- 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" });
274
275
  return actions;
275
276
  }
276
277
  /** `cw operator status <id> --json` — a wider payload than `summarizeRun`
@@ -102,18 +102,22 @@ class CoolWorkflowRunner {
102
102
  /** `dispatch` — build the next dispatch manifest (persisting through
103
103
  * createDispatchManifest's own writes) and checkpoint. */
104
104
  dispatch(runId, options = {}) {
105
- const run = this.loadRun(runId);
106
- const limit = numberOption(options.limit);
107
- const manifest = (0, dispatch_1.createDispatchManifest)(run, limit, {
108
- sandboxProfileId: stringOption(options.sandbox) || stringOption(options.sandboxProfile) || stringOption(options.sandboxProfileId),
109
- backendId: stringOption(options.backend) || stringOption(options.backendId) || stringOption(options.executionBackend),
110
- multiAgentRunId: stringOption(options.multiAgentRun || options.multiAgentRunId || options["multi-agent-run"]),
111
- multiAgentGroupId: stringOption(options.multiAgentGroup || options.multiAgentGroupId || options.group || options["multi-agent-group"]),
112
- multiAgentRoleId: stringOption(options.multiAgentRole || options.multiAgentRoleId || options.role || options["multi-agent-role"]),
113
- multiAgentFanoutId: stringOption(options.multiAgentFanout || options.multiAgentFanoutId || options.fanout || options["multi-agent-fanout"]),
105
+ // Hold the state.json lock across the whole load -> change -> save so a
106
+ // concurrent run mutation cannot drop this dispatch (lost-update class,
107
+ // matching pipeline-cli.ts's dispatchRun).
108
+ return (0, run_store_2.withRunStateLock)(runId, this.cwd(), (run) => {
109
+ const limit = numberOption(options.limit);
110
+ const manifest = (0, dispatch_1.createDispatchManifest)(run, limit, {
111
+ sandboxProfileId: stringOption(options.sandbox) || stringOption(options.sandboxProfile) || stringOption(options.sandboxProfileId),
112
+ backendId: stringOption(options.backend) || stringOption(options.backendId) || stringOption(options.executionBackend),
113
+ multiAgentRunId: stringOption(options.multiAgentRun || options.multiAgentRunId || options["multi-agent-run"]),
114
+ multiAgentGroupId: stringOption(options.multiAgentGroup || options.multiAgentGroupId || options.group || options["multi-agent-group"]),
115
+ multiAgentRoleId: stringOption(options.multiAgentRole || options.multiAgentRoleId || options.role || options["multi-agent-role"]),
116
+ multiAgentFanoutId: stringOption(options.multiAgentFanout || options.multiAgentFanoutId || options.fanout || options["multi-agent-fanout"]),
117
+ });
118
+ (0, run_store_2.saveCheckpoint)(run);
119
+ return manifest;
114
120
  });
115
- (0, run_store_2.saveCheckpoint)(run);
116
- return manifest;
117
121
  }
118
122
  /** `showWorkerManifest` — write + return a worker's manifest. */
119
123
  showWorkerManifest(runId, workerId) {
@@ -126,10 +130,13 @@ class CoolWorkflowRunner {
126
130
  /** `recordWorkerOutput` — accept a worker's result and checkpoint. Mirrors
127
131
  * v2's workerOutputCli: recordWorkerOutput + saveCheckpoint. */
128
132
  recordWorkerOutput(runId, workerId, resultPath, options = {}) {
129
- const run = this.loadRun(runId);
130
- const output = (0, worker_isolation_1.recordWorkerOutput)(run, workerId, this.resolveFromBase(resultPath), options);
131
- (0, run_store_2.saveCheckpoint)(run);
132
- return output;
133
+ // Hold the state.json lock across the whole load -> change -> save so a
134
+ // concurrent run mutation cannot drop this worker output (lost-update class).
135
+ return (0, run_store_2.withRunStateLock)(runId, this.cwd(), (run) => {
136
+ const output = (0, worker_isolation_1.recordWorkerOutput)(run, workerId, this.resolveFromBase(resultPath), options);
137
+ (0, run_store_2.saveCheckpoint)(run);
138
+ return output;
139
+ });
133
140
  }
134
141
  /** `auditSummary` — the trust-audit rollup. */
135
142
  auditSummary(runId) {
@@ -172,29 +179,33 @@ class CoolWorkflowRunner {
172
179
  * `{ runId, commit }` to match the old orchestrator's shape (the scripts
173
180
  * read `commitResult.commit`). */
174
181
  commit(runId, input = {}) {
175
- const run = this.loadRun(runId);
176
- const options = typeof input === "string" ? { reason: input } : input;
177
- const allowCheckpoint = Boolean(options.allowUnverifiedCheckpoint || options["allow-unverified-checkpoint"]);
178
- const hasGateOption = Boolean(options.verifier || options.verifierNode || options["verifier-node"] || options.candidate || options.selection);
179
- try {
180
- const commit = (0, commit_1.commitState)(run, {
181
- reason: stringOption(options.reason) || "manual",
182
- verifierNodeId: stringOption(options.verifier) || stringOption(options.verifierNode) || stringOption(options["verifier-node"]),
183
- candidateId: stringOption(options.candidate),
184
- selectionId: stringOption(options.selection),
185
- verifierGated: hasGateOption || !allowCheckpoint,
186
- allowUnverifiedCheckpoint: allowCheckpoint,
187
- source: "cli",
188
- });
189
- (0, report_1.writeReport)(run);
190
- (0, run_store_2.saveCheckpoint)(run);
191
- return { runId: run.id, commit };
192
- }
193
- catch (error) {
194
- (0, report_1.writeReport)(run);
195
- (0, run_store_2.saveCheckpoint)(run);
196
- throw error;
197
- }
182
+ // Hold the state.json lock across the whole load -> commit -> save (both
183
+ // the success and the fail-closed catch persist) so a concurrent run
184
+ // mutation cannot drop this commit (lost-update class).
185
+ return (0, run_store_2.withRunStateLock)(runId, this.cwd(), (run) => {
186
+ const options = typeof input === "string" ? { reason: input } : input;
187
+ const allowCheckpoint = Boolean(options.allowUnverifiedCheckpoint || options["allow-unverified-checkpoint"]);
188
+ const hasGateOption = Boolean(options.verifier || options.verifierNode || options["verifier-node"] || options.candidate || options.selection);
189
+ try {
190
+ const commit = (0, commit_1.commitState)(run, {
191
+ reason: stringOption(options.reason) || "manual",
192
+ verifierNodeId: stringOption(options.verifier) || stringOption(options.verifierNode) || stringOption(options["verifier-node"]),
193
+ candidateId: stringOption(options.candidate),
194
+ selectionId: stringOption(options.selection),
195
+ verifierGated: hasGateOption || !allowCheckpoint,
196
+ allowUnverifiedCheckpoint: allowCheckpoint,
197
+ source: "cli",
198
+ });
199
+ (0, report_1.writeReport)(run);
200
+ (0, run_store_2.saveCheckpoint)(run);
201
+ return { runId: run.id, commit };
202
+ }
203
+ catch (error) {
204
+ (0, report_1.writeReport)(run);
205
+ (0, run_store_2.saveCheckpoint)(run);
206
+ throw error;
207
+ }
208
+ });
198
209
  }
199
210
  /** `report` — write the run's report.md; returns `{ path }` (old shape). */
200
211
  report(runId) {