cool-workflow 0.2.4 → 0.2.6

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 (155) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/.gemini-plugin/mcp.json +10 -0
  4. package/.gemini-plugin/plugin.json +40 -0
  5. package/.opencode-plugin/mcp.json +10 -0
  6. package/.opencode-plugin/plugin.json +40 -0
  7. package/README.md +204 -48
  8. package/apps/architecture-review/app.json +1 -1
  9. package/apps/architecture-review-fast/app.json +1 -1
  10. package/apps/end-to-end-golden-path/app.json +1 -1
  11. package/apps/pr-review-fix-ci/app.json +1 -1
  12. package/apps/release-cut/app.json +1 -1
  13. package/apps/research-synthesis/app.json +1 -1
  14. package/dist/cli/dispatch.js +32 -4
  15. package/dist/cli/entry.js +11 -19
  16. package/dist/cli/global-flags.js +113 -0
  17. package/dist/cli/parseargv.js +7 -1
  18. package/dist/core/capability-data.js +337 -8
  19. package/dist/core/format/help.js +73 -3
  20. package/dist/core/format/recovery-hint.js +32 -0
  21. package/dist/core/multi-agent/collaboration.js +35 -6
  22. package/dist/core/multi-agent/runtime.js +7 -0
  23. package/dist/core/multi-agent/trust-policy.js +7 -1
  24. package/dist/core/pipeline/contract.js +7 -0
  25. package/dist/core/pipeline/error-feedback.js +2 -2
  26. package/dist/core/state/run-paths.js +3 -30
  27. package/dist/core/trust/evidence-grounding.js +13 -1
  28. package/dist/core/util/cli-args.js +22 -0
  29. package/dist/core/version.js +1 -1
  30. package/dist/mcp/dispatch.js +22 -2
  31. package/dist/mcp/server.js +215 -36
  32. package/dist/mcp/tool-process.js +181 -0
  33. package/dist/mcp-server.js +28 -1
  34. package/dist/shell/agent-config.js +11 -2
  35. package/dist/shell/audit-cli.js +1 -1
  36. package/dist/shell/commit.js +8 -2
  37. package/dist/shell/coordinator-io.js +73 -1
  38. package/dist/shell/drive.js +195 -128
  39. package/dist/shell/error-feedback-io.js +6 -0
  40. package/dist/shell/execution-backend/agent.js +201 -26
  41. package/dist/shell/execution-backend/container.js +44 -10
  42. package/dist/shell/execution-backend/local.js +33 -0
  43. package/dist/shell/fs-atomic.js +96 -12
  44. package/dist/shell/ledger-cli.js +9 -2
  45. package/dist/shell/metrics-cli.js +1 -1
  46. package/dist/shell/multi-agent-cli.js +9 -2
  47. package/dist/shell/multi-agent-operator-ux.js +1 -1
  48. package/dist/shell/observability.js +7 -4
  49. package/dist/shell/onramp.js +48 -5
  50. package/dist/shell/operator-ux.js +1 -1
  51. package/dist/shell/perf-trace.js +136 -0
  52. package/dist/shell/pipeline.js +3 -2
  53. package/dist/shell/reclamation-io.js +10 -9
  54. package/dist/shell/report-view-cli.js +2 -0
  55. package/dist/shell/run-export-cli.js +5 -2
  56. package/dist/shell/run-export.js +293 -31
  57. package/dist/shell/run-registry-io.js +8 -0
  58. package/dist/shell/run-store.js +183 -3
  59. package/dist/shell/scheduler-io.js +101 -10
  60. package/dist/shell/state-explosion-cli.js +4 -1
  61. package/dist/shell/telemetry-demo.js +1 -1
  62. package/dist/shell/telemetry-ledger-io.js +36 -24
  63. package/dist/shell/trust-audit.js +178 -36
  64. package/dist/shell/workbench-host.js +121 -10
  65. package/dist/shell/workbench.js +83 -6
  66. package/dist/shell/worker-isolation.js +1 -1
  67. package/dist/wiring/capability-table/basics.js +5 -0
  68. package/dist/wiring/capability-table/exec-backend.js +40 -22
  69. package/dist/wiring/capability-table/parity.js +10 -5
  70. package/dist/wiring/capability-table/pipeline.js +32 -0
  71. package/dist/wiring/capability-table/registry-core.js +26 -3
  72. package/dist/wiring/capability-table/reporting.js +7 -1
  73. package/dist/wiring/capability-table/scheduling-registry.js +8 -2
  74. package/dist/wiring/capability-table/trust-ledger.js +54 -12
  75. package/docs/agent-delegation-drive.7.md +17 -0
  76. package/docs/cli-mcp-parity.7.md +46 -5
  77. package/docs/contract-migration-tooling.7.md +4 -0
  78. package/docs/control-plane-scheduling.7.md +38 -0
  79. package/docs/cross-agent-ledger.7.md +20 -8
  80. package/docs/durable-state-and-locking.7.md +4 -0
  81. package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
  82. package/docs/execution-backends.7.md +4 -0
  83. package/docs/fix.7.md +4 -4
  84. package/docs/getting-started.md +40 -32
  85. package/docs/index.md +17 -0
  86. package/docs/launch/demo.tape +4 -3
  87. package/docs/mcp-app-surface.7.md +27 -0
  88. package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
  89. package/docs/multi-agent-eval-replay-harness.7.md +4 -0
  90. package/docs/multi-agent-operator-ux.7.md +4 -0
  91. package/docs/node-snapshot-diff-replay.7.md +4 -0
  92. package/docs/observability-cost-accounting.7.md +4 -0
  93. package/docs/project-index.md +53 -9
  94. package/docs/real-execution-backends.7.md +4 -0
  95. package/docs/release-and-migration.7.md +4 -0
  96. package/docs/release-tooling.7.md +12 -0
  97. package/docs/routine.7.md +22 -0
  98. package/docs/run-registry-control-plane.7.md +37 -9
  99. package/docs/run-retention-reclamation.7.md +12 -4
  100. package/docs/state-explosion-management.7.md +4 -0
  101. package/docs/team-collaboration.7.md +4 -0
  102. package/docs/trust-audit-anchor.7.md +4 -0
  103. package/docs/web-desktop-workbench.7.md +72 -2
  104. package/docs/workflow-app-framework.7.md +31 -0
  105. package/manifest/plugin.manifest.json +1 -1
  106. package/manifest/source-context-profiles.json +4 -3
  107. package/package.json +6 -3
  108. package/scripts/agents/agent-adapter-core.js +26 -0
  109. package/scripts/agents/claude-p-agent.js +4 -1
  110. package/scripts/agents/codex-agent.js +4 -0
  111. package/scripts/agents/cw-attest-wrap.js +1 -1
  112. package/scripts/agents/gemini-agent.js +4 -0
  113. package/scripts/agents/opencode-agent.js +5 -0
  114. package/scripts/block-unapproved-tag.js +160 -0
  115. package/scripts/canonical-apps.js +4 -4
  116. package/scripts/children/batch-delegate-child.js +72 -4
  117. package/scripts/children/http-batch-delegate-child.js +132 -0
  118. package/scripts/children/http-delegate-child.js +8 -0
  119. package/scripts/dogfood-release.js +2 -2
  120. package/scripts/fake-date-for-reproduction.js +1 -1
  121. package/scripts/gen-manifests.js +32 -61
  122. package/scripts/golden-path.js +4 -4
  123. package/scripts/lang-policy-check.js +82 -0
  124. package/scripts/parity-check.js +42 -23
  125. package/scripts/purity-baseline.json +0 -3
  126. package/scripts/purity-gate.js +16 -3
  127. package/scripts/release-check.js +2 -1
  128. package/scripts/release-flow.js +169 -41
  129. package/scripts/release-gate.js +208 -0
  130. package/scripts/release-oneclick.js +38 -7
  131. package/scripts/release-tags.js +47 -0
  132. package/scripts/schema-version-inventory.json +31 -0
  133. package/scripts/validate-run-state-schema.js +95 -4
  134. package/scripts/verdict-keygen.js +2 -2
  135. package/scripts/verify-bump-reproduction.js +193 -0
  136. package/scripts/verify-release-verdict.js +139 -0
  137. package/scripts/verify-verdict-signature.js +1 -1
  138. package/ui/workbench/app.css +50 -10
  139. package/ui/workbench/app.js +269 -34
  140. package/ui/workbench/index.html +4 -2
  141. package/ui/workbench/inspection.js +51 -0
  142. package/ui/workbench/navigation.js +44 -0
  143. package/docs/agent-framework.md +0 -177
  144. package/docs/designs/handoff-ledger.md +0 -145
  145. package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +0 -40
  146. package/docs/launch/launch-kit.md +0 -195
  147. package/docs/launch/pre-launch-checklist.md +0 -53
  148. package/docs/readme-v0.1.87-full.md +0 -301
  149. package/docs/routines.md +0 -101
  150. package/docs/scheduled-tasks.md +0 -80
  151. package/scripts/agents/claude-p-agent.sh +0 -9
  152. package/scripts/block-unapproved-tag.sh +0 -75
  153. package/scripts/release-gate.sh +0 -94
  154. package/scripts/verify-bump-reproduction.sh +0 -148
  155. package/scripts/verify-container-selfref.js +0 -64
