cool-workflow 0.1.84 → 0.1.86

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 (63) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +47 -331
  4. package/apps/architecture-review/app.json +1 -1
  5. package/apps/architecture-review-fast/app.json +1 -1
  6. package/apps/architecture-review-fast/workflow.js +2 -0
  7. package/apps/end-to-end-golden-path/app.json +1 -1
  8. package/apps/pr-review-fix-ci/app.json +1 -1
  9. package/apps/release-cut/app.json +1 -1
  10. package/apps/research-synthesis/app.json +1 -1
  11. package/dist/capability-core.js +220 -5
  12. package/dist/capability-registry.js +90 -51
  13. package/dist/cli/command-surface.js +47 -2
  14. package/dist/doctor.js +37 -1
  15. package/dist/mcp/tool-call.js +428 -0
  16. package/dist/mcp/tool-definitions.js +1027 -0
  17. package/dist/mcp-surface.js +5 -1416
  18. package/dist/onramp.js +421 -0
  19. package/dist/orchestrator.js +20 -15
  20. package/dist/run-export.js +139 -1
  21. package/dist/telemetry-demo.js +119 -0
  22. package/dist/types/report-bundle.js +6 -0
  23. package/dist/types.js +1 -0
  24. package/dist/version.js +1 -1
  25. package/dist/workbench-host.js +1 -8
  26. package/docs/agent-delegation-drive.7.md +23 -2
  27. package/docs/cli-mcp-parity.7.md +38 -7
  28. package/docs/contract-migration-tooling.7.md +4 -0
  29. package/docs/control-plane-scheduling.7.md +4 -0
  30. package/docs/dogfood-one-real-repo.7.md +9 -1
  31. package/docs/durable-state-and-locking.7.md +4 -0
  32. package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
  33. package/docs/execution-backends.7.md +4 -0
  34. package/docs/getting-started.md +40 -2
  35. package/docs/index.md +51 -37
  36. package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
  37. package/docs/multi-agent-eval-replay-harness.7.md +4 -0
  38. package/docs/multi-agent-operator-ux.7.md +4 -0
  39. package/docs/node-snapshot-diff-replay.7.md +4 -0
  40. package/docs/observability-cost-accounting.7.md +4 -0
  41. package/docs/project-index.md +23 -7
  42. package/docs/real-execution-backends.7.md +4 -0
  43. package/docs/release-and-migration.7.md +5 -1
  44. package/docs/release-history.md +342 -0
  45. package/docs/release-tooling.7.md +18 -0
  46. package/docs/report-verifiable-bundle.7.md +123 -0
  47. package/docs/run-registry-control-plane.7.md +4 -0
  48. package/docs/run-retention-reclamation.7.md +4 -0
  49. package/docs/state-explosion-management.7.md +4 -0
  50. package/docs/team-collaboration.7.md +4 -0
  51. package/docs/web-desktop-workbench.7.md +4 -0
  52. package/manifest/plugin.manifest.json +1 -1
  53. package/manifest/source-context-profiles.json +1 -1
  54. package/package.json +2 -1
  55. package/scripts/architecture-review-fast.js +44 -2
  56. package/scripts/canonical-apps.js +4 -4
  57. package/scripts/dogfood-release.js +55 -133
  58. package/scripts/golden-path.js +4 -4
  59. package/scripts/onramp-check.js +47 -0
  60. package/scripts/parity-check.js +740 -2
  61. package/scripts/release-check.js +2 -0
  62. package/scripts/source-context.js +31 -5
  63. package/scripts/version-sync-check.js +4 -2
@@ -30,6 +30,9 @@ const node = process.execPath;
30
30
  const cli = path.join(pluginRoot, "dist", "cli.js");
31
31
  const mcpServer = path.join(pluginRoot, "dist", "mcp-server.js");
32
32
  const registry = require(path.join(pluginRoot, "dist", "capability-registry.js"));
33
+ const { formatHelp } = require(path.join(pluginRoot, "dist", "orchestrator.js"));
34
+ const { loadRunFromCwd, saveCheckpoint } = require(path.join(pluginRoot, "dist", "state.js"));
35
+ const { appendRunNode, createStateNode } = require(path.join(pluginRoot, "dist", "state-node.js"));
33
36
 
34
37
  function capById(id) {
35
38
  const cap = registry.CAPABILITY_REGISTRY.find((entry) => entry.capability === id);
@@ -41,6 +44,10 @@ function jsonFlag(cap) {
41
44
  return cap.cli.jsonMode === "flag" ? ["--json"] : [];
42
45
  }
43
46
 
47
+ function runCli(argv, cwd) {
48
+ return JSON.parse(execFileSync(node, [cli, ...argv], { cwd, encoding: "utf8" }));
49
+ }
50
+
44
51
  // ---- 1. static surface parity ---------------------------------------------
45
52
  function liveMcpTools() {
46
53
  const result = execFileSync(node, [mcpServer], {
@@ -65,6 +72,22 @@ function cliDispatchSources() {
65
72
  return [cli, path.join(pluginRoot, "dist", "cli", "command-surface.js")].filter((file) => fs.existsSync(file));
66
73
  }
67
74
 
75
+ function cliHelpTokens() {
76
+ const lines = formatHelp().split(/\r?\n/);
77
+ const start = lines.indexOf("Commands:");
78
+ if (start < 0) return [];
79
+ const tokens = new Set();
80
+ for (const line of lines.slice(start + 1)) {
81
+ if (!line.trim()) break;
82
+ const first = line.trim().split(/\s+/)[0];
83
+ for (const token of first.split("|")) {
84
+ const clean = token.replace(/[<[].*$/, "");
85
+ if (clean) tokens.add(clean);
86
+ }
87
+ }
88
+ return [...tokens].sort();
89
+ }
90
+
68
91
  // ---- 2. payload identity ---------------------------------------------------
69
92
  // Generation-moment ISO timestamps are presentation metadata, not capability
70
93
  // data: the same field carries the wall-clock instant of the render. Neutralize
@@ -73,6 +96,711 @@ function canonical(value) {
73
96
  return JSON.stringify(value).replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/g, "<ts>");
74
97
  }
75
98
 
99
+ function escapeRegExp(value) {
100
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
101
+ }
102
+
103
+ function replaceAllLiteral(text, search, replacement) {
104
+ return text.replace(new RegExp(escapeRegExp(search), "g"), replacement);
105
+ }
106
+
107
+ function canonicalScenario(value, context) {
108
+ let text = canonical(value);
109
+ for (const root of context.workspaceRoots || []) {
110
+ text = replaceAllLiteral(text, root, "<workspace>");
111
+ }
112
+ for (const runId of context.runIds || []) {
113
+ text = replaceAllLiteral(text, runId, "<runId>");
114
+ }
115
+ return text
116
+ .replace(/"durationMs":\d+/g, '"durationMs":<ms>')
117
+ .replace(/dispatch-\d{8}T\d{6}Z-\d{4}/g, "dispatch-<ts>-<seq>")
118
+ .replace(/(architecture-review|end-to-end-golden-path)-\d{8}T\d{6}Z-[a-f0-9]+/g, "<runId>")
119
+ .replace(/(architecture-review|end-to-end-golden-path)-<timestamp>-[a-f0-9]+/g, "<runId>")
120
+ .replace(/snap-<runId>([^"]*)-[a-f0-9]{12}/g, "snap-<runId>$1-<snap>")
121
+ .replace(/replay-snap-<runId>([^"]*)-<snap>-[a-f0-9]{8}/g, "replay-snap-<runId>$1-<snap>-<replay>")
122
+ .replace(/sha256:[a-f0-9]{32,64}/g, "sha256:<hash>")
123
+ .replace(/[a-f0-9]{64}/g, "<hex64>");
124
+ }
125
+
126
+ function makeWorkspace(label) {
127
+ const workspace = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), `cw-parity-${label}-`)));
128
+ fs.writeFileSync(path.join(workspace, "README.md"), "# CLI/MCP parity fixture\n", "utf8");
129
+ return workspace;
130
+ }
131
+
132
+ function bootstrapRun(workspace) {
133
+ return runCli(["plan", "end-to-end-golden-path", "--question", "CLI/MCP scenario payload parity"], workspace);
134
+ }
135
+
136
+ function bootstrapTopologyRun(workspace) {
137
+ return runCli(["plan", "architecture-review", "--repo", workspace, "--question", "CLI/MCP topology scenario parity"], workspace);
138
+ }
139
+
140
+ function writeParityResult(resultPath) {
141
+ fs.writeFileSync(
142
+ resultPath,
143
+ [
144
+ "# Result",
145
+ "",
146
+ "Deterministic parity result.",
147
+ "",
148
+ "```cw:result",
149
+ '{ "summary": "parity result", "findings": [], "evidence": ["README.md:1"] }',
150
+ "```",
151
+ ""
152
+ ].join("\n"),
153
+ "utf8"
154
+ );
155
+ }
156
+
157
+ function readmePath(workspace) {
158
+ return path.join(workspace, "README.md");
159
+ }
160
+
161
+ function dispatchWorkerCli(workspace, runId) {
162
+ const dispatch = runCli(["dispatch", runId, "--limit", "1"], workspace);
163
+ const task = dispatch.tasks[0];
164
+ assert.ok(task?.workerId, `${runId}: dispatch produced no worker id`);
165
+ assert.ok(task.workerResultPath, `${runId}: dispatch produced no worker result path`);
166
+ return {
167
+ workerId: task.workerId,
168
+ taskId: task.id,
169
+ resultPath: task.workerResultPath
170
+ };
171
+ }
172
+
173
+ async function dispatchWorkerMcp(mcp, workspace, runId) {
174
+ const dispatch = await mcp.tool("cw_dispatch", { cwd: workspace, runId, limit: 1 });
175
+ const task = dispatch.tasks[0];
176
+ assert.ok(task?.workerId, `${runId}: MCP dispatch produced no worker id`);
177
+ assert.ok(task.workerResultPath, `${runId}: MCP dispatch produced no worker result path`);
178
+ return {
179
+ workerId: task.workerId,
180
+ taskId: task.id,
181
+ resultPath: task.workerResultPath
182
+ };
183
+ }
184
+
185
+ function workerOutputCli(workspace, runId, worker) {
186
+ writeParityResult(worker.resultPath);
187
+ runCli(["worker", "output", runId, worker.workerId, worker.resultPath], workspace);
188
+ return worker;
189
+ }
190
+
191
+ async function workerOutputMcp(mcp, workspace, runId, worker) {
192
+ writeParityResult(worker.resultPath);
193
+ await mcp.tool("cw_worker_output", { cwd: workspace, runId, workerId: worker.workerId, resultPath: worker.resultPath });
194
+ return worker;
195
+ }
196
+
197
+ function verifiedWorkerCli(workspace, runId) {
198
+ return workerOutputCli(workspace, runId, dispatchWorkerCli(workspace, runId));
199
+ }
200
+
201
+ async function verifiedWorkerMcp(mcp, workspace, runId) {
202
+ return workerOutputMcp(mcp, workspace, runId, await dispatchWorkerMcp(mcp, workspace, runId));
203
+ }
204
+
205
+ function resultNodeId(runId) {
206
+ return `${runId}:result:golden:path`;
207
+ }
208
+
209
+ function verifiedNodeCli(workspace, runId) {
210
+ verifiedWorkerCli(workspace, runId);
211
+ return { nodeId: resultNodeId(runId) };
212
+ }
213
+
214
+ async function verifiedNodeMcp(mcp, workspace, runId) {
215
+ await verifiedWorkerMcp(mcp, workspace, runId);
216
+ return { nodeId: resultNodeId(runId) };
217
+ }
218
+
219
+ function nodeSnapshotsCli(workspace, runId) {
220
+ const node = verifiedNodeCli(workspace, runId);
221
+ const baseline = runCli(["node", "snapshot", runId, node.nodeId], workspace);
222
+ const candidate = runCli(["node", "snapshot", runId, node.nodeId], workspace);
223
+ return { ...node, baselineSnapshotId: baseline.snapshotId, candidateSnapshotId: candidate.snapshotId };
224
+ }
225
+
226
+ async function nodeSnapshotsMcp(mcp, workspace, runId) {
227
+ const node = await verifiedNodeMcp(mcp, workspace, runId);
228
+ const baseline = await mcp.tool("cw_node_snapshot", { cwd: workspace, runId, nodeId: node.nodeId });
229
+ const candidate = await mcp.tool("cw_node_snapshot", { cwd: workspace, runId, nodeId: node.nodeId });
230
+ return { ...node, baselineSnapshotId: baseline.snapshotId, candidateSnapshotId: candidate.snapshotId };
231
+ }
232
+
233
+ function nodeReplayCli(workspace, runId) {
234
+ const snapshots = nodeSnapshotsCli(workspace, runId);
235
+ const replay = runCli(["node", "replay", runId, snapshots.baselineSnapshotId], workspace);
236
+ return { ...snapshots, replayId: replay.replayId };
237
+ }
238
+
239
+ async function nodeReplayMcp(mcp, workspace, runId) {
240
+ const snapshots = await nodeSnapshotsMcp(mcp, workspace, runId);
241
+ const replay = await mcp.tool("cw_node_replay", { cwd: workspace, runId, snapshotId: snapshots.baselineSnapshotId });
242
+ return { ...snapshots, replayId: replay.replayId };
243
+ }
244
+
245
+ function registerCandidateCli(workspace, runId, worker, candidateId = "parity-candidate") {
246
+ runCli(["candidate", "register", runId, "--id", candidateId, "--worker", worker.workerId, "--kind", "worker-output"], workspace);
247
+ return { candidateId, worker };
248
+ }
249
+
250
+ async function registerCandidateMcp(mcp, workspace, runId, worker, candidateId = "parity-candidate") {
251
+ await mcp.tool("cw_candidate_register", { cwd: workspace, runId, id: candidateId, worker: worker.workerId, kind: "worker-output" });
252
+ return { candidateId, worker };
253
+ }
254
+
255
+ function scoreCandidateCli(workspace, runId, candidateId) {
256
+ return runCli([
257
+ "candidate",
258
+ "score",
259
+ runId,
260
+ candidateId,
261
+ "--criterion",
262
+ "correctness=4",
263
+ "--criterion",
264
+ "evidence=4",
265
+ "--criterion",
266
+ "fit=2",
267
+ "--max",
268
+ "10",
269
+ "--evidence",
270
+ "README.md:1",
271
+ "--notes",
272
+ "parity score",
273
+ "--scorer",
274
+ "parity"
275
+ ], workspace);
276
+ }
277
+
278
+ async function scoreCandidateMcp(mcp, workspace, runId, candidateId) {
279
+ return mcp.tool("cw_candidate_score", {
280
+ cwd: workspace,
281
+ runId,
282
+ candidateId,
283
+ criterion: ["correctness=4", "evidence=4", "fit=2"],
284
+ max: 10,
285
+ evidence: ["README.md:1"],
286
+ notes: "parity score",
287
+ scorer: "parity"
288
+ });
289
+ }
290
+
291
+ function failedWorkerFeedbackCli(workspace, runId) {
292
+ const worker = dispatchWorkerCli(workspace, runId);
293
+ const failed = runCli([
294
+ "worker",
295
+ "fail",
296
+ runId,
297
+ worker.workerId,
298
+ "parity failure",
299
+ "--code",
300
+ "runtime-error",
301
+ "--path",
302
+ readmePath(workspace)
303
+ ], workspace);
304
+ const feedbackId = failed.feedbackIds?.[0];
305
+ assert.ok(feedbackId, `${runId}: worker failure produced no feedback id`);
306
+ return { worker, feedbackId };
307
+ }
308
+
309
+ async function failedWorkerFeedbackMcp(mcp, workspace, runId) {
310
+ const worker = await dispatchWorkerMcp(mcp, workspace, runId);
311
+ const failed = await mcp.tool("cw_worker_fail", {
312
+ cwd: workspace,
313
+ runId,
314
+ workerId: worker.workerId,
315
+ message: "parity failure",
316
+ code: "runtime-error",
317
+ path: readmePath(workspace)
318
+ });
319
+ const feedbackId = failed.feedbackIds?.[0];
320
+ assert.ok(feedbackId, `${runId}: MCP worker failure produced no feedback id`);
321
+ return { worker, feedbackId };
322
+ }
323
+
324
+ function seedFailedNode(workspace, runId) {
325
+ const run = loadRunFromCwd(runId, workspace);
326
+ const nodeId = `${run.id}:parity:failed-node`;
327
+ if (!(run.nodes || []).some((node) => node.id === nodeId)) {
328
+ appendRunNode(run, createStateNode({
329
+ id: nodeId,
330
+ kind: "error",
331
+ status: "failed",
332
+ loopStage: "adjust",
333
+ errors: [{
334
+ code: "runtime-error",
335
+ message: "parity collect failure",
336
+ at: "2020-01-01T00:00:00.000Z",
337
+ path: readmePath(workspace),
338
+ retryable: false
339
+ }],
340
+ metadata: { pipelineStage: "parity" }
341
+ }));
342
+ saveCheckpoint(run);
343
+ }
344
+ return nodeId;
345
+ }
346
+
347
+ function seedVerifiedNode(workspace, runId) {
348
+ const run = loadRunFromCwd(runId, workspace);
349
+ const nodeId = `${run.id}:parity:verified-node`;
350
+ if (!(run.nodes || []).some((node) => node.id === nodeId)) {
351
+ appendRunNode(run, createStateNode({
352
+ id: nodeId,
353
+ kind: "verifier",
354
+ status: "verified",
355
+ loopStage: "adjust",
356
+ evidence: [{ id: "parity:verified", source: "test", locator: "README.md:1" }]
357
+ }));
358
+ saveCheckpoint(run);
359
+ }
360
+ return nodeId;
361
+ }
362
+
363
+ function sandboxProfileFile(workspace) {
364
+ const profileFile = path.join(workspace, "parity-sandbox-profile.json");
365
+ const profile = {
366
+ schemaVersion: 1,
367
+ id: "parity-readonly",
368
+ title: "Parity Readonly",
369
+ description: "Local sandbox profile fixture for CLI/MCP parity.",
370
+ readPaths: ["$cwd"],
371
+ writePaths: [],
372
+ workerOutput: { result: true, artifacts: true, logs: true },
373
+ execute: { mode: "none" },
374
+ network: { mode: "none" },
375
+ env: { inherit: false, expose: [] },
376
+ hostInstructions: ["Used only by the parity check."]
377
+ };
378
+ fs.writeFileSync(profileFile, `${JSON.stringify(profile, null, 2)}\n`, "utf8");
379
+ return profileFile;
380
+ }
381
+
382
+ function applyTopologyCli(workspace, runId) {
383
+ return runCli([
384
+ "topology",
385
+ "apply",
386
+ runId,
387
+ "map-reduce",
388
+ "--id",
389
+ "parity-map",
390
+ "--mapper-count",
391
+ "2",
392
+ "--task",
393
+ "map:server-api",
394
+ "--task",
395
+ "map:web-client"
396
+ ], workspace);
397
+ }
398
+
399
+ async function applyTopologyMcp(mcp, workspace, runId) {
400
+ return mcp.tool("cw_topology_apply", {
401
+ cwd: workspace,
402
+ runId,
403
+ topologyId: "map-reduce",
404
+ id: "parity-map",
405
+ mapperCount: 2,
406
+ task: ["map:server-api", "map:web-client"]
407
+ });
408
+ }
409
+
410
+ function refreshSummaryCli(workspace, runId) {
411
+ return runCli(["summary", "refresh", runId, "--json"], workspace);
412
+ }
413
+
414
+ async function refreshSummaryMcp(mcp, workspace, runId) {
415
+ return mcp.tool("cw_summary_refresh", { cwd: workspace, runId });
416
+ }
417
+
418
+ function scenarioWorkspacePair(capability) {
419
+ const slug = capability.replace(/[^a-z0-9]+/gi, "-");
420
+ return {
421
+ cliWorkspace: makeWorkspace(`${slug}-cli`),
422
+ mcpWorkspace: makeWorkspace(`${slug}-mcp`)
423
+ };
424
+ }
425
+
426
+ const RUNLESS_SCENARIOS = new Set([
427
+ "plan",
428
+ "app.show",
429
+ "app.validate",
430
+ "app.package",
431
+ "topology.show",
432
+ "topology.validate",
433
+ "sandbox.show",
434
+ "sandbox.validate",
435
+ "sandbox.choose",
436
+ "sandbox.resolve"
437
+ ]);
438
+
439
+ const TOPOLOGY_SCENARIOS = new Set(["topology.apply", "topology.summary", "topology.graph"]);
440
+
441
+ function prepareRegisteredCandidateCli(workspace, runId, candidateId = "parity-candidate") {
442
+ return registerCandidateCli(workspace, runId, verifiedWorkerCli(workspace, runId), candidateId);
443
+ }
444
+
445
+ async function prepareRegisteredCandidateMcp(mcp, workspace, runId, candidateId = "parity-candidate") {
446
+ return registerCandidateMcp(mcp, workspace, runId, await verifiedWorkerMcp(mcp, workspace, runId), candidateId);
447
+ }
448
+
449
+ function prepareScoredCandidateCli(workspace, runId, candidateId = "parity-candidate") {
450
+ const candidate = prepareRegisteredCandidateCli(workspace, runId, candidateId);
451
+ scoreCandidateCli(workspace, runId, candidate.candidateId);
452
+ return candidate;
453
+ }
454
+
455
+ async function prepareScoredCandidateMcp(mcp, workspace, runId, candidateId = "parity-candidate") {
456
+ const candidate = await prepareRegisteredCandidateMcp(mcp, workspace, runId, candidateId);
457
+ await scoreCandidateMcp(mcp, workspace, runId, candidate.candidateId);
458
+ return candidate;
459
+ }
460
+
461
+ function prepareScenarioCli(capability, workspace, runId) {
462
+ switch (capability) {
463
+ case "node.show":
464
+ case "node.snapshot":
465
+ return verifiedNodeCli(workspace, runId);
466
+ case "node.diff":
467
+ return nodeSnapshotsCli(workspace, runId);
468
+ case "node.replay":
469
+ return { snapshotId: nodeSnapshotsCli(workspace, runId).baselineSnapshotId };
470
+ case "node.replay.verify":
471
+ return nodeReplayCli(workspace, runId);
472
+ case "worker.list":
473
+ case "worker.show":
474
+ case "worker.manifest":
475
+ case "worker.validate":
476
+ case "worker.fail":
477
+ return { worker: dispatchWorkerCli(workspace, runId) };
478
+ case "worker.output": {
479
+ const worker = dispatchWorkerCli(workspace, runId);
480
+ writeParityResult(worker.resultPath);
481
+ return { worker };
482
+ }
483
+ case "candidate.register":
484
+ return { worker: verifiedWorkerCli(workspace, runId), candidateId: "parity-candidate" };
485
+ case "candidate.list":
486
+ case "candidate.show":
487
+ case "candidate.score":
488
+ case "candidate.reject":
489
+ return prepareRegisteredCandidateCli(workspace, runId);
490
+ case "candidate.rank":
491
+ case "candidate.select":
492
+ return prepareScoredCandidateCli(workspace, runId);
493
+ case "feedback.collect":
494
+ return { failedNodeId: seedFailedNode(workspace, runId) };
495
+ case "feedback.list":
496
+ case "feedback.show":
497
+ case "feedback.task":
498
+ return failedWorkerFeedbackCli(workspace, runId);
499
+ case "feedback.resolve": {
500
+ const feedback = failedWorkerFeedbackCli(workspace, runId);
501
+ return { ...feedback, verifierNodeId: seedVerifiedNode(workspace, runId) };
502
+ }
503
+ default:
504
+ return {};
505
+ }
506
+ }
507
+
508
+ async function prepareScenarioMcp(capability, mcp, workspace, runId) {
509
+ switch (capability) {
510
+ case "node.show":
511
+ case "node.snapshot":
512
+ return verifiedNodeMcp(mcp, workspace, runId);
513
+ case "node.diff":
514
+ return nodeSnapshotsMcp(mcp, workspace, runId);
515
+ case "node.replay": {
516
+ const snapshots = await nodeSnapshotsMcp(mcp, workspace, runId);
517
+ return { snapshotId: snapshots.baselineSnapshotId };
518
+ }
519
+ case "node.replay.verify":
520
+ return nodeReplayMcp(mcp, workspace, runId);
521
+ case "worker.list":
522
+ case "worker.show":
523
+ case "worker.manifest":
524
+ case "worker.validate":
525
+ case "worker.fail":
526
+ return { worker: await dispatchWorkerMcp(mcp, workspace, runId) };
527
+ case "worker.output": {
528
+ const worker = await dispatchWorkerMcp(mcp, workspace, runId);
529
+ writeParityResult(worker.resultPath);
530
+ return { worker };
531
+ }
532
+ case "candidate.register":
533
+ return { worker: await verifiedWorkerMcp(mcp, workspace, runId), candidateId: "parity-candidate" };
534
+ case "candidate.list":
535
+ case "candidate.show":
536
+ case "candidate.score":
537
+ case "candidate.reject":
538
+ return prepareRegisteredCandidateMcp(mcp, workspace, runId);
539
+ case "candidate.rank":
540
+ case "candidate.select":
541
+ return prepareScoredCandidateMcp(mcp, workspace, runId);
542
+ case "feedback.collect":
543
+ return { failedNodeId: seedFailedNode(workspace, runId) };
544
+ case "feedback.list":
545
+ case "feedback.show":
546
+ case "feedback.task":
547
+ return failedWorkerFeedbackMcp(mcp, workspace, runId);
548
+ case "feedback.resolve": {
549
+ const feedback = await failedWorkerFeedbackMcp(mcp, workspace, runId);
550
+ return { ...feedback, verifierNodeId: seedVerifiedNode(workspace, runId) };
551
+ }
552
+ default:
553
+ return {};
554
+ }
555
+ }
556
+
557
+ function runScenarioCli(capability, workspace, runId, context = {}) {
558
+ switch (capability) {
559
+ case "plan":
560
+ return runCli(["plan", "end-to-end-golden-path", "--question", "CLI/MCP scenario payload parity"], workspace);
561
+ case "app.show":
562
+ return runCli(["app", "show", "end-to-end-golden-path"], workspace);
563
+ case "app.validate":
564
+ return runCli(["app", "validate", "end-to-end-golden-path"], workspace);
565
+ case "app.package":
566
+ return runCli(["app", "package", "end-to-end-golden-path"], workspace);
567
+ case "topology.show":
568
+ return runCli(["topology", "show", "map-reduce"], workspace);
569
+ case "topology.validate":
570
+ return runCli(["topology", "validate", "map-reduce"], workspace);
571
+ case "topology.apply":
572
+ return applyTopologyCli(workspace, runId);
573
+ case "topology.summary":
574
+ return runCli(["topology", "summary", runId, "--json"], workspace);
575
+ case "topology.graph":
576
+ return runCli(["topology", "graph", runId, "--json"], workspace);
577
+ case "summary.refresh":
578
+ return refreshSummaryCli(workspace, runId);
579
+ case "summary.show":
580
+ return runCli(["summary", "show", runId, "--json"], workspace);
581
+ case "sandbox.show":
582
+ return runCli(["sandbox", "show", "readonly"], workspace);
583
+ case "sandbox.validate":
584
+ return runCli(["sandbox", "validate", sandboxProfileFile(workspace)], workspace);
585
+ case "sandbox.choose":
586
+ return runCli(["sandbox", "choose", "readonly"], workspace);
587
+ case "sandbox.resolve":
588
+ return runCli(["sandbox", "resolve", "readonly"], workspace);
589
+ case "approve":
590
+ return runCli(["approve", "run", runId, runId, "--actor", "parity-operator", "--role", "reviewer", "--rationale", "scenario approval"], workspace);
591
+ case "reject":
592
+ return runCli(["reject", "run", runId, runId, "--actor", "parity-operator", "--role", "reviewer", "--reason", "scenario rejection"], workspace);
593
+ case "comment.add":
594
+ return runCli(["comment", "add", "run", runId, runId, "--body", "scenario comment", "--actor", "parity-operator", "--role", "reviewer"], workspace);
595
+ case "handoff":
596
+ return runCli(["handoff", "run", runId, runId, "--from", "parity-operator", "--to", "parity-peer", "--reason", "scenario handoff"], workspace);
597
+ case "review.policy":
598
+ return runCli([
599
+ "review",
600
+ "policy",
601
+ runId,
602
+ "--required-approvals",
603
+ "2",
604
+ "--authorized-roles",
605
+ "reviewer,lead",
606
+ "--applies-to",
607
+ "commit,selection",
608
+ "--allow-self-approval"
609
+ ], workspace);
610
+ case "node.show":
611
+ return runCli(["node", "show", runId, context.nodeId], workspace);
612
+ case "node.snapshot":
613
+ return runCli(["node", "snapshot", runId, context.nodeId], workspace);
614
+ case "node.diff":
615
+ return runCli(["node", "diff", runId, context.baselineSnapshotId, context.candidateSnapshotId], workspace);
616
+ case "node.replay":
617
+ return runCli(["node", "replay", runId, context.snapshotId], workspace);
618
+ case "node.replay.verify":
619
+ return runCli(["node", "verify", runId, context.replayId], workspace);
620
+ case "worker.list":
621
+ return runCli(["worker", "list", runId], workspace);
622
+ case "worker.show":
623
+ return runCli(["worker", "show", runId, context.worker.workerId], workspace);
624
+ case "worker.manifest":
625
+ return runCli(["worker", "manifest", runId, context.worker.workerId], workspace);
626
+ case "worker.output":
627
+ return runCli(["worker", "output", runId, context.worker.workerId, context.worker.resultPath], workspace);
628
+ case "worker.fail":
629
+ return runCli(["worker", "fail", runId, context.worker.workerId, "parity failure", "--code", "runtime-error", "--path", readmePath(workspace)], workspace);
630
+ case "worker.validate":
631
+ return runCli(["worker", "validate", runId, context.worker.workerId, context.worker.resultPath], workspace);
632
+ case "candidate.list":
633
+ return runCli(["candidate", "list", runId], workspace);
634
+ case "candidate.show":
635
+ return runCli(["candidate", "show", runId, context.candidateId], workspace);
636
+ case "candidate.register":
637
+ return runCli(["candidate", "register", runId, "--id", context.candidateId, "--worker", context.worker.workerId, "--kind", "worker-output"], workspace);
638
+ case "candidate.score":
639
+ return scoreCandidateCli(workspace, runId, context.candidateId);
640
+ case "candidate.rank":
641
+ return runCli(["candidate", "rank", runId], workspace);
642
+ case "candidate.select":
643
+ return runCli(["candidate", "select", runId, context.candidateId, "--reason", "parity selected", "--selectedBy", "parity"], workspace);
644
+ case "candidate.reject":
645
+ return runCli(["candidate", "reject", runId, context.candidateId, "--reason", "parity rejected"], workspace);
646
+ case "feedback.list":
647
+ return runCli(["feedback", "list", runId], workspace);
648
+ case "feedback.show":
649
+ return runCli(["feedback", "show", runId, context.feedbackId], workspace);
650
+ case "feedback.collect":
651
+ return runCli(["feedback", "collect", runId], workspace);
652
+ case "feedback.task":
653
+ return runCli(["feedback", "task", runId, context.feedbackId, "--verify", "npm test"], workspace);
654
+ case "feedback.resolve":
655
+ return runCli(["feedback", "resolve", runId, context.feedbackId, "--status", "resolved", "--node", context.verifierNodeId, "--message", "parity resolved"], workspace);
656
+ default:
657
+ throw new Error(`No CLI payload scenario for ${capability}`);
658
+ }
659
+ }
660
+
661
+ async function runScenarioMcp(capability, mcp, workspace, runId, context = {}) {
662
+ switch (capability) {
663
+ case "plan":
664
+ return mcp.tool("cw_plan", { cwd: workspace, workflowId: "end-to-end-golden-path", question: "CLI/MCP scenario payload parity" });
665
+ case "app.show":
666
+ return mcp.tool("cw_app_show", { cwd: workspace, appId: "end-to-end-golden-path" });
667
+ case "app.validate":
668
+ return mcp.tool("cw_app_validate", { cwd: workspace, appId: "end-to-end-golden-path" });
669
+ case "app.package":
670
+ return mcp.tool("cw_app_package", { cwd: workspace, appId: "end-to-end-golden-path" });
671
+ case "topology.show":
672
+ return mcp.tool("cw_topology_show", { cwd: workspace, topologyId: "map-reduce" });
673
+ case "topology.validate":
674
+ return mcp.tool("cw_topology_validate", { cwd: workspace, topologyId: "map-reduce" });
675
+ case "topology.apply":
676
+ return applyTopologyMcp(mcp, workspace, runId);
677
+ case "topology.summary":
678
+ return mcp.tool("cw_topology_summary", { cwd: workspace, runId });
679
+ case "topology.graph":
680
+ return mcp.tool("cw_topology_graph", { cwd: workspace, runId });
681
+ case "summary.refresh":
682
+ return refreshSummaryMcp(mcp, workspace, runId);
683
+ case "summary.show":
684
+ return mcp.tool("cw_summary_show", { cwd: workspace, runId });
685
+ case "sandbox.show":
686
+ return mcp.tool("cw_sandbox_show", { cwd: workspace, profileId: "readonly" });
687
+ case "sandbox.validate":
688
+ return mcp.tool("cw_sandbox_validate", { cwd: workspace, profileFile: sandboxProfileFile(workspace) });
689
+ case "sandbox.choose":
690
+ return mcp.tool("cw_sandbox_choose", { cwd: workspace, profileId: "readonly" });
691
+ case "sandbox.resolve":
692
+ return mcp.tool("cw_sandbox_resolve", { cwd: workspace, profileId: "readonly" });
693
+ case "approve":
694
+ return mcp.tool("cw_approve", { cwd: workspace, runId, targetKind: "run", targetId: runId, actor: "parity-operator", role: "reviewer", rationale: "scenario approval" });
695
+ case "reject":
696
+ return mcp.tool("cw_reject", { cwd: workspace, runId, targetKind: "run", targetId: runId, actor: "parity-operator", role: "reviewer", reason: "scenario rejection" });
697
+ case "comment.add":
698
+ return mcp.tool("cw_comment_add", { cwd: workspace, runId, targetKind: "run", targetId: runId, body: "scenario comment", actor: "parity-operator", role: "reviewer" });
699
+ case "handoff":
700
+ return mcp.tool("cw_handoff", { cwd: workspace, runId, targetKind: "run", targetId: runId, from: "parity-operator", to: "parity-peer", reason: "scenario handoff" });
701
+ case "review.policy":
702
+ return mcp.tool("cw_review_policy", {
703
+ cwd: workspace,
704
+ runId,
705
+ requiredApprovals: 2,
706
+ authorizedRoles: "reviewer,lead",
707
+ appliesTo: "commit,selection",
708
+ allowSelfApproval: true
709
+ });
710
+ case "node.show":
711
+ return mcp.tool("cw_node_show", { cwd: workspace, runId, nodeId: context.nodeId });
712
+ case "node.snapshot":
713
+ return mcp.tool("cw_node_snapshot", { cwd: workspace, runId, nodeId: context.nodeId });
714
+ case "node.diff":
715
+ return mcp.tool("cw_node_diff", { cwd: workspace, runId, baselineSnapshotId: context.baselineSnapshotId, candidateSnapshotId: context.candidateSnapshotId });
716
+ case "node.replay":
717
+ return mcp.tool("cw_node_replay", { cwd: workspace, runId, snapshotId: context.snapshotId });
718
+ case "node.replay.verify":
719
+ return mcp.tool("cw_node_replay_verify", { cwd: workspace, runId, replayId: context.replayId });
720
+ case "worker.list":
721
+ return mcp.tool("cw_worker_list", { cwd: workspace, runId });
722
+ case "worker.show":
723
+ return mcp.tool("cw_worker_show", { cwd: workspace, runId, workerId: context.worker.workerId });
724
+ case "worker.manifest":
725
+ return mcp.tool("cw_worker_manifest", { cwd: workspace, runId, workerId: context.worker.workerId });
726
+ case "worker.output":
727
+ return mcp.tool("cw_worker_output", { cwd: workspace, runId, workerId: context.worker.workerId, resultPath: context.worker.resultPath });
728
+ case "worker.fail":
729
+ return mcp.tool("cw_worker_fail", { cwd: workspace, runId, workerId: context.worker.workerId, message: "parity failure", code: "runtime-error", path: readmePath(workspace) });
730
+ case "worker.validate":
731
+ return mcp.tool("cw_worker_validate", { cwd: workspace, runId, workerId: context.worker.workerId, resultPath: context.worker.resultPath });
732
+ case "candidate.list":
733
+ return mcp.tool("cw_candidate_list", { cwd: workspace, runId });
734
+ case "candidate.show":
735
+ return mcp.tool("cw_candidate_show", { cwd: workspace, runId, candidateId: context.candidateId });
736
+ case "candidate.register":
737
+ return mcp.tool("cw_candidate_register", { cwd: workspace, runId, id: context.candidateId, worker: context.worker.workerId, kind: "worker-output" });
738
+ case "candidate.score":
739
+ return scoreCandidateMcp(mcp, workspace, runId, context.candidateId);
740
+ case "candidate.rank":
741
+ return mcp.tool("cw_candidate_rank", { cwd: workspace, runId });
742
+ case "candidate.select":
743
+ return mcp.tool("cw_candidate_select", { cwd: workspace, runId, candidateId: context.candidateId, reason: "parity selected", selectedBy: "parity" });
744
+ case "candidate.reject":
745
+ return mcp.tool("cw_candidate_reject", { cwd: workspace, runId, candidateId: context.candidateId, reason: "parity rejected" });
746
+ case "feedback.list":
747
+ return mcp.tool("cw_feedback_list", { cwd: workspace, runId });
748
+ case "feedback.show":
749
+ return mcp.tool("cw_feedback_show", { cwd: workspace, runId, feedbackId: context.feedbackId });
750
+ case "feedback.collect":
751
+ return mcp.tool("cw_feedback_collect", { cwd: workspace, runId });
752
+ case "feedback.task":
753
+ return mcp.tool("cw_feedback_task", { cwd: workspace, runId, feedbackId: context.feedbackId, verify: "npm test" });
754
+ case "feedback.resolve":
755
+ return mcp.tool("cw_feedback_resolve", { cwd: workspace, runId, feedbackId: context.feedbackId, status: "resolved", node: context.verifierNodeId, message: "parity resolved" });
756
+ default:
757
+ throw new Error(`No MCP payload scenario for ${capability}`);
758
+ }
759
+ }
760
+
761
+ async function executeScenario(target, mcp) {
762
+ const { cliWorkspace, mcpWorkspace } = scenarioWorkspacePair(target.capability);
763
+ if (RUNLESS_SCENARIOS.has(target.capability)) {
764
+ const cliOut = runScenarioCli(target.capability, cliWorkspace);
765
+ const mcpOut = await runScenarioMcp(target.capability, mcp, mcpWorkspace);
766
+ return {
767
+ cliPayload: canonicalScenario(cliOut, {
768
+ workspaceRoots: [cliWorkspace],
769
+ runIds: cliOut.runId ? [cliOut.runId] : []
770
+ }),
771
+ mcpPayload: canonicalScenario(mcpOut, {
772
+ workspaceRoots: [mcpWorkspace],
773
+ runIds: mcpOut.runId ? [mcpOut.runId] : []
774
+ })
775
+ };
776
+ }
777
+
778
+ const cliPlan = TOPOLOGY_SCENARIOS.has(target.capability) ? bootstrapTopologyRun(cliWorkspace) : bootstrapRun(cliWorkspace);
779
+ const mcpPlan = TOPOLOGY_SCENARIOS.has(target.capability) ? bootstrapTopologyRun(mcpWorkspace) : bootstrapRun(mcpWorkspace);
780
+ if (target.capability === "topology.summary" || target.capability === "topology.graph") {
781
+ applyTopologyCli(cliWorkspace, cliPlan.runId);
782
+ await applyTopologyMcp(mcp, mcpWorkspace, mcpPlan.runId);
783
+ }
784
+ if (target.capability === "summary.show") {
785
+ refreshSummaryCli(cliWorkspace, cliPlan.runId);
786
+ await refreshSummaryMcp(mcp, mcpWorkspace, mcpPlan.runId);
787
+ }
788
+ const cliContext = prepareScenarioCli(target.capability, cliWorkspace, cliPlan.runId);
789
+ const mcpContext = await prepareScenarioMcp(target.capability, mcp, mcpWorkspace, mcpPlan.runId);
790
+ const cliOut = runScenarioCli(target.capability, cliWorkspace, cliPlan.runId, cliContext);
791
+ const mcpOut = await runScenarioMcp(target.capability, mcp, mcpWorkspace, mcpPlan.runId, mcpContext);
792
+ return {
793
+ cliPayload: canonicalScenario(cliOut, {
794
+ workspaceRoots: [cliWorkspace],
795
+ runIds: [cliPlan.runId]
796
+ }),
797
+ mcpPayload: canonicalScenario(mcpOut, {
798
+ workspaceRoots: [mcpWorkspace],
799
+ runIds: [mcpPlan.runId]
800
+ })
801
+ };
802
+ }
803
+
76
804
  function openMcp() {
77
805
  const server = spawn(node, [mcpServer], { cwd: pluginRoot, stdio: ["pipe", "pipe", "pipe"] });
78
806
  const lines = readline.createInterface({ input: server.stdout });
@@ -123,15 +851,22 @@ async function payloadParity() {
123
851
  await mcp.rpc("initialize", {});
124
852
  for (const target of registry.payloadProbeTargets()) {
125
853
  const cap = capById(target.capability);
854
+ if (target.kind === "scenario") {
855
+ const result = await executeScenario(target, mcp);
856
+ checked.push(target.capability);
857
+ if (result.cliPayload !== result.mcpPayload) mismatches.push(target.capability);
858
+ continue;
859
+ }
126
860
  // jsonMode is the single source for the CLI's --json policy; this probe only
127
861
  // appends --json for "flag" verbs and JSON.parse-es the result. The human
128
862
  // rendering and "default"-verb no-flag JSON are pinned to cap.cli.jsonMode by
129
863
  // the companion test/cli-jsonmode-parity-smoke.js, so cli.ts can't silently
130
864
  // re-encode that policy by hand and drift from this registry data.
865
+ if (target.kind !== "global" && target.kind !== "run") throw new Error(`Unknown payload probe target kind: ${target.kind}`);
131
866
  const cliArgv = target.kind === "run"
132
867
  ? [...cap.cli.path, runId, ...jsonFlag(cap)]
133
868
  : [...cap.cli.path, ...jsonFlag(cap)];
134
- const cliOut = JSON.parse(execFileSync(node, [cli, ...cliArgv], { cwd: workspace, encoding: "utf8" }));
869
+ const cliOut = runCli(cliArgv, workspace);
135
870
  const mcpArgs = target.kind === "run" ? { cwd: workspace, runId } : { cwd: workspace };
136
871
  const mcpOut = await mcp.tool(cap.mcp.tool, mcpArgs);
137
872
  checked.push(target.capability);
@@ -147,7 +882,8 @@ async function main() {
147
882
  const check = process.argv.includes("--check");
148
883
  const tools = liveMcpTools();
149
884
  const tokens = cliDispatchTokens();
150
- const report = registry.buildParityReport({ mcpTools: tools, cliTokens: tokens });
885
+ const helpTokens = cliHelpTokens();
886
+ const report = registry.buildParityReport({ mcpTools: tools, cliTokens: tokens, helpTokens });
151
887
  const payload = await payloadParity();
152
888
 
153
889
  const ok = report.ok && payload.mismatches.length === 0;
@@ -170,6 +906,8 @@ async function main() {
170
906
  if (report.undeclaredMcpTools.length) lines.push(` - server exposes MCP tools not declared in the registry: ${report.undeclaredMcpTools.join(", ")}`);
171
907
  if (report.missingCliTokens.length) lines.push(` - registry declares CLI tokens absent from dist/cli.js: ${report.missingCliTokens.join(", ")}`);
172
908
  if (report.undeclaredCliTokens.length) lines.push(` - dist/cli.js dispatches tokens not declared in the registry: ${report.undeclaredCliTokens.join(", ")}`);
909
+ if (report.helpMissingCliTokens.length) lines.push(` - registry declares CLI commands absent from cw help: ${report.helpMissingCliTokens.join(", ")}`);
910
+ if (report.helpUndeclaredCliTokens.length) lines.push(` - cw help lists commands not declared in the registry: ${report.helpUndeclaredCliTokens.join(", ")}`);
173
911
  if (report.reasonlessExceptions.length) lines.push(` - surface-specific / payload-divergent capabilities missing a recorded reason: ${report.reasonlessExceptions.join(", ")}`);
174
912
  if (report.payloadProbeUnclassified.length) lines.push(` - payload-identical capabilities neither probed nor deferred: ${report.payloadProbeUnclassified.join(", ")}`);
175
913
  if (report.payloadProbeDuplicateClassifications.length) lines.push(` - payload probe duplicate classifications: ${report.payloadProbeDuplicateClassifications.join(", ")}`);