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
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ // core/format/completion.ts — formatCompletionScript.
3
+ //
4
+ // Pure function of already-loaded data (MORE_COMMANDS_TOKENS from
5
+ // help.ts). No new command list to hand-maintain: the completion word
6
+ // list is the SAME token set formatHelp()'s "More commands" section
7
+ // displays, plus the two top-level verbs that list doesn't carry
8
+ // ("help" and "doctor" — see help.ts's formatHelp comment on why "help"
9
+ // is absent from both places). Deliberately shallow: completes only
10
+ // TOP-LEVEL command names, not subcommands or flags — a v1 scope that
11
+ // still covers the single most common completion need (typing the verb)
12
+ // without hand-maintaining a second, deeper copy of the capability
13
+ // table's subcommand shape.
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.completionWords = completionWords;
16
+ exports.formatCompletionScript = formatCompletionScript;
17
+ const help_1 = require("./help");
18
+ /** The full top-level completion word list: `help`/`doctor` (absent from
19
+ * MORE_COMMANDS_TOKENS — see help.ts) plus every token formatHelp's
20
+ * "More commands" section lists (already includes `version`/`fix`). */
21
+ function completionWords() {
22
+ return ["help", "doctor", ...help_1.MORE_COMMANDS_TOKENS];
23
+ }
24
+ function bashScript(words) {
25
+ return [
26
+ "# cw bash completion — generated by `cw completion bash`.",
27
+ "# Install: eval \"$(cw completion bash)\" (e.g. in ~/.bashrc)",
28
+ "_cw_complete() {",
29
+ ' local cur="${COMP_WORDS[COMP_CWORD]}"',
30
+ " if [ \"$COMP_CWORD\" -eq 1 ]; then",
31
+ ` COMPREPLY=( $(compgen -W "${words.join(" ")}" -- "$cur") )`,
32
+ " fi",
33
+ "}",
34
+ "complete -F _cw_complete cw",
35
+ "",
36
+ ].join("\n");
37
+ }
38
+ function zshScript(words) {
39
+ return [
40
+ "#compdef cw",
41
+ "# cw zsh completion — generated by `cw completion zsh`.",
42
+ "# Install: cw completion zsh > \"${fpath[1]}/_cw\" (then restart the shell)",
43
+ `_arguments '1: :(${words.join(" ")})'`,
44
+ "",
45
+ ].join("\n");
46
+ }
47
+ function fishScript(words) {
48
+ return [
49
+ "# cw fish completion — generated by `cw completion fish`.",
50
+ "# Install: cw completion fish > ~/.config/fish/completions/cw.fish",
51
+ `complete -c cw -n "__fish_use_subcommand" -a "${words.join(" ")}"`,
52
+ "",
53
+ ].join("\n");
54
+ }
55
+ /** `cw completion <bash|zsh|fish>` — a static completion script for the
56
+ * top-level command word list. Throws on an unrecognized shell name
57
+ * (the CLI layer turns that into the standard `cw: <message>` /
58
+ * `Try:` shape, same as every other input-validation error). */
59
+ function formatCompletionScript(shell) {
60
+ const words = completionWords();
61
+ if (shell === "bash")
62
+ return bashScript(words);
63
+ if (shell === "zsh")
64
+ return zshScript(words);
65
+ if (shell === "fish")
66
+ return fishScript(words);
67
+ throw new Error(`Unknown shell: ${shell}\n Try: cw completion bash|zsh|fish`);
68
+ }
@@ -25,13 +25,22 @@
25
25
  // `tools/list` round-trip read the SAME table rows for any capability
26
26
  // wired into both.
27
27
  Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.MORE_COMMANDS_TOKENS = void 0;
28
29
  exports.formatHelp = formatHelp;
29
30
  exports.formatCommandHelp = formatCommandHelp;
30
31
  exports.formatInfo = formatInfo;
32
+ exports.formatSearchResults = formatSearchResults;
31
33
  const capability_table_1 = require("../capability-table");
32
- /** src/orchestrator.ts:934-951 — the exact "More commands" token set, in
33
- * this exact order (space-joined in the source, pipe-joined for display). */
34
- const MORE_COMMANDS_TOKENS = [
34
+ /** src/orchestrator.ts:934-951 — the "More commands" token set, in the old
35
+ * build's order (space-joined in the source, pipe-joined for display).
36
+ * One change from the old capture: `update` is gone. The verb had no code
37
+ * behind it in this build (`cw update` said "Unknown command"), so the
38
+ * help must not offer it. See parseargv.ts KNOWN_COMMANDS. Exported (only
39
+ * as a read-only source list, `formatHelp` below still owns the byte-
40
+ * pinned display format) so `core/format/completion.ts` can build a
41
+ * shell-completion word list from the SAME data instead of a third
42
+ * hand-maintained copy. */
43
+ exports.MORE_COMMANDS_TOKENS = [
35
44
  "list", "search", "info", "init", "plan", "status", "next", "dispatch",
36
45
  "result", "state", "commit", "report", "app", "sandbox", "backend",
37
46
  "contract", "node", "feedback", "worker", "audit", "candidate", "review",
@@ -40,7 +49,7 @@ const MORE_COMMANDS_TOKENS = [
40
49
  "summary", "blackboard", "coordinator", "metrics", "operator", "sched",
41
50
  "gc", "telemetry", "migration", "demo", "workbench", "approve", "reject",
42
51
  "comment", "handoff", "ledger", "graph", "eval", "man", "version",
43
- "update", "fix",
52
+ "fix", "completion",
44
53
  ];
45
54
  const MORE_COMMANDS_WRAP_WIDTH = 76;
46
55
  /** src/orchestrator.ts:940-951 — greedily pack pipe-joined tokens into
@@ -124,7 +133,14 @@ const COMMAND_HELP_ROWS = {
124
133
  { command: "cw sched complete", summary: "Complete a held lease (terminal success)." },
125
134
  { command: "cw sched reclaim", summary: "Reclaim expired leases (each counts a failed attempt)." },
126
135
  { command: "cw sched reset", summary: "Reset a parked entry to ready (operator recovery)." },
127
- { command: "cw sched policy", summary: "Show or set the scheduling policy (concurrency/attempts/backoff/TTL)." },
136
+ // Two rows for one command string, on purpose SPEC/cli-probe.md's
137
+ // "Odd things a rebuild must copy or fix on purpose" item 5 names
138
+ // `sched policy` as one of the show/set pairs a rebuild's help
139
+ // printer must keep doubled; this single merged row was a regression
140
+ // that lost the doubling (docs/rebuild/SPEC/cli-help/sched.txt has
141
+ // both), restored here with the old ground truth's exact wording.
142
+ { command: "cw sched policy", summary: "Show the scheduling policy (file or default)." },
143
+ { command: "cw sched policy", summary: "Set scheduling policy fields (concurrency/attempts/backoff/TTL)." },
128
144
  ],
129
145
  registry: [
130
146
  { command: "cw registry refresh", summary: "Recompute and persist the derived run registry index." },
@@ -205,16 +221,22 @@ const COMMAND_HELP_ROWS = {
205
221
  /** src/orchestrator.ts:899-933 — the top-level `cw help` text. Color is
206
222
  * intentionally NOT applied here (this milestone's conformance runs pipe
207
223
  * stdout, so NO_COLOR/non-TTY always wins); byte content matches the
208
- * plain-text capture at SPEC/cli-help/_root.txt exactly. */
224
+ * plain-text capture at SPEC/cli-help/_root.txt, but for the dead
225
+ * `update` lines, which were taken out on purpose (no code was behind
226
+ * the verb — see MORE_COMMANDS_TOKENS note above), for a `--json`
227
+ * Flags row, added because the old build's capture predates the ~68
228
+ * capability-table rows that now support `--json`/`--format json`
229
+ * (io.ts's wantsJson) — the flag existed but was never documented here —
230
+ * and for a `--quiet` Flags row, a new CLI spelling of the existing
231
+ * CW_DRIVE_PROGRESS=0 env var (see cli/entry.ts). */
209
232
  function formatHelp() {
210
- const moreCommandsLines = wrapPipeJoined(MORE_COMMANDS_TOKENS, MORE_COMMANDS_WRAP_WIDTH);
233
+ const moreCommandsLines = wrapPipeJoined(exports.MORE_COMMANDS_TOKENS, MORE_COMMANDS_WRAP_WIDTH);
211
234
  const lines = [
212
235
  "Cool Workflow",
213
236
  "",
214
237
  ' -q "question" [-claude|-codex|-gemini|-deepseek] Ask a question, get a report',
215
238
  ' -q "question" --link <url> Review a remote repo by URL',
216
239
  " version Show version",
217
- " update Update to latest release",
218
240
  " doctor Check setup",
219
241
  " fix Show fix commands for setup issues",
220
242
  "",
@@ -229,6 +251,8 @@ function formatHelp() {
229
251
  " --verbose Show full agent narration live (default: compact)",
230
252
  " --full Verbose, plus the report printed inline at the end",
231
253
  " --no-color Disable ANSI color (also honors NO_COLOR / FORCE_COLOR)",
254
+ " --json Print JSON for commands that support it",
255
+ " --quiet Suppress [drive] progress lines (not agent output)",
232
256
  "",
233
257
  "More commands",
234
258
  ...moreCommandsLines,
@@ -312,3 +336,21 @@ function formatInfo(appId, data) {
312
336
  lines.push(` Run: cw quickstart ${appId} --repo . --question "..."`);
313
337
  return lines.join("\n");
314
338
  }
339
+ /** `cw search <keyword>`'s human text — byte-exact to the milestone-1
340
+ * carry-over's own formatSearchResults (moved here from cli/dispatch.ts
341
+ * so the search capability-table row, which lives in core/, can render
342
+ * its own text without core importing from cli/). */
343
+ function formatSearchResults(keyword, results) {
344
+ if (results.length === 0) {
345
+ return `No workflows matched "${keyword}".\n Tip: cw list for all available workflows.`;
346
+ }
347
+ const lines = [`${results.length} workflow${results.length === 1 ? "" : "s"} matching "${keyword}"`];
348
+ for (const r of results) {
349
+ lines.push(` ${r.id} — ${r.title}`);
350
+ const cut = r.summary.length > 120 ? `${r.summary.slice(0, 119)}…` : r.summary;
351
+ lines.push(` ${cut}`);
352
+ }
353
+ lines.push("");
354
+ lines.push("Use cw info <id> for full details.");
355
+ return lines.join("\n");
356
+ }
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ // core/format/safe-json.ts — a byte-capped JSON.stringify shared by the CLI's
3
+ // `--json` output (cli/io.ts) and the MCP `tools/call` result (mcp/server.ts).
4
+ //
5
+ // Both surfaces JSON.stringify a caller-selected result with no size limit.
6
+ // An aggregate/dashboard capability (e.g. cw_workbench_view, cw comment
7
+ // list --json on a run with thousands of comments) can grow to hundreds of
8
+ // MB: a useless dump for a calling agent's context window at best, and at
9
+ // worst a RangeError ("Invalid string length") once the string crosses
10
+ // V8's per-string ceiling (~256M UTF-16 code units) — the same failure
11
+ // class PR #316 fixed for batch stdout reconciliation, here on the output
12
+ // side instead. Both `--json` and MCP callers already catch a thrown error
13
+ // and report it cleanly (cli/entry.ts's top-level catch; mcp/server.ts's
14
+ // tools/call try/catch), so this is not a crash fix so much as a "return
15
+ // something useful instead of a cryptic RangeError, or an unusable giant
16
+ // payload" fix.
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.MAX_JSON_OUTPUT_BYTES = void 0;
19
+ exports.safeJsonStringify = safeJsonStringify;
20
+ /** Generous but bounded: comfortably above any real cw result today, far
21
+ * below V8's ~256M-UTF-16-code-unit ceiling. */
22
+ exports.MAX_JSON_OUTPUT_BYTES = 10 * 1024 * 1024;
23
+ /** Overflow-notice `detail` (a caught error's message) is truncated to this
24
+ * many characters — the notice itself must stay small and predictable
25
+ * regardless of how large the underlying error's message was (e.g. an
26
+ * error wrapping a huge stored blackboard-message body), both so the
27
+ * notice can never itself approach any cap, and so JSON.stringify-ing the
28
+ * notice can never itself throw. */
29
+ const MAX_DETAIL_CHARS = 500;
30
+ /** JSON.stringify `value` the same way `JSON.stringify(value, null, 2)`
31
+ * always has. If that would exceed `maxBytes` (or throws — e.g. a
32
+ * RangeError from a value so large stringify itself blows V8's string
33
+ * ceiling), returns a small, valid JSON string describing the overflow
34
+ * instead of the real payload. Byte-identical to the un-capped form for
35
+ * every result under the cap — the common case is untouched.
36
+ *
37
+ * `JSON.stringify` returns the actual value `undefined` (not a string,
38
+ * despite its `string` return-type declaration) for `undefined`, a
39
+ * function, or a Symbol — never throws for those. That is passed through
40
+ * unchanged (matching the un-capped form's pre-existing behavior for this
41
+ * edge case: `content[0].text`/`printJson`'s `${text}` end up printing
42
+ * literally `undefined`) rather than measured as a byte length. */
43
+ function safeJsonStringify(value, maxBytes = exports.MAX_JSON_OUTPUT_BYTES) {
44
+ let text;
45
+ try {
46
+ text = JSON.stringify(value, null, 2);
47
+ }
48
+ catch (error) {
49
+ return JSON.stringify(overflowPayload(undefined, maxBytes, error), null, 2);
50
+ }
51
+ if (typeof text !== "string")
52
+ return text;
53
+ const bytes = Buffer.byteLength(text, "utf8");
54
+ if (bytes <= maxBytes)
55
+ return text;
56
+ return JSON.stringify(overflowPayload(bytes, maxBytes), null, 2);
57
+ }
58
+ function overflowPayload(bytes, maxBytes, error) {
59
+ const rawDetail = error instanceof Error ? error.message : error !== undefined ? String(error) : undefined;
60
+ const detail = rawDetail !== undefined
61
+ ? rawDetail.length > MAX_DETAIL_CHARS
62
+ ? `${rawDetail.slice(0, MAX_DETAIL_CHARS)}… (truncated)`
63
+ : rawDetail
64
+ : undefined;
65
+ return {
66
+ error: "result-too-large",
67
+ message: `This result is too large to return in full (${bytes !== undefined ? `${bytes} bytes` : "exceeds the size limit while serializing"}, ` +
68
+ `cap is ${maxBytes} bytes). Narrow the query (a --limit/--status/--since filter, or a more specific tool/verb) and try again.`,
69
+ capBytes: maxBytes,
70
+ ...(bytes !== undefined ? { actualBytes: bytes } : {}),
71
+ ...(detail ? { detail } : {}),
72
+ };
73
+ }
@@ -156,7 +156,7 @@ function stateExplosionReportLines(report) {
156
156
  lines.push(` - ${reason}`);
157
157
  if (report.compactGraph.collapsedNodeCount > 0) {
158
158
  lines.push(`- Graph compacted: ${report.compactGraph.collapsedNodeCount} nodes collapsed into ${report.compactGraph.syntheticNodes.length} summary nodes`);
159
- lines.push(` - Use: \`node scripts/cw.js multi-agent graph ${report.runId} --view full --json\``);
159
+ lines.push(` - Use: \`cw multi-agent graph ${report.runId} --view full --json\``);
160
160
  }
161
161
  if (report.hiddenSourceRecords.length) {
162
162
  for (const hidden of report.hiddenSourceRecords) {
@@ -123,7 +123,11 @@ function detectTies(candidates) {
123
123
  const groups = new Map();
124
124
  for (const candidate of candidates) {
125
125
  const key = String(candidate.normalized);
126
- groups.set(key, [...(groups.get(key) || []), candidate.candidateId]);
126
+ const group = groups.get(key);
127
+ if (group)
128
+ group.push(candidate.candidateId);
129
+ else
130
+ groups.set(key, [candidate.candidateId]);
127
131
  }
128
132
  return Array.from(groups.values()).filter((group) => group.length > 1);
129
133
  }
@@ -36,6 +36,7 @@ exports.listComments = listComments;
36
36
  exports.distinctTargets = distinctTargets;
37
37
  exports.formatReviewStatus = formatReviewStatus;
38
38
  exports.formatCommentList = formatCommentList;
39
+ const collate_1 = require("../util/collate");
39
40
  exports.COLLABORATION_SCHEMA_VERSION = 1;
40
41
  /** The single, honest stand-in for an absent identity. */
41
42
  exports.UNATTRIBUTED_ACTOR = {
@@ -213,7 +214,7 @@ function matchesAnyTarget(target, related) {
213
214
  return related.some((entry) => sameTarget(target, entry));
214
215
  }
215
216
  function compareByCreated(left, right) {
216
- return left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id);
217
+ return (0, collate_1.stableCompare)(left.createdAt, right.createdAt) || (0, collate_1.stableCompare)(left.id, right.id);
217
218
  }
218
219
  function disqualify(record, policy, selfIds) {
219
220
  const actor = record.actor;
@@ -414,16 +415,16 @@ function buildTimeline(state) {
414
415
  function buildNextActions(runId, states, policy) {
415
416
  const actions = [];
416
417
  if (!policy) {
417
- actions.push(`node scripts/cw.js review policy ${runId} --requiredApprovals 1 --authorizedRoles reviewer --appliesTo commit`);
418
+ actions.push(`cw review policy ${runId} --requiredApprovals 1 --authorizedRoles reviewer --appliesTo commit`);
418
419
  return actions;
419
420
  }
420
421
  for (const state of states) {
421
422
  if (state.status === "pending" || state.status === "blocked" || state.status === "unattributed") {
422
- actions.push(`node scripts/cw.js approve ${state.target.kind} ${runId} ${state.target.id} --role <authorized-role> --actor <id> --attested`);
423
+ actions.push(`cw approve ${state.target.kind} ${runId} ${state.target.id} --role <authorized-role> --actor <id> --attested`);
423
424
  }
424
425
  }
425
426
  if (!actions.length)
426
- actions.push(`node scripts/cw.js review status ${runId} --json`);
427
+ actions.push(`cw review status ${runId} --json`);
427
428
  return actions;
428
429
  }
429
430
  /** Self ids for a candidate/selection target: its producing worker +
@@ -453,7 +454,7 @@ function distinctTargets(state) {
453
454
  seen.set(targetKey(record.target), record.target);
454
455
  for (const record of state.handoffs)
455
456
  seen.set(targetKey(record.target), record.target);
456
- return [...seen.values()].sort((left, right) => targetKey(left).localeCompare(targetKey(right)));
457
+ return [...seen.values()].sort((left, right) => (0, collate_1.stableCompare)(targetKey(left), targetKey(right)));
457
458
  }
458
459
  function formatReviewStatus(report) {
459
460
  const lines = [];
@@ -47,6 +47,7 @@ exports.summarizeBlackboard = summarizeBlackboard;
47
47
  exports.listBlackboardMessages = listBlackboardMessages;
48
48
  exports.listBlackboardArtifacts = listBlackboardArtifacts;
49
49
  exports.buildBlackboardGraph = buildBlackboardGraph;
50
+ const collate_1 = require("../util/collate");
50
51
  exports.BLACKBOARD_SCHEMA_VERSION = 1;
51
52
  /** Dedup, SORTS. Coordinator-side sorting `unique` — byte-identical
52
53
  * behavior to core/multi-agent/runtime.ts's own copy, kept as a
@@ -420,7 +421,7 @@ function summarizeBlackboard(runId, state, blackboardId, defaultIndexPath) {
420
421
  ].sort();
421
422
  const readyForFanin = Boolean(board && !openQuestions.length && !conflicts.length && artifacts.length > 0 && missingEvidence.length === 0);
422
423
  const latestSnapshot = scoped(state.snapshots)
423
- .sort((left, right) => left.createdAt.localeCompare(right.createdAt))
424
+ .sort((left, right) => (0, collate_1.stableCompare)(left.createdAt, right.createdAt))
424
425
  .at(-1);
425
426
  return {
426
427
  runId,
@@ -442,24 +443,24 @@ function summarizeBlackboard(runId, state, blackboardId, defaultIndexPath) {
442
443
  }
443
444
  function nextAction(runId, board, openQuestions, conflicts, artifacts) {
444
445
  if (!board)
445
- return `node scripts/cw.js blackboard topic create ${runId} --id <topic-id> --title "<title>"`;
446
+ return `cw blackboard topic create ${runId} --id <topic-id> --title "<title>"`;
446
447
  if (conflicts.length)
447
- return `node scripts/cw.js coordinator decision ${runId} --kind conflict-resolution --outcome accepted --subject ${conflicts[0].id} --reason "<reason>"`;
448
+ return `cw coordinator decision ${runId} --kind conflict-resolution --outcome accepted --subject ${conflicts[0].id} --reason "<reason>"`;
448
449
  if (openQuestions.length)
449
- return `node scripts/cw.js blackboard message post ${runId} --topic ${openQuestions[0].topicId} --body "<answer with evidence>"`;
450
+ return `cw blackboard message post ${runId} --topic ${openQuestions[0].topicId} --body "<answer with evidence>"`;
450
451
  if (!artifacts.length)
451
- return `node scripts/cw.js blackboard artifact add ${runId} --path <path> --kind <kind>`;
452
- return `node scripts/cw.js blackboard snapshot ${runId}`;
452
+ return `cw blackboard artifact add ${runId} --path <path> --kind <kind>`;
453
+ return `cw blackboard snapshot ${runId}`;
453
454
  }
454
455
  function listBlackboardMessages(state, options = {}) {
455
456
  return state.messages
456
457
  .filter((message) => (!options.blackboardId || message.blackboardId === options.blackboardId) && (!options.topicId || message.topicId === options.topicId))
457
- .sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id));
458
+ .sort((left, right) => (0, collate_1.stableCompare)(left.createdAt, right.createdAt) || (0, collate_1.stableCompare)(left.id, right.id));
458
459
  }
459
460
  function listBlackboardArtifacts(state, options = {}) {
460
461
  return state.artifacts
461
462
  .filter((artifact) => (!options.blackboardId || artifact.blackboardId === options.blackboardId) && (!options.topicId || artifact.topicId === options.topicId))
462
- .sort((left, right) => left.id.localeCompare(right.id));
463
+ .sort((left, right) => (0, collate_1.stableCompare)(left.id, right.id));
463
464
  }
464
465
  function buildBlackboardGraph(runId, state, recordPath, messagesPath) {
465
466
  const nodes = [];
@@ -841,15 +841,15 @@ function summarizeMultiAgent(run) {
841
841
  function nextMultiAgentAction(run, blockedReasons) {
842
842
  const state = ensureMultiAgentState(run);
843
843
  if (!state.runs.length)
844
- return `node scripts/cw.js multi-agent run ${run.id} --id <multi-agent-run-id>`;
844
+ return `cw multi-agent run ${run.id} --id <multi-agent-run-id>`;
845
845
  if (blockedReasons.length)
846
- return `node scripts/cw.js multi-agent fanin ${run.id} --group <group-id> --fanout <fanout-id>`;
846
+ return `cw multi-agent fanin ${run.id} --group <group-id> --fanout <fanout-id>`;
847
847
  const running = state.memberships.find((membership) => membership.status === "running");
848
848
  if (running?.workerId)
849
- return `node scripts/cw.js worker manifest ${run.id} ${running.workerId}`;
849
+ return `cw worker manifest ${run.id} ${running.workerId}`;
850
850
  const groupWithoutFanin = state.groups.find((group) => group.membershipIds.length && !group.faninIds.length);
851
851
  if (groupWithoutFanin)
852
- return `node scripts/cw.js multi-agent fanin ${run.id} --group ${groupWithoutFanin.id}`;
852
+ return `cw multi-agent fanin ${run.id} --group ${groupWithoutFanin.id}`;
853
853
  return undefined;
854
854
  }
855
855
  function buildMultiAgentGraph(run) {
@@ -244,9 +244,9 @@ function topologyRunId(definition, taskIds, runId, sequence, hash) {
244
244
  }
245
245
  function nextActionsFor(runId, topologyRunId, fanoutId) {
246
246
  return [
247
- `node scripts/cw.js dispatch ${runId} --multi-agent-fanout ${fanoutId}`,
248
- `node scripts/cw.js multi-agent fanin ${runId} ${topologyRunId}-fanin --fanout ${fanoutId}`,
249
- `node scripts/cw.js topology summary ${runId}`,
247
+ `cw dispatch ${runId} --multi-agent-fanout ${fanoutId}`,
248
+ `cw multi-agent fanin ${runId} ${topologyRunId}-fanin --fanout ${fanoutId}`,
249
+ `cw topology summary ${runId}`,
250
250
  ];
251
251
  }
252
252
  function statusToNodeStatus(status) {
@@ -20,6 +20,7 @@ exports.formatCommitId = formatCommitId;
20
20
  exports.gateFailureSeq = gateFailureSeq;
21
21
  const evidence_grounding_1 = require("../trust/evidence-grounding");
22
22
  const result_normalize_1 = require("./result-normalize");
23
+ const collate_1 = require("../util/collate");
23
24
  /** The sandbox profile that accepted a candidate's worker output — the
24
25
  * worker's own profile when present, else the backing task's. Pure port of
25
26
  * the old build's src/gates.ts sandboxProfileForCandidate. */
@@ -97,7 +98,7 @@ function findSelectionNode(run, selectionId) {
97
98
  function latestSelectionForCandidate(run, candidateId) {
98
99
  return [...(run.candidateSelections || [])]
99
100
  .filter((s) => s.candidateId === candidateId)
100
- .sort((a, b) => (b.selectedAt || "").localeCompare(a.selectedAt || ""))[0];
101
+ .sort((a, b) => (0, collate_1.stableCompare)(b.selectedAt || "", a.selectedAt || ""))[0];
101
102
  }
102
103
  function evidenceLocatorString(entry) {
103
104
  const ref = entry.locator || entry.path || entry.summary || entry.id;
@@ -20,7 +20,14 @@ exports.formatDispatchId = formatDispatchId;
20
20
  * everything after it (`null`). */
21
21
  function firstRunnablePhase(run) {
22
22
  for (const phase of run.phases) {
23
- const phaseTasks = run.tasks.filter((task) => phase.taskIds.includes(task.id));
23
+ // A Set lookup, not `taskIds.includes()` re-scanned per task: this loop is
24
+ // O(phases x tasks), not O(phases x tasks x taskIds-per-phase) -- the
25
+ // latter degrades to O(tasks^2) since total taskIds across phases scales
26
+ // with total tasks, and this function is called several times per drive
27
+ // hop (once per hop directly, again inside selectDriveTask, again inside
28
+ // driveConcurrentRound), compounding to O(tasks^3) over a whole run.
29
+ const taskIds = new Set(phase.taskIds);
30
+ const phaseTasks = run.tasks.filter((task) => taskIds.has(task.id));
24
31
  if (phaseTasks.some((task) => task.status === "running"))
25
32
  return phase;
26
33
  if (phaseTasks.some((task) => task.status === "pending"))
@@ -34,7 +41,8 @@ function firstRunnablePhase(run) {
34
41
  * running when some task is running or completed, else pending. */
35
42
  function updatePhaseStatuses(run) {
36
43
  for (const phase of run.phases) {
37
- const phaseTasks = run.tasks.filter((task) => phase.taskIds.includes(task.id));
44
+ const taskIds = new Set(phase.taskIds);
45
+ const phaseTasks = run.tasks.filter((task) => taskIds.has(task.id));
38
46
  if (phaseTasks.length > 0 && phaseTasks.every((task) => task.status === "completed")) {
39
47
  phase.status = "completed";
40
48
  }
@@ -69,12 +77,18 @@ function formatDispatchTask(task) {
69
77
  };
70
78
  }
71
79
  /** `nextDispatchTasks(run, limit?)` — pending tasks of the first runnable
72
- * phase, capped, mapped through formatDispatchTask. */
80
+ * phase, capped, mapped through formatDispatchTask. `??`, not `||`: an
81
+ * explicit `limit: 0` (or a configured `maxConcurrentAgents: 0`) means
82
+ * "dispatch nothing", not "no limit was given" — `0 || fallback` used to
83
+ * silently replace a real zero with the fallback. Negative numbers are
84
+ * clamped to 0 before reaching `.slice()`, which otherwise treats a
85
+ * negative end index as "drop that many from the end" instead of "cap at
86
+ * this many". */
73
87
  function nextDispatchTasks(run, limit) {
74
88
  const runnablePhase = firstRunnablePhase(run);
75
89
  if (!runnablePhase)
76
90
  return [];
77
- const max = Number(limit || run.workflow.limits.maxConcurrentAgents || 4);
91
+ const max = Math.max(0, Math.floor(limit ?? run.workflow.limits.maxConcurrentAgents ?? 4));
78
92
  const runnableTaskIds = new Set(runnablePhase.taskIds);
79
93
  return run.tasks
80
94
  .filter((task) => task.status === "pending" && runnableTaskIds.has(task.id))
@@ -45,7 +45,8 @@ function selectDriveTask(run) {
45
45
  const phase = (0, dispatch_1.firstRunnablePhase)(run);
46
46
  if (!phase)
47
47
  return undefined;
48
- const phaseTasks = run.tasks.filter((task) => phase.taskIds.includes(task.id));
48
+ const taskIds = new Set(phase.taskIds);
49
+ const phaseTasks = run.tasks.filter((task) => taskIds.has(task.id));
49
50
  return phaseTasks.find((task) => task.status === "running") || phaseTasks.find((task) => task.status === "pending");
50
51
  }
51
52
  function countCompleted(run) {
@@ -49,6 +49,7 @@ exports.RUN_STATE_MIGRATIONS = void 0;
49
49
  exports.findMigrationPath = findMigrationPath;
50
50
  exports.migrateRunState = migrateRunState;
51
51
  exports.reverseRunState = reverseRunState;
52
+ exports.derivePhases = derivePhases;
52
53
  const path = __importStar(require("node:path"));
53
54
  const version_1 = require("../version");
54
55
  const schema_1 = require("./schema");
@@ -118,6 +119,12 @@ function findMigrationPath(steps, fromVersion, toVersion) {
118
119
  }
119
120
  return { reachable: false, path: [], error: `no migration path from schemaVersion ${fromVersion} to ${toVersion}` };
120
121
  }
122
+ /** Shared by migrateRunState/reverseRunState: missing EITHER `workflow` OR
123
+ * `paths` (or both) is the signal — a real run always has both together,
124
+ * so a lone surviving one of the two is not proof the rest wasn't lost. */
125
+ function computeSuspectedDataLoss(input) {
126
+ return !("workflow" in input) || !("paths" in input);
127
+ }
121
128
  function migrateRunState(input, options = {}) {
122
129
  const report = {
123
130
  status: "current",
@@ -130,12 +137,14 @@ function migrateRunState(input, options = {}) {
130
137
  changes: [],
131
138
  warnings: [],
132
139
  errors: [],
140
+ suspectedDataLoss: false,
133
141
  };
134
142
  if (!isRecord(input)) {
135
143
  report.status = "unsupported";
136
144
  report.errors.push("Run state must be a JSON object.");
137
145
  return { run: {}, report };
138
146
  }
147
+ report.suspectedDataLoss = computeSuspectedDataLoss(input);
139
148
  if (report.detectedSchemaVersion < version_1.MIN_SUPPORTED_RUN_STATE_SCHEMA_VERSION) {
140
149
  report.status = "unsupported";
141
150
  report.errors.push(`Unsupported run-state schemaVersion ${schemaVersionDescription(input, report.detectedSchemaVersion)}.`);
@@ -185,12 +194,14 @@ function reverseRunState(input, targetSchemaVersion, options = {}) {
185
194
  changes: [],
186
195
  warnings: [],
187
196
  errors: [],
197
+ suspectedDataLoss: false,
188
198
  };
189
199
  if (!isRecord(input)) {
190
200
  report.status = "unsupported";
191
201
  report.errors.push("Run state must be a JSON object.");
192
202
  return { run: {}, report };
193
203
  }
204
+ report.suspectedDataLoss = computeSuspectedDataLoss(input);
194
205
  if (targetSchemaVersion < version_1.MIN_SUPPORTED_RUN_STATE_SCHEMA_VERSION) {
195
206
  report.status = "unsupported";
196
207
  report.errors.push(`Target schemaVersion ${targetSchemaVersion} is below the minimum supported ${version_1.MIN_SUPPORTED_RUN_STATE_SCHEMA_VERSION}.`);
@@ -428,7 +439,11 @@ function derivePhases(tasks) {
428
439
  const taskId = stringValue(task.id);
429
440
  if (!taskId)
430
441
  continue;
431
- byPhase.set(phase, [...(byPhase.get(phase) || []), taskId]);
442
+ const taskIds = byPhase.get(phase);
443
+ if (taskIds)
444
+ taskIds.push(taskId);
445
+ else
446
+ byPhase.set(phase, [taskId]);
432
447
  }
433
448
  if (byPhase.size === 0)
434
449
  return [];