@@ -16,23 +16,26 @@ const registry_core_1 = require("./registry-core");
16
16
  // their milestone-2 placeholder `mcp.handler` with the real body, exactly
17
17
  // as milestones 3/4 did for their own rows.
18
18
  // ---------------------------------------------------------------------
19
- const exec_backend_cli_1 = require("../../shell/exec-backend-cli");
20
19
  const cli_args_1 = require("../../core/util/cli-args");
21
- // This slice is required unconditionally at startup for every command;
22
- // load doctor/app-run-cli lazily so only doctor/fix/sandbox.choose/
23
- // sandbox.resolve/app.run handlers pay their require cost.
20
+ // This slice is required unconditionally at startup for every command; every
21
+ // shell import below is lazy (require()'d only inside the handler that
22
+ // actually uses it), so a command that never touches doctor/fix/sandbox/
23
+ // backend/app.run never pays any of these shell modules' require cost.
24
24
  function loadDoctor() {
25
25
  return require("../../shell/doctor");
26
26
  }
27
27
  function loadAppRunCli() {
28
28
  return require("../../shell/app-run-cli");
29
29
  }
30
+ function loadExecBackendCli() {
31
+ return require("../../shell/exec-backend-cli");
32
+ }
30
33
  (0, registry_core_1.attachCliBinding)("sandbox.list", {
31
34
  path: ["sandbox", "list"],
32
35
  jsonMode: "default",
33
- handler: (args) => ({ json: (0, exec_backend_cli_1.listSandboxProfilesCli)(args.options) }),
36
+ handler: (args) => ({ json: loadExecBackendCli().listSandboxProfilesCli(args.options) }),
34
37
  });
35
- registry_core_1.REGISTRY_BY_CAPABILITY.get("sandbox.list").mcp.handler = (args) => (0, exec_backend_cli_1.listSandboxProfilesCli)(args);
38
+ registry_core_1.REGISTRY_BY_CAPABILITY.get("sandbox.list").mcp.handler = (args) => loadExecBackendCli().listSandboxProfilesCli(args);
36
39
  // GAP #24: cw_sandbox_choose / cw_sandbox_resolve + cw_app_run were declared
37
40
  // MCP-only rows with the notYetImplemented placeholder handler. Wire them to
38
41
  // the ported shell bodies (both are MCP-only in the old build — no CLI path).
@@ -42,18 +45,18 @@ registry_core_1.REGISTRY_BY_CAPABILITY.get("app.run").mcp.handler = (args) => lo
42
45
  (0, registry_core_1.attachCliBinding)("sandbox.show", {
43
46
  path: ["sandbox", "show"],
44
47
  jsonMode: "default",
45
- handler: (args) => ({ json: (0, exec_backend_cli_1.showSandboxProfileCli)((0, cli_args_1.required)(args.positionals[0], "profile id"), args.options) }),
48
+ handler: (args) => ({ json: loadExecBackendCli().showSandboxProfileCli((0, cli_args_1.required)(args.positionals[0], "profile id"), args.options) }),
46
49
  });
47
- registry_core_1.REGISTRY_BY_CAPABILITY.get("sandbox.show").mcp.handler = (args) => (0, exec_backend_cli_1.showSandboxProfileCli)((0, cli_args_1.required)((0, cli_args_1.optionalArg)(args.profileId), "profile id"), args);
50
+ registry_core_1.REGISTRY_BY_CAPABILITY.get("sandbox.show").mcp.handler = (args) => loadExecBackendCli().showSandboxProfileCli((0, cli_args_1.required)((0, cli_args_1.optionalArg)(args.profileId), "profile id"), args);
48
51
  (0, registry_core_1.attachCliBinding)("sandbox.validate", {
49
52
  path: ["sandbox", "validate"],
50
53
  jsonMode: "default",
51
54
  handler: (args) => {
52
- const result = (0, exec_backend_cli_1.validateSandboxProfileCli)((0, cli_args_1.required)(args.positionals[0], "profile file"), args.options);
55
+ const result = loadExecBackendCli().validateSandboxProfileCli((0, cli_args_1.required)(args.positionals[0], "profile file"), args.options);
53
56
  return { json: result, exitCode: result.valid ? undefined : 1 };
54
57
  },
55
58
  });
56
- registry_core_1.REGISTRY_BY_CAPABILITY.get("sandbox.validate").mcp.handler = (args) => (0, exec_backend_cli_1.validateSandboxProfileCli)((0, cli_args_1.required)((0, cli_args_1.optionalArg)(args.profileFile), "profile file"), args);
59
+ registry_core_1.REGISTRY_BY_CAPABILITY.get("sandbox.validate").mcp.handler = (args) => loadExecBackendCli().validateSandboxProfileCli((0, cli_args_1.required)((0, cli_args_1.optionalArg)(args.profileFile), "profile file"), args);
57
60
  // PARITY: `sandbox.choose`/`sandbox.resolve` are BOTH-surface capabilities
58
61
  // per SPEC/mcp.md (old build cli.path ["sandbox","choose"]/["sandbox",
59
62
  // "resolve"]) — they were left MCP-only at GAP #24 (see the comment
@@ -74,21 +77,21 @@ registry_core_1.REGISTRY_BY_CAPABILITY.get("sandbox.validate").mcp.handler = (ar
74
77
  (0, registry_core_1.attachCliBinding)("backend.list", {
75
78
  path: ["backend", "list"],
76
79
  jsonMode: "default",
77
- handler: () => ({ json: (0, exec_backend_cli_1.listBackendsCli)() }),
80
+ handler: () => ({ json: loadExecBackendCli().listBackendsCli() }),
78
81
  });
79
- registry_core_1.REGISTRY_BY_CAPABILITY.get("backend.list").mcp.handler = () => (0, exec_backend_cli_1.listBackendsCli)();
82
+ registry_core_1.REGISTRY_BY_CAPABILITY.get("backend.list").mcp.handler = () => loadExecBackendCli().listBackendsCli();
80
83
  (0, registry_core_1.attachCliBinding)("backend.show", {
81
84
  path: ["backend", "show"],
82
85
  jsonMode: "default",
83
- handler: (args) => ({ json: (0, exec_backend_cli_1.showBackendCli)((0, cli_args_1.required)(args.positionals[0], "backend id")) }),
86
+ handler: (args) => ({ json: loadExecBackendCli().showBackendCli((0, cli_args_1.required)(args.positionals[0], "backend id")) }),
84
87
  });
85
- registry_core_1.REGISTRY_BY_CAPABILITY.get("backend.show").mcp.handler = (args) => (0, exec_backend_cli_1.showBackendCli)((0, cli_args_1.required)((0, cli_args_1.optionalArg)(args.backendId), "backend id"));
88
+ registry_core_1.REGISTRY_BY_CAPABILITY.get("backend.show").mcp.handler = (args) => loadExecBackendCli().showBackendCli((0, cli_args_1.required)((0, cli_args_1.optionalArg)(args.backendId), "backend id"));
86
89
  (0, registry_core_1.attachCliBinding)("backend.probe", {
87
90
  path: ["backend", "probe"],
88
91
  jsonMode: "default",
89
- handler: (args) => ({ json: (0, exec_backend_cli_1.probeBackendCli)(args.positionals[0], args.options) }),
92
+ handler: (args) => ({ json: loadExecBackendCli().probeBackendCli(args.positionals[0], args.options) }),
90
93
  });
91
- registry_core_1.REGISTRY_BY_CAPABILITY.get("backend.probe").mcp.handler = (args) => (0, exec_backend_cli_1.probeBackendCli)((0, cli_args_1.optionalArg)(args.backendId), args);
94
+ registry_core_1.REGISTRY_BY_CAPABILITY.get("backend.probe").mcp.handler = (args) => loadExecBackendCli().probeBackendCli((0, cli_args_1.optionalArg)(args.backendId), args);
92
95
  // `backend agent config [show]` = read-only; `backend agent config set
93
96
  // ...` = mutating. CLI path is ["backend", "agent"] (2 tokens, matching
94
97
  // dispatchTable's supported path lengths); the remaining positionals
@@ -102,8 +105,8 @@ registry_core_1.REGISTRY_BY_CAPABILITY.get("backend.probe").mcp.handler = (args)
102
105
  handler: (args) => {
103
106
  const action = args.positionals[1];
104
107
  if (action === "set")
105
- return { json: (0, exec_backend_cli_1.backendAgentConfigSet)(args.options) };
106
- return { json: (0, exec_backend_cli_1.backendAgentConfigShow)(args.options) };
108
+ return { json: loadExecBackendCli().backendAgentConfigSet(args.options) };
109
+ return { json: loadExecBackendCli().backendAgentConfigShow(args.options) };
107
110
  },
108
111
  });
109
112
  // `backend.agent.config.set` shares the SAME dispatch path/handler as
@@ -124,12 +127,12 @@ registry_core_1.REGISTRY_BY_CAPABILITY.get("backend.probe").mcp.handler = (args)
124
127
  handler: (args) => {
125
128
  const action = args.positionals[1];
126
129
  if (action === "set")
127
- return { json: (0, exec_backend_cli_1.backendAgentConfigSet)(args.options) };
128
- return { json: (0, exec_backend_cli_1.backendAgentConfigShow)(args.options) };
130
+ return { json: loadExecBackendCli().backendAgentConfigSet(args.options) };
131
+ return { json: loadExecBackendCli().backendAgentConfigShow(args.options) };
129
132
  },
130
133
  });
131
- registry_core_1.REGISTRY_BY_CAPABILITY.get("backend.agent.config.show").mcp.handler = (args) => (0, exec_backend_cli_1.backendAgentConfigShow)(args);
132
- registry_core_1.REGISTRY_BY_CAPABILITY.get("backend.agent.config.set").mcp.handler = (args) => (0, exec_backend_cli_1.backendAgentConfigSet)(args);
134
+ registry_core_1.REGISTRY_BY_CAPABILITY.get("backend.agent.config.show").mcp.handler = (args) => loadExecBackendCli().backendAgentConfigShow(args);
135
+ registry_core_1.REGISTRY_BY_CAPABILITY.get("backend.agent.config.set").mcp.handler = (args) => loadExecBackendCli().backendAgentConfigSet(args);
133
136
  // PARITY: `backend.agent.config.set` mutates $CW_HOME/agent-config.json
134
137
  // (secret-stripped) before returning the effective config; both surfaces
135
138
  // perform the same write, so it is a documented opt-out from the
@@ -154,6 +157,21 @@ registry_core_1.REGISTRY_BY_CAPABILITY.get("backend.agent.config.set").reason =
154
157
  const text = (0, cli_args_1.wantsJson)(args.options) ? undefined : args.options.fix ? `${doctor.formatDoctorFixes(report)}\n` : doctor.formatDoctorReport(report);
155
158
  return { json: report, text, exitCode: report.ok ? undefined : 1 };
156
159
  },
160
+ // UI/UX fix: `cw help doctor` used to list only the one-line summary
161
+ // above, so a first-run user had no way to learn these flags without
162
+ // reading source. Real flag names verified against this handler and
163
+ // the `runDoctor(args.options, ...)` call it makes (shell/doctor.ts).
164
+ flags: [
165
+ { name: "--onramp", summary: "Run the extra onramp checks (3-step quick start)." },
166
+ { name: "--fix", summary: "Print fix commands instead of the full report." },
167
+ { name: "--changed-from REF", summary: "Check the onramp only for the change since REF." },
168
+ { name: "--json", summary: "Print the report as JSON." },
169
+ ],
170
+ // The handler above and runDoctor (shell/doctor.ts) read exactly
171
+ // onramp/fix/changed-from/json plus the shared globals (cwd, format)
172
+ // — the list above is complete, so the dispatcher may warn about an
173
+ // unknown flag (TTY-only; see cli/global-flags.ts).
174
+ flagsComplete: true,
157
175
  }, "Environment diagnostics are inherently local to the CLI host — Node version, $PATH, $CW_HOME/cwd writability. An MCP client diagnosing the server process's environment is not meaningful; agents already receive the same readiness facts in their typed results (e.g. status: blocked, agentConfigured). Inspired by `brew doctor`.");
158
176
  (0, registry_core_1.addCliOnlyCapability)("fix", "Print consolidated fix commands for CW setup issues.", {
159
177
  path: ["fix"],
@@ -413,20 +413,25 @@ function lintRegistry() {
413
413
  }
414
414
  /**
415
415
  * Compare the declared registry against the ACTUAL surfaces and report every
416
- * fail-closed gap. `mcpTools` is the live `tools/list` result; `cliTokens` is the
417
- * set of `case "<token>"` strings parsed from the CLI source.
416
+ * fail-closed gap. `mcpTools` is the live `tools/list` result. `cliTokens` is
417
+ * optional test input for an independent token source; the table-driven live
418
+ * gate leaves it out and probes dispatcher reachability instead.
418
419
  */
419
420
  function buildParityReport(input) {
420
421
  const declaredTools = new Set((0, registry_core_1.declaredMcpTools)());
421
422
  const actualTools = new Set(input.mcpTools);
422
423
  const declaredTokens = new Set(declaredCliTokens());
423
- const actualTokens = new Set(input.cliTokens);
424
+ const actualTokens = new Set(input.cliTokens || []);
424
425
  const declaredHelpTokens = new Set(declaredCliHelpTokens());
425
426
  const actualHelpTokens = new Set(input.helpTokens || []);
426
427
  const missingMcpTools = [...declaredTools].filter((tool) => !actualTools.has(tool)).sort();
427
428
  const undeclaredMcpTools = [...actualTools].filter((tool) => !declaredTools.has(tool)).sort();
428
- const missingCliTokens = [...declaredTokens].filter((token) => !actualTokens.has(token)).sort();
429
- const undeclaredCliTokens = [...actualTokens].filter((token) => !declaredTokens.has(token)).sort();
429
+ const missingCliTokens = input.cliTokens
430
+ ? [...declaredTokens].filter((token) => !actualTokens.has(token)).sort()
431
+ : [];
432
+ const undeclaredCliTokens = input.cliTokens
433
+ ? [...actualTokens].filter((token) => !declaredTokens.has(token)).sort()
434
+ : [];
430
435
  const helpMissingCliTokens = input.helpTokens
431
436
  ? [...declaredHelpTokens].filter((token) => !actualHelpTokens.has(token)).sort()
432
437
  : [];
@@ -45,6 +45,13 @@ function loadCommitSummary() {
45
45
  // only so `cw help run` lists both capabilities.
46
46
  (0, registry_core_1.attachCliBinding)("run.drive.step", {
47
47
  path: ["run"],
48
+ // NOTE: the doubled "cw run drive" command column (this row + run.drive's
49
+ // preview row below) is pinned by SPEC/cli-probe.md "Odd things" item 5
50
+ // ("run drive (preview + drive)" — "A rebuild's help printer must keep
51
+ // these doubled rows"), same as `cw sched policy`. So the UI/UX fix for
52
+ // the two look-alike rows lives in the SUMMARIES (each now names its own
53
+ // invocation spelling — see the summary edits where these capabilities
54
+ // are declared), never in the row count or the command column.
48
55
  helpPath: ["run", "drive"],
49
56
  jsonMode: "default",
50
57
  handler: async (args) => {
@@ -124,6 +131,19 @@ function loadCommitSummary() {
124
131
  });
125
132
  registry_core_1.REGISTRY_BY_CAPABILITY.get("run.drive").mcp.handler = (args) => loadPipelineCli().runDrivePreview(args);
126
133
  registry_core_1.REGISTRY_BY_CAPABILITY.get("run.drive.step").mcp.handler = (args) => loadPipelineCli().runDriveStep(args);
134
+ // UI/UX fix: `cw help run` prints TWO "cw run drive" rows — the doubling
135
+ // itself is pinned by SPEC/cli-probe.md "Odd things" item 5 ("run drive
136
+ // (preview + drive)": a rebuild's help printer must keep these doubled
137
+ // rows), so the command column and the row count must not change. What CAN
138
+ // change is the summary text: it used to leave a reader no way to tell
139
+ // which spelling gets which behavior. Each summary now ends with the exact
140
+ // command form that reaches it. Help-page-only: `summary` is overridden on
141
+ // the Capability row, `mcp.description` (the tools/list byte surface) is
142
+ // already captured by buildMcpBinding and stays untouched.
143
+ registry_core_1.REGISTRY_BY_CAPABILITY.get("run.drive").summary =
144
+ "Preview the next agent-delegation drive step for a run (read-only, deterministic). Use: cw run drive <run-id>.";
145
+ registry_core_1.REGISTRY_BY_CAPABILITY.get("run.drive.step").summary =
146
+ "Drive a run by delegating each worker to the agent backend (plan->dispatch->fulfill->accept->commit; --once for one step). Use: cw run <app> --drive.";
127
147
  // PARITY: `run.drive.step` advances the run by spawning the external
128
148
  // agent per worker and recording attested output — not a read probe.
129
149
  // CLI (--drive/--step) and MCP route through the same drive() core; the
@@ -246,6 +266,18 @@ registry_core_1.REGISTRY_BY_CAPABILITY.get("run.restore").mcp.handler = (args) =
246
266
  const exitCode = (result.mode === "check" && result.ok === false) || bundleFailed ? 1 : undefined;
247
267
  return { json: result, exitCode };
248
268
  },
269
+ // UI/UX fix: `cw help quickstart` used to list only the one-line
270
+ // summary above, so a first-run user had no way to learn these flags
271
+ // without reading source. Real flag names verified against
272
+ // shell/pipeline-cli.ts's quickstartRun.
273
+ flags: [
274
+ { name: "--check", summary: "Check setup only; do not plan, run, or write anything." },
275
+ { name: "--preview", summary: "Show the next step, but do not run it." },
276
+ { name: "--bundle", summary: "Seal a finished run into a portable, self-checked bundle." },
277
+ { name: "--with-trust-key KEY", summary: "Add a public key to the bundle for a strict check." },
278
+ { name: "--resume", summary: "Move a stopped run forward one step, or to the end with --run ID." },
279
+ { name: "--link URL", summary: "Review a remote repo by its URL." },
280
+ ],
249
281
  }, "quickstart composes plan/runDrive/report; SPEC/mcp.md's declared cli-only list names it explicitly (no MCP peer). `audit-run` is a CLI-only alias of the same wrapper.", "quickstart");
250
282
  (0, registry_core_1.attachCliBinding)("dispatch", {
251
283
  path: ["dispatch"],
@@ -113,11 +113,13 @@ function buildMcpBinding(row) {
113
113
  // into its separate AND-groups here (the one place the row shape is turned
114
114
  // into the runtime McpBinding.requiredArgs contract).
115
115
  const requiredArgs = row.requiredArgs.flatMap((group) => group.split(",").map((entry) => entry.trim()).filter(Boolean));
116
+ const annotations = capability_data_1.MCP_TOOL_ANNOTATIONS[row.tool];
116
117
  return {
117
118
  tool: row.tool,
118
119
  requiredArgs: requiredArgs.length ? requiredArgs : undefined,
119
120
  properties: row.properties,
120
121
  description: row.description,
122
+ ...(annotations ? { annotations } : {}),
121
123
  handler,
122
124
  };
123
125
  }
@@ -179,7 +181,19 @@ function findCapabilityByCliPath(path) {
179
181
  function cliCapabilities() {
180
182
  return exports.REGISTRY.filter((row) => Boolean(row.cli));
181
183
  }
182
- /** `tools/list`'s exact array, in the pinned source order. */
184
+ /** `tools/list`'s exact array, in the pinned source order. Each
185
+ * property's shape comes from the first hit, in order: a per-tool
186
+ * `PROPERTY_OVERRIDES` entry, then the shared `COMMON_PROPERTY_TYPES`
187
+ * entry for that property name, then the plain `stringProperty`
188
+ * fallback for any name neither table covers.
189
+ *
190
+ * `inputSchema.required` is built from `row.mcp.requiredArgs`, which
191
+ * mcp/dispatch.ts's `requiredToolArguments` also reads: each array
192
+ * entry is one AND-required group, already split from the transcript's
193
+ * comma form (see `buildMcpBinding` above). A group holding `|` is an
194
+ * OR-group — at least one of the named keys must be present, so no
195
+ * single name from it can go into the flat, AND-only `required` list;
196
+ * a group with no `|` is one plain required name. */
183
197
  function mcpToolDefinitions() {
184
198
  const definitions = [];
185
199
  for (const row of exports.REGISTRY) {
@@ -188,12 +202,21 @@ function mcpToolDefinitions() {
188
202
  const overrides = capability_data_1.PROPERTY_OVERRIDES[row.mcp.tool] ?? {};
189
203
  const properties = {};
190
204
  for (const propName of row.mcp.properties) {
191
- properties[propName] = overrides[propName] ?? (0, capability_data_1.stringProperty)(propName);
205
+ properties[propName] = overrides[propName] ?? capability_data_1.COMMON_PROPERTY_TYPES[propName] ?? (0, capability_data_1.stringProperty)(propName);
192
206
  }
207
+ const required = (row.mcp.requiredArgs ?? []).filter((group) => !group.includes("|"));
193
208
  definitions.push({
194
209
  name: row.mcp.tool,
195
210
  description: row.mcp.description,
196
- inputSchema: { type: "object", properties, additionalProperties: true },
211
+ // Additive behavior hints (MCP_TOOL_ANNOTATIONS): present only for
212
+ // tools whose handler was checked by hand; omitted otherwise.
213
+ ...(row.mcp.annotations ? { annotations: row.mcp.annotations } : {}),
214
+ inputSchema: {
215
+ type: "object",
216
+ properties,
217
+ additionalProperties: true,
218
+ ...(required.length ? { required } : {}),
219
+ },
197
220
  });
198
221
  }
199
222
  return definitions;
@@ -325,7 +325,13 @@ registry_core_1.REGISTRY_BY_CAPABILITY.get("workbench.view").mcp.handler = (args
325
325
  // renderCliResult: it is synchronous, so a genuinely blocking serve
326
326
  // must perform its own stdout write and keep the event loop alive
327
327
  // rather than returning a CliHandlerResult at all.
328
- void host.run();
328
+ // run() already fails closed internally (a `cw:` line + exit 1) for the
329
+ // known bind failures; this .catch is a backstop so any future throw
330
+ // path can never surface as an unhandled-rejection stack dump.
331
+ void host.run().catch((error) => {
332
+ process.stderr.write(`cw: ${error instanceof Error ? error.message : String(error)}\n`);
333
+ process.exitCode = 1;
334
+ });
329
335
  return { json: undefined };
330
336
  },
331
337
  });
@@ -76,8 +76,14 @@ function firstPositionalArg(args, index = 0) {
76
76
  return { json: registryCli.scheduleDaemonTickCli(args.options) };
77
77
  // Never returns (matches the old build's forever daemon loop);
78
78
  // the process stays alive via the DesktopSchedulerDaemon's own
79
- // setInterval, printing one tick line per interval.
80
- void registryCli.scheduleDaemonRunForever(args.options);
79
+ // setInterval, printing one tick line per interval. The daemon's
80
+ // run() already guards each tick and fails closed with a `cw:`
81
+ // line; this .catch is a backstop against any unguarded throw so
82
+ // it can never surface as an unhandled-rejection stack dump.
83
+ void registryCli.scheduleDaemonRunForever(args.options).catch((error) => {
84
+ process.stderr.write(`cw: ${error instanceof Error ? error.message : String(error)}\n`);
85
+ process.exitCode = 1;
86
+ });
81
87
  return {};
82
88
  }
83
89
  default:
@@ -18,10 +18,13 @@ const cli_args_1 = require("../../core/util/cli-args");
18
18
  // cli/parseargv.ts) even though dispatchTable now handles it as a real
19
19
  // row — a known, preserved wart.
20
20
  // ---------------------------------------------------------------------
21
- const ledger_cli_1 = require("../../shell/ledger-cli");
22
21
  // This file is required at startup for every command. Loading these shell
23
22
  // modules only when their handler runs, not at import time, keeps that
24
- // load cost out of commands that never touch telemetry/audit/demo/report.
23
+ // load cost out of commands that never touch ledger/telemetry/audit/demo/
24
+ // report.
25
+ function loadLedgerCli() {
26
+ return require("../../shell/ledger-cli");
27
+ }
25
28
  function loadTelemetryCli() {
26
29
  return require("../../shell/telemetry-cli");
27
30
  }
@@ -40,42 +43,81 @@ function loadReportCli() {
40
43
  (0, registry_core_1.attachCliBinding)("ledger.propose", {
41
44
  path: ["ledger", "propose"],
42
45
  jsonMode: "default",
43
- handler: (args) => ({ json: (0, ledger_cli_1.ledgerProposeCli)(args.options) }),
46
+ handler: (args) => ({ json: loadLedgerCli().ledgerProposeCli(args.options) }),
47
+ // UI/UX fix: `cw help ledger` used to list only the one-line summary for
48
+ // each row, so a first-run user had no way to learn these flags without
49
+ // reading source. Real flag names verified against ledgerProposeCli's
50
+ // required()/stringOption() calls above.
51
+ flags: [
52
+ { name: "--from AGENT/REPO", summary: "Who is making the change." },
53
+ { name: "--to AGENT/REPO", summary: "Who should get the change." },
54
+ { name: "--title TEXT", summary: "A short name for the change." },
55
+ { name: "--rationale TEXT", summary: "Why the change should happen." },
56
+ { name: "--files LIST", summary: "The files the change touches, as a comma list." },
57
+ { name: "--diff PATCH", summary: "The change as one diff file." },
58
+ ],
59
+ // ledgerProposeCli reads exactly from/to/title/rationale/files/diff
60
+ // (shell/ledger-cli.ts) — the list above is complete, so the dispatcher
61
+ // may warn about an unknown flag (TTY-only; see cli/global-flags.ts).
62
+ flagsComplete: true,
44
63
  });
45
- registry_core_1.REGISTRY_BY_CAPABILITY.get("ledger.propose").mcp.handler = (args) => (0, ledger_cli_1.ledgerProposeMcp)(args);
64
+ registry_core_1.REGISTRY_BY_CAPABILITY.get("ledger.propose").mcp.handler = (args) => loadLedgerCli().ledgerProposeMcp(args);
46
65
  (0, registry_core_1.attachCliBinding)("ledger.review", {
47
66
  path: ["ledger", "review"],
48
67
  jsonMode: "default",
49
- handler: (args) => ({ json: (0, ledger_cli_1.ledgerReviewCli)(args.options) }),
68
+ handler: (args) => ({ json: loadLedgerCli().ledgerReviewCli(args.options) }),
69
+ // Real flag names verified against ledgerReviewCli's required()/
70
+ // stringOption() calls above — NOT the same flag set as ledger propose
71
+ // (this row needs --target/--verdict/--findings, not --title/
72
+ // --rationale/--diff).
73
+ flags: [
74
+ { name: "--from AGENT/REPO", summary: "Who is doing the review." },
75
+ { name: "--to AGENT/REPO", summary: "Who made the change being reviewed." },
76
+ { name: "--target ID", summary: "The id of the proposal or PR to review." },
77
+ { name: "--verdict approved|rejected", summary: "The result of the review." },
78
+ { name: "--findings TEXT", summary: "Notes on what the review found." },
79
+ ],
80
+ // ledgerReviewCli reads exactly from/to/target/verdict/findings
81
+ // (shell/ledger-cli.ts) — the list above is complete, so the dispatcher
82
+ // may warn about an unknown flag (TTY-only; see cli/global-flags.ts).
83
+ flagsComplete: true,
50
84
  });
51
- registry_core_1.REGISTRY_BY_CAPABILITY.get("ledger.review").mcp.handler = (args) => (0, ledger_cli_1.ledgerReviewMcp)(args);
85
+ registry_core_1.REGISTRY_BY_CAPABILITY.get("ledger.review").mcp.handler = (args) => loadLedgerCli().ledgerReviewMcp(args);
52
86
  (0, registry_core_1.attachCliBinding)("ledger.verify", {
53
87
  path: ["ledger", "verify"],
54
88
  jsonMode: "default",
55
89
  handler: (args) => {
56
- const result = (0, ledger_cli_1.ledgerVerifyCli)(args.options);
90
+ const result = loadLedgerCli().ledgerVerifyCli(args.options);
57
91
  return { json: result, exitCode: result.ok ? undefined : 1 };
58
92
  },
59
93
  });
60
- registry_core_1.REGISTRY_BY_CAPABILITY.get("ledger.verify").mcp.handler = (args) => (0, ledger_cli_1.ledgerVerifyEntry)(args.entry);
94
+ registry_core_1.REGISTRY_BY_CAPABILITY.get("ledger.verify").mcp.handler = (args) => loadLedgerCli().ledgerVerifyEntry(args.entry);
61
95
  (0, registry_core_1.attachCliBinding)("ledger.apply", {
62
96
  path: ["ledger", "apply"],
63
97
  jsonMode: "default",
64
98
  handler: (args) => {
65
- const result = (0, ledger_cli_1.ledgerApplyCli)(args.options);
99
+ const result = loadLedgerCli().ledgerApplyCli(args.options);
66
100
  return { json: result, exitCode: result.ok ? undefined : 1 };
67
101
  },
68
102
  });
69
- registry_core_1.REGISTRY_BY_CAPABILITY.get("ledger.apply").mcp.handler = (args) => (0, ledger_cli_1.ledgerApplyEntry)(args.entry);
103
+ registry_core_1.REGISTRY_BY_CAPABILITY.get("ledger.apply").mcp.handler = (args) => loadLedgerCli().ledgerApplyEntry(args.entry);
70
104
  (0, registry_core_1.attachCliBinding)("ledger.list", {
71
105
  path: ["ledger", "list"],
72
106
  jsonMode: "default",
73
107
  handler: (args) => {
74
- const result = (0, ledger_cli_1.ledgerListCli)(args.options);
108
+ const result = loadLedgerCli().ledgerListCli(args.options);
75
109
  return { json: result, exitCode: result.allOk ? undefined : 1 };
76
110
  },
111
+ // UI/UX fix: `--dir` here named the ledger directory, while the global
112
+ // front door (cli/entry.ts) treats `--dir` as an alias of `--repo` for
113
+ // every command — one flag, two meanings. `--ledger-dir` is the
114
+ // unambiguous spelling; `--dir` stays as the legacy alias, byte-compat.
115
+ flags: [
116
+ { name: "--ledger-dir DIR", summary: "The ledger directory to read. Give it more than once to union-verify mirrors." },
117
+ { name: "--dir DIR", summary: "Old name for --ledger-dir (also the global --repo alias; kept working)." },
118
+ ],
77
119
  });
78
- registry_core_1.REGISTRY_BY_CAPABILITY.get("ledger.list").mcp.handler = (args) => (0, ledger_cli_1.ledgerListMcp)(args);
120
+ registry_core_1.REGISTRY_BY_CAPABILITY.get("ledger.list").mcp.handler = (args) => loadLedgerCli().ledgerListMcp(args);
79
121
  (0, registry_core_1.attachCliBinding)("telemetry.verify", {
80
122
  path: ["telemetry", "verify"],
81
123
  jsonMode: "flag",
@@ -125,6 +125,19 @@ exactly one fixed step (injected `now`); bare `--drive` runs to the end or to a
125
125
  parked/blocked stop. `run drive <run-id>` (no `--step`) is the read-only, fixed
126
126
  preview of the next step.
127
127
 
128
+ ## Concurrent rounds (`--concurrency N`)
129
+
130
+ `--drive --concurrency N` (or an auto-width parallel phase) runs a whole round's
131
+ tasks in ONE window instead of one at a time. CW dispatches the round's tasks,
132
+ collects every agent outcome concurrently, then settles + accepts them in
133
+ DETERMINISTIC batch (task-id) order — so the wall-clock drops but the recorded
134
+ state, reports, and exit codes are byte-identical to a serial run. Both agent
135
+ shapes get real concurrency: a **CLI-binary** agent (`CW_AGENT_COMMAND`) runs
136
+ all children at once through the batch delegate child, and an **HTTP-endpoint**
137
+ agent (`CW_AGENT_ENDPOINT`) POSTs all N delegations at once through the HTTP
138
+ batch delegate child. A cache-hit task still settles on the serial path inside
139
+ the same round. An unconfigured agent still refuses (it is never a fake pass).
140
+
128
141
  ## Fail closed — probe vs refusal vs park
129
142
 
130
143
  - **Probe.** `backend probe agent` reports `readiness: "ready"` iff a
@@ -423,3 +436,7 @@ The one-command `cw -q` headline now routes the question and defaults the repo t
423
436
  0.2.3
424
437
 
425
438
  0.2.4
439
+
440
+ 0.2.5
441
+
442
+ 0.2.6
@@ -74,6 +74,43 @@ show/validate/apply/summary/graph, sandbox show/validate/choose/resolve, state
74
74
  summary refresh/show, `plan`, `approve`, `reject`, `comment.add`, `handoff`, and
75
75
  `review.policy`.
76
76
 
77
+ ## MCP Tool Hints and Version Negotiation
78
+
79
+ A `tools/list` entry may carry the standard MCP `annotations` field with
80
+ two behavior hints:
81
+
82
+ - `readOnlyHint: true` — the tool only reads; it makes no change on disk.
83
+ - `destructiveHint: true` (with `readOnlyHint: false`) — the tool deletes
84
+ stored data (the gc / delete sweeps: `cw_gc_run`, `cw_orphans_gc`,
85
+ `cw_clones_gc`, `cw_schedule_delete`, `cw_routine_delete`).
86
+
87
+ The hints come from one side table (`MCP_TOOL_ANNOTATIONS` in
88
+ `src/core/capability-data.ts`), and a tool goes in only after its handler
89
+ was read and checked by hand — a wrong "read-only" mark on a writing tool
90
+ would be a safety bug, so a tool we are not certain about carries no
91
+ `annotations` field at all. Absent beats wrong. The field is additive: a
92
+ client that does not know it simply does not read it.
93
+
94
+ The MCP `initialize` reply also negotiates the protocol version now: a
95
+ client that asks for a supported version gets that version echoed back,
96
+ and any other request falls back to the newest supported entry (see
97
+ `SUPPORTED_PROTOCOL_VERSIONS` in `src/mcp/server.ts`). With today's
98
+ one-entry list the reply bytes are unchanged.
99
+
100
+ ## MCP Tool Authority
101
+
102
+ `CW_MCP_ENABLED_TOOLS` and `CW_MCP_DISABLED_TOOLS` are optional comma lists of
103
+ exact MCP tool names. Space at either end of a name is taken out, and a second
104
+ name has no effect. The enabled list is an allowlist. The disabled list is then
105
+ applied, so deny has the last word. With both values unset, the server keeps its
106
+ full `tools/list` output and present tool access.
107
+
108
+ An empty name or a name not in the live registry stops server start with a
109
+ stderr diagnostic and a non-zero exit. Under a good policy, `tools/list` keeps
110
+ the registry order and shows only allowed tools. A call to a filtered tool gives
111
+ the normal MCP `isError: true` result and does not start its handler. This is a
112
+ server control, not an OS sandbox or an approval system.
113
+
77
114
  ## The Parity Matrix
78
115
 
79
116
  The matrix below is made from the live registry — one row per capability,
@@ -440,11 +477,11 @@ The shared arg helpers (`required`, `optionalArg`, `wantsJson`) live in
440
477
  `src/core/util/cli-args.ts`, a pure module used by the CLI and MCP sides of
441
478
  every wiring slice; they were moved out of `cli/io.ts` because the purity
442
479
  gate's layer rule says `wiring/` may not take imports from `cli/`. The parity
443
- scanner works the same way: `cliDispatchTokens()` gets the live token list
444
- straight from the registry's CLI capabilities (`cli.caseTokens` when a row
445
- names more than one first token, else `cli.path`), then makes sure each
446
- declared path resolves through the live dispatcher there is no grep over
447
- built dispatch source.
480
+ gate does not use a token list made from the registry as proof of that same
481
+ registry. It runs real `cw help`, checks every declared CLI path through the
482
+ dispatcher lookup used by `dispatch(args)`, reads MCP `tools/list` from a live
483
+ server, and runs CLI-process to MCP payload probes. There is no grep over built
484
+ dispatch source.
448
485
 
449
486
  `test/cli-mcp-parity-smoke.js` proves the contract from end to end. It checks
450
487
  registry ⇄ CLI ⇄ MCP coverage (every declared capability is found on its declared
@@ -643,3 +680,7 @@ CLI golden-path fixes: `cw -q "…"` routes the question (was read as an app id
643
680
  0.2.3
644
681
 
645
682
  0.2.4
683
+
684
+ 0.2.5
685
+
686
+ 0.2.6
@@ -183,3 +183,7 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
183
183
  0.2.3
184
184
 
185
185
  0.2.4
186
+
187
+ 0.2.5
188
+
189
+ 0.2.6
@@ -66,6 +66,40 @@ Additive: `RunQueueEntry` gets the optional `attempts`/`leaseId`/`leaseExpiresAt
66
66
  `queue add|list|drain|show` verbs that are already there do not change. No new database, no
67
67
  daemon-owned state.
68
68
 
69
+ ## Scheduled Tasks In Practice
70
+
71
+ CW scheduled tasks give looping prompts, cron-like schedules, one-shot
72
+ reminders, expiration, jitter, and clear completion. Schedules live in
73
+ `.cw/schedules/tasks.json`.
74
+
75
+ ```bash
76
+ # a /loop-ready schedule (cw loop is the short form of schedule create --kind loop)
77
+ cw loop --intervalMinutes 30 --prompt "Check this workflow and continue if work is due."
78
+
79
+ # a cron schedule
80
+ cw schedule create --kind cron --cron "*/15 * * * *" --prompt "Run the due workflow scan."
81
+
82
+ # a one-shot reminder
83
+ cw schedule create --kind reminder --delayMinutes 60 --prompt "Remind me to inspect the report."
84
+
85
+ # list, inspect, and manage
86
+ cw schedule list
87
+ cw schedule due
88
+ cw schedule complete <schedule-id>
89
+ cw schedule pause <schedule-id> / resume <schedule-id> / run-now <schedule-id>
90
+ cw schedule history <schedule-id>
91
+ cw schedule delete <schedule-id>
92
+
93
+ # the local daemon: one sweep, or a loop
94
+ cw schedule daemon --once
95
+ cw schedule daemon --intervalSeconds 60
96
+ ```
97
+
98
+ Notes: time is measured to the minute; expiration defaults to 7 days;
99
+ `jitterSeconds` can put space between runs. CW does not start the daemon by
100
+ default — use `schedule daemon`, cron, or another overseer to call
101
+ `schedule due` and run due prompts.
102
+
69
103
  ## See Also
70
104
 
71
105
  run-registry-control-plane(7), cli-mcp-parity(7), release-and-migration(7)
@@ -167,3 +201,7 @@ _No behavioral change in v0.1.89 (CLI-surface golden-path + help-output fixes on
167
201
  0.2.3
168
202
 
169
203
  0.2.4
204
+
205
+ 0.2.5
206
+
207
+ 0.2.6