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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cool-workflow",
3
3
  "description": "A workflow control plane and run-time you are able to check: it sends out jobs in TypeScript, makes certain of work against facts before it goes through, puts state into fixed records, orders jobs by time, runs jobs again and again, gets a group of agents to do their parts together, and talks MCP. It gives the doing of the work to outside agents — it never runs the models itself.",
4
- "version": "0.2.2",
4
+ "version": "0.2.3",
5
5
  "author": {
6
6
  "name": "COOLWHITE LLC"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cool-workflow",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "A workflow control plane and run-time you are able to check: it sends out jobs in TypeScript, makes certain of work against facts before it goes through, puts state into fixed records, orders jobs by time, runs jobs again and again, gets a group of agents to do their parts together, and talks MCP. It gives the doing of the work to outside agents — it never runs the models itself.",
5
5
  "author": {
6
6
  "name": "COOLWHITE LLC"
@@ -3,7 +3,7 @@
3
3
  "id": "architecture-review",
4
4
  "title": "Architecture Review",
5
5
  "summary": "Map a repository architecture, assess risks, verify important findings, and synthesize an evidence-backed verdict.",
6
- "version": "0.2.2",
6
+ "version": "0.2.3",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "architecture-review-fast",
4
4
  "title": "Architecture Review Fast",
5
5
  "summary": "Run a shorter architecture review with parallel map and assess phases for faster first results.",
6
- "version": "0.2.2",
6
+ "version": "0.2.3",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "end-to-end-golden-path",
4
4
  "title": "End-to-End Golden Path",
5
5
  "summary": "Deterministic one-worker workflow app for proving the CW integration chain.",
6
- "version": "0.2.2",
6
+ "version": "0.2.3",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "pr-review-fix-ci",
4
4
  "title": "PR Review Fix CI",
5
5
  "summary": "Review a pull request or branch, inspect CI failures, diagnose actionable issues, optionally patch, verify, and summarize with evidence.",
6
- "version": "0.2.2",
6
+ "version": "0.2.3",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "release-cut",
4
4
  "title": "Release Cut",
5
5
  "summary": "Prepare a release with checklist discipline: version checks, changelog, tests, packaging, release notes, and final verification.",
6
- "version": "0.2.2",
6
+ "version": "0.2.3",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -3,7 +3,7 @@
3
3
  "id": "research-synthesis",
4
4
  "title": "Research Synthesis",
5
5
  "summary": "Split a research question into claims, investigate sources, cross-check evidence, verify claims, and synthesize a concise answer.",
6
- "version": "0.2.2",
6
+ "version": "0.2.3",
7
7
  "author": "COOLWHITE LLC",
8
8
  "inputs": [
9
9
  {
@@ -54,8 +54,14 @@ function renderCliResult(result, jsonMode, options) {
54
54
  /** Tries the capability table for `args`. Matches the row whose
55
55
  * `cli.path` is `[command]` or `[command, positionals[0]]` (the only two
56
56
  * path lengths any milestone-2 row uses); returns true when a table row
57
- * handled the command. */
58
- function dispatchTable(args) {
57
+ * handled the command. `await`ing the handler's result is a no-op for
58
+ * the ~199 rows whose handler returns a plain CliHandlerResult (an
59
+ * `await` on a non-Promise value just resolves on the next microtask,
60
+ * invisible to any caller that itself awaits dispatch()) -- it only
61
+ * matters for the live drive rows, which return a real Promise so their
62
+ * round loop can actually stay interruptible (see shell/drive.ts's
63
+ * driveAsync). */
64
+ async function dispatchTable(args) {
59
65
  const candidates = [[args.command]];
60
66
  if (args.positionals.length > 0)
61
67
  candidates.push([args.command, args.positionals[0]]);
@@ -68,7 +74,7 @@ function dispatchTable(args) {
68
74
  positionals: args.positionals.slice(consumed),
69
75
  options: args.options,
70
76
  };
71
- const result = row.cli.handler(cliArgs);
77
+ const result = await row.cli.handler(cliArgs);
72
78
  renderCliResult(result, row.cli.jsonMode, args.options);
73
79
  return true;
74
80
  }
@@ -161,8 +167,8 @@ function dispatchLegacy(args) {
161
167
  *
162
168
  * Tries the capability table FIRST (real rows always win), then falls
163
169
  * back to the milestone-1 legacy switch for verbs not yet migrated. */
164
- function dispatch(args) {
165
- if (dispatchTable(args))
170
+ async function dispatch(args) {
171
+ if (await dispatchTable(args))
166
172
  return;
167
173
  dispatchLegacy(args);
168
174
  }
package/dist/cli/entry.js CHANGED
@@ -33,6 +33,9 @@ const io_1 = require("./io");
33
33
  * comes from core/format/help.ts either way. */
34
34
  function printVersion() {
35
35
  const row = (0, capability_table_1.findCapability)("version");
36
+ // `version`'s own handler is a pure, synchronous projection (never one of
37
+ // the live drive rows) -- safe to read `.text` straight off it rather than
38
+ // route through the generic (possibly-async) dispatch path.
36
39
  const result = row?.cli?.handler({ positionals: [], options: {} });
37
40
  process.stdout.write(result?.text ?? "");
38
41
  }
@@ -88,6 +91,21 @@ async function runCli(argv = process.argv.slice(2)) {
88
91
  process.env.CW_NO_COLOR = "1";
89
92
  if (args.options.full)
90
93
  process.env.CW_OUTPUT = "full";
94
+ // --quiet is a documented CLI spelling of the existing CW_DRIVE_PROGRESS=0
95
+ // env var (shell/drive.ts's emitProgress) — it only silences the terse
96
+ // "[drive] " progress lines, not the end-of-run summary or any other
97
+ // Rule of Silence gate point (SPEC/reporting-ux.md's 3 gate points are
98
+ // each independent; --verbose/--full don't touch them either). It also
99
+ // does NOT reach two OTHER TTY-gated narration channels a user might
100
+ // reasonably expect it to quiet: live raw agent stderr streaming
101
+ // (shell/execution-backend/agent.ts's shouldStreamAgentStderr, gated by
102
+ // CW_AGENT_STREAM/CW_NO_STREAM/isTTY) and the local backend's
103
+ // "● Running…"/"✓ Done" lines (shell/execution-backend/local.ts, gated
104
+ // by isTTY with no env override at all). Folding those in is a larger,
105
+ // separate change — this flag's help text says "not agent output" on
106
+ // purpose so the scope is clear without reading this comment.
107
+ if (args.options.quiet)
108
+ process.env.CW_DRIVE_PROGRESS = "0";
91
109
  // `cw <verb> --help` / `-h` -> per-command help
92
110
  // (src/cli/command-surface.ts:80-83).
93
111
  if ((args.options.help || args.options.h) && args.command && !args.command.startsWith("-")) {
@@ -103,13 +121,35 @@ async function runCli(argv = process.argv.slice(2)) {
103
121
  else if (!args.command && typeof args.options.question === "string") {
104
122
  args.command = "quickstart";
105
123
  }
106
- (0, dispatch_1.dispatch)(args);
124
+ await (0, dispatch_1.dispatch)(args);
125
+ }
126
+ /** Broken pipe (`cw ... --json | head`): when the reader at the other end
127
+ * of a pipe goes away early, a write to stdout gives an async 'error'
128
+ * event that no promise .catch can see — without a listener node comes
129
+ * down hard with a stack trace. Raw writes are all over the CLI (help,
130
+ * version, printJson, human text), so ONE process-level listener here
131
+ * covers every write point: EPIPE says "the reader has all it needs",
132
+ * so stop quietly with code 0. Any other stream error is thrown again
133
+ * and still comes up as an uncaughtException, same as before. */
134
+ function exitQuietOnEpipe(stream) {
135
+ stream.on("error", (error) => {
136
+ if (error && error.code === "EPIPE")
137
+ process.exit(0);
138
+ throw error;
139
+ });
107
140
  }
108
141
  /** Top-level run wrapper matching src/cli.ts's catch shape byte-for-byte:
109
142
  * `cw: <message>\n` then, only when a hint matches, ` Try: <hint>\n` on
110
143
  * stderr; `process.exitCode = 1` (never a hard `process.exit`). */
111
144
  function main(argv = process.argv.slice(2)) {
145
+ exitQuietOnEpipe(process.stdout);
146
+ exitQuietOnEpipe(process.stderr);
112
147
  return runCli(argv).catch((error) => {
148
+ // On some platforms a broken-pipe write throws EPIPE in the write call
149
+ // itself (not as a stream 'error' event) — same broken pipe, same quiet
150
+ // exit 0, never a `cw: write EPIPE` line.
151
+ if (error?.code === "EPIPE")
152
+ return;
113
153
  const message = error instanceof Error ? error.message : String(error);
114
154
  process.stderr.write(`${(0, term_1.bold)("cw:", process.stderr)} ${(0, term_1.red)(message, process.stderr)}\n`);
115
155
  const hint = recoveryHint(message);
package/dist/cli/io.js CHANGED
@@ -16,6 +16,7 @@ exports.optionalArg = optionalArg;
16
16
  exports.printJson = printJson;
17
17
  exports.wantsJson = wantsJson;
18
18
  const help_1 = require("../core/format/help");
19
+ const safe_json_1 = require("../core/format/safe-json");
19
20
  const term_1 = require("../shell/term");
20
21
  /** Bold ONLY the fixed "Cool Workflow" header line of `formatHelp()`'s
21
22
  * plain text. */
@@ -34,9 +35,12 @@ function required(value, label) {
34
35
  function optionalArg(value) {
35
36
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
36
37
  }
37
- /** Machine payload to stdout (stdout = data; never colored, never chrome). */
38
+ /** Machine payload to stdout (stdout = data; never colored, never chrome).
39
+ * Byte-capped via safeJsonStringify — an aggregate result too large to be
40
+ * useful (or large enough to blow V8's string limit) prints a small
41
+ * overflow notice instead of hundreds of MB. */
38
42
  function printJson(value) {
39
- process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
43
+ process.stdout.write(`${(0, safe_json_1.safeJsonStringify)(value)}\n`);
40
44
  }
41
45
  /** True when the caller asked for JSON output (`--json` or `--format json`). */
42
46
  function wantsJson(options) {
@@ -116,6 +116,7 @@ exports.KNOWN_COMMANDS = new Set([
116
116
  "topology", "summary", "blackboard", "coordinator", "metrics", "operator",
117
117
  "sched", "gc", "telemetry", "migration", "demo", "workbench", "approve",
118
118
  "reject", "comment", "handoff", "graph", "eval", "version", "fix",
119
+ "completion",
119
120
  ]);
120
121
  /** Levenshtein edit distance between two strings. */
121
122
  function levenshtein(a, b) {
@@ -153,6 +153,7 @@ exports.MCP_TOOL_DATA = [
153
153
  { tool: "cw_coordinator_decision", capability: "coordinator.decision", requiredArgs: ["runId"], properties: ["runId", "cwd", "id", "kind", "outcome", "reason", "subject", "evidence", "artifact", "message"], description: "Record a coordinator decision." },
154
154
  { tool: "cw_audit_summary", capability: "audit.summary", requiredArgs: ["runId"], properties: ["runId", "cwd"], description: "Read the trust/audit summary." },
155
155
  { tool: "cw_audit_verify", capability: "audit.verify", requiredArgs: ["runId"], properties: ["runId", "cwd", "expectHead", "expectCount"], description: "Re-prove a run's trust-audit hash chain (fail-closed exit)." },
156
+ { tool: "cw_audit_repair", capability: "audit.repair", requiredArgs: ["runId"], properties: ["runId", "cwd", "write", "expectHead", "expectCount"], description: "Repair a torn trailing write in a run's trust-audit event log (dry-run unless write is set; fails closed if the corruption is not confined to the trailing line, or if expectHead/expectCount is given and not met)." },
156
157
  { tool: "cw_audit_worker", capability: "audit.worker", requiredArgs: ["runId"], properties: ["runId", "cwd", "workerId"], description: "Read trust/audit for one worker." },
157
158
  { tool: "cw_audit_provenance", capability: "audit.provenance", requiredArgs: ["runId"], properties: ["runId", "cwd", "workerId", "worker", "candidateId", "candidate", "commitId", "commit"], description: "Inspect evidence provenance." },
158
159
  { tool: "cw_audit_multi_agent", capability: "audit.multi-agent", requiredArgs: ["runId"], properties: ["runId", "cwd"], description: "Read the multi-agent trust/policy/provenance audit." },
@@ -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,6 +25,7 @@
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;
@@ -34,8 +35,12 @@ const capability_table_1 = require("../capability-table");
34
35
  * build's order (space-joined in the source, pipe-joined for display).
35
36
  * One change from the old capture: `update` is gone. The verb had no code
36
37
  * behind it in this build (`cw update` said "Unknown command"), so the
37
- * help must not offer it. See parseargv.ts KNOWN_COMMANDS. */
38
- const MORE_COMMANDS_TOKENS = [
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 = [
39
44
  "list", "search", "info", "init", "plan", "status", "next", "dispatch",
40
45
  "result", "state", "commit", "report", "app", "sandbox", "backend",
41
46
  "contract", "node", "feedback", "worker", "audit", "candidate", "review",
@@ -44,7 +49,7 @@ const MORE_COMMANDS_TOKENS = [
44
49
  "summary", "blackboard", "coordinator", "metrics", "operator", "sched",
45
50
  "gc", "telemetry", "migration", "demo", "workbench", "approve", "reject",
46
51
  "comment", "handoff", "ledger", "graph", "eval", "man", "version",
47
- "fix",
52
+ "fix", "completion",
48
53
  ];
49
54
  const MORE_COMMANDS_WRAP_WIDTH = 76;
50
55
  /** src/orchestrator.ts:940-951 — greedily pack pipe-joined tokens into
@@ -128,7 +133,14 @@ const COMMAND_HELP_ROWS = {
128
133
  { command: "cw sched complete", summary: "Complete a held lease (terminal success)." },
129
134
  { command: "cw sched reclaim", summary: "Reclaim expired leases (each counts a failed attempt)." },
130
135
  { command: "cw sched reset", summary: "Reset a parked entry to ready (operator recovery)." },
131
- { 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)." },
132
144
  ],
133
145
  registry: [
134
146
  { command: "cw registry refresh", summary: "Recompute and persist the derived run registry index." },
@@ -211,9 +223,14 @@ const COMMAND_HELP_ROWS = {
211
223
  * stdout, so NO_COLOR/non-TTY always wins); byte content matches the
212
224
  * plain-text capture at SPEC/cli-help/_root.txt, but for the dead
213
225
  * `update` lines, which were taken out on purpose (no code was behind
214
- * the verb — see MORE_COMMANDS_TOKENS note above). */
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). */
215
232
  function formatHelp() {
216
- const moreCommandsLines = wrapPipeJoined(MORE_COMMANDS_TOKENS, MORE_COMMANDS_WRAP_WIDTH);
233
+ const moreCommandsLines = wrapPipeJoined(exports.MORE_COMMANDS_TOKENS, MORE_COMMANDS_WRAP_WIDTH);
217
234
  const lines = [
218
235
  "Cool Workflow",
219
236
  "",
@@ -234,6 +251,8 @@ function formatHelp() {
234
251
  " --verbose Show full agent narration live (default: compact)",
235
252
  " --full Verbose, plus the report printed inline at the end",
236
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)",
237
256
  "",
238
257
  "More commands",
239
258
  ...moreCommandsLines,
@@ -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
  }
@@ -415,16 +415,16 @@ function buildTimeline(state) {
415
415
  function buildNextActions(runId, states, policy) {
416
416
  const actions = [];
417
417
  if (!policy) {
418
- 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`);
419
419
  return actions;
420
420
  }
421
421
  for (const state of states) {
422
422
  if (state.status === "pending" || state.status === "blocked" || state.status === "unattributed") {
423
- 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`);
424
424
  }
425
425
  }
426
426
  if (!actions.length)
427
- actions.push(`node scripts/cw.js review status ${runId} --json`);
427
+ actions.push(`cw review status ${runId} --json`);
428
428
  return actions;
429
429
  }
430
430
  /** Self ids for a candidate/selection target: its producing worker +
@@ -443,14 +443,14 @@ function summarizeBlackboard(runId, state, blackboardId, defaultIndexPath) {
443
443
  }
444
444
  function nextAction(runId, board, openQuestions, conflicts, artifacts) {
445
445
  if (!board)
446
- 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>"`;
447
447
  if (conflicts.length)
448
- 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>"`;
449
449
  if (openQuestions.length)
450
- 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>"`;
451
451
  if (!artifacts.length)
452
- return `node scripts/cw.js blackboard artifact add ${runId} --path <path> --kind <kind>`;
453
- 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}`;
454
454
  }
455
455
  function listBlackboardMessages(state, options = {}) {
456
456
  return state.messages
@@ -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,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) {