@deksden-com/dd-flow-cli 0.2.0 → 0.3.1

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 (47) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +76 -5
  3. package/dist/build-info.json +10 -6
  4. package/dist/cli/help.js +165 -20
  5. package/dist/cli/run-cli.js +427 -17
  6. package/dist/schemas/compatibility.schema.json +105 -0
  7. package/dist/schemas/engine-manifest.schema.json +61 -0
  8. package/dist/schemas/flow-guidance.schema.json +90 -0
  9. package/dist/schemas/flow-run-index.schema.json +30 -4
  10. package/dist/schemas/global-dashboard-data.schema.json +126 -0
  11. package/dist/schemas/mb-sdlc-review-report.schema.json +242 -0
  12. package/dist/schemas/mb-upgrade-migration-report.schema.json +93 -0
  13. package/dist/schemas/plan-stage-report.schema.json +83 -0
  14. package/dist/schemas/project-dashboard-data.schema.json +122 -0
  15. package/dist/schemas/project-flow-pack-manifest.schema.json +5 -1
  16. package/dist/schemas/project-summary.schema.json +73 -0
  17. package/dist/schemas/protocol-dashboard-data.schema.json +112 -0
  18. package/dist/schemas/status-report.schema.json +38 -2
  19. package/dist/schemas/version-report.schema.json +22 -0
  20. package/dist/services/build-info.js +26 -3
  21. package/dist/services/canon.js +93 -22
  22. package/dist/services/cleanup.js +45 -1
  23. package/dist/services/cli-operation-classifier.js +104 -0
  24. package/dist/services/compatibility-preflight.js +124 -0
  25. package/dist/services/config.js +31 -0
  26. package/dist/services/dashboard-targets.js +95 -0
  27. package/dist/services/dashboard.js +972 -10
  28. package/dist/services/engines.js +532 -0
  29. package/dist/services/flow-guidance.js +221 -0
  30. package/dist/services/hooks.js +1 -1
  31. package/dist/services/ids.js +106 -0
  32. package/dist/services/lanes.js +333 -1
  33. package/dist/services/merge-queue.js +106 -16
  34. package/dist/services/merge-worker.js +67 -4
  35. package/dist/services/migrations.js +231 -0
  36. package/dist/services/project-summary.js +122 -0
  37. package/dist/services/projects.js +44 -3
  38. package/dist/services/protocol-lifecycle.js +144 -0
  39. package/dist/services/protocols.js +660 -7
  40. package/dist/services/runs.js +98 -21
  41. package/dist/services/schema-validation.js +84 -4
  42. package/dist/services/sessions.js +21 -4
  43. package/dist/services/status.js +199 -1
  44. package/dist/services/version-status.js +59 -9
  45. package/dist/storage/database.js +31 -0
  46. package/dist/storage/paths.js +33 -0
  47. package/package.json +3 -2
@@ -3,24 +3,31 @@ import { helpForArgs } from "./help.js";
3
3
  import { AppError, isAppError } from "../shared/errors.js";
4
4
  import { writeJson } from "../shared/json.js";
5
5
  import { archiveProject, getProjectStatus, migrateProjectIds, registerProject, resolveProject } from "../services/projects.js";
6
- import { cancelProtocol, getProtocolStatus, readyForMerge, registerProtocol, requireProtocol, transitionProtocol } from "../services/protocols.js";
6
+ import { cancelProtocol, getProtocolBlockers, getProtocolStatus, getReadyProtocols, implementProtocol, readyForMerge, registerProtocol, requireProtocol, syncProtocolFromRun, transitionProtocol } from "../services/protocols.js";
7
7
  import { blockPlanItem, completePlanItem, getPlanStatus, setProtocolPlan, skipPlanItem, startPlanItem } from "../services/plans.js";
8
8
  import { cancelMergeQueueJob, claimNextMergeJob, completeMergeJob, failMergeJob, getMergeQueueStatus, noteMergeJob, waitNextMergeJob } from "../services/merge-queue.js";
9
9
  import { getMergeWorkerStatus, oneShotMergeClaim, startMergeWorker, stopProjectMergeWorker } from "../services/merge-worker.js";
10
10
  import { cleanupApply, cleanupScan } from "../services/cleanup.js";
11
11
  import { getCodexHomeStatus, getCodexHooksStatus, handleCodexHook, initCodexHome, installCodexHooks, planCodexHome, printCodexHomeEnv, printCodexHooks, removeCodexHome, removeCodexHooks } from "../services/hooks.js";
12
12
  import { bootstrapWorktree, closeWorktree, createWorktreeRecord, getWorktreeStatus, planWorktree } from "../services/worktrees.js";
13
- import { acquireLaneLock, checkLaneWorkspace, getLaneStatus, heartbeatLaneLock, releaseLaneLock, setLaneWorkspace, waitForLaneLock } from "../services/lanes.js";
13
+ import { acquireLaneLock, cancelLaneWaiter, checkLaneWorkspace, getLaneWaiters, getLaneStatus, heartbeatLaneLock, releaseLaneLock, setLaneWorkspace, waitAcquireLaneLock, waitForLaneLock } from "../services/lanes.js";
14
14
  import { getProjectConfigStatus, setProjectConfigValue } from "../services/config.js";
15
15
  import { getCanonStatus, registerCanonRoot, resolveCanonRoot } from "../services/canon.js";
16
16
  import { getRuntimeStatus } from "../services/status.js";
17
- import { autoRefreshDashboards, getCmuxStatus, openDashboard, refreshDashboard, refreshGlobalDashboard, renderDashboard, renderGlobalDashboard } from "../services/dashboard.js";
17
+ import { getCliVersionReport } from "../services/build-info.js";
18
+ import { autoRefreshDashboards, dashboardData, getCmuxStatus, openDashboard, openGlobalDashboard, openProtocolDashboard, refreshAllDashboards, refreshDashboard, refreshGlobalDashboard, renderDashboard, renderGlobalDashboard } from "../services/dashboard.js";
19
+ import { dashboardTargetSummary, preferredDashboardCommand, relatedDashboardCommands, resolveDashboardTarget } from "../services/dashboard-targets.js";
18
20
  import { getFlowSessionStatus, registerFlowSession, stopFlowSession, stopMergeWorker } from "../services/sessions.js";
19
21
  import { attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunStatus, listFlowRuns, startFlowRun } from "../services/runs.js";
22
+ import { previewNextEntityId } from "../services/ids.js";
20
23
  import { validateSchema } from "../services/schema-validation.js";
21
24
  import { preflightMemoryPermissions } from "../services/memory-permissions.js";
25
+ import { planMigration, verifyMigrationReport } from "../services/migrations.js";
26
+ import { publishProjectSummary } from "../services/project-summary.js";
22
27
  import { requireProjectByRoot } from "../services/projects.js";
23
28
  import { resolveProjectRoot } from "../storage/paths.js";
29
+ import { doctorEngines, engineInfo, installCurrentEngine, listEngines, resolveEngine, routeArgsThroughEngine } from "../services/engines.js";
30
+ import { preflightCliCompatibility } from "../services/compatibility-preflight.js";
24
31
  const defaultIo = {
25
32
  stdout: process.stdout,
26
33
  stderr: process.stderr,
@@ -34,6 +41,14 @@ export async function runCli(args, io = defaultIo, env = process.env) {
34
41
  if (args.includes("--progress-jsonl")) {
35
42
  throw new AppError("usage", "--progress-jsonl is not implemented yet; use --json for final structured progress", 2);
36
43
  }
44
+ if (output.args.length === 1 && output.args[0] === "--version") {
45
+ const payload = getCliVersionReport();
46
+ if (output.json)
47
+ writeJson(io.stdout, payload);
48
+ else
49
+ io.stdout.write(`dd-flow ${payload.cli.version}\n`);
50
+ return 0;
51
+ }
37
52
  const help = helpForArgs(output.args);
38
53
  if (help) {
39
54
  io.stdout.write(`${help}\n`);
@@ -41,7 +56,16 @@ export async function runCli(args, io = defaultIo, env = process.env) {
41
56
  }
42
57
  const context = createContext(env);
43
58
  emitHumanProgress(io, output, progress, "start");
44
- const result = await dispatch(output.args, context, io);
59
+ const routed = await routeArgsThroughEngine(context, output.args, io, io.stdin, env);
60
+ if (routed?.routed && !routed.inProcess) {
61
+ emitHumanProgress(io, output, progress, "done");
62
+ return routed.exitCode;
63
+ }
64
+ const dispatchContext = routed?.inProcess
65
+ ? createContext({ ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: "in-process", DD_FLOW_ROUTED_FROM: getCliVersionReport().cli.version })
66
+ : context;
67
+ preflightCliCompatibility(dispatchContext, output.args, env);
68
+ const result = await dispatch(output.args, dispatchContext, io);
45
69
  refreshDashboardsAfterMutation(context, output.args, result);
46
70
  emitHumanProgress(io, output, progress, "done");
47
71
  const payload = attachProgress(result, progress, output);
@@ -60,10 +84,34 @@ export async function runCli(args, io = defaultIo, env = process.env) {
60
84
  if (output.json)
61
85
  writeJson(io.stderr, errorPayload);
62
86
  else
63
- io.stderr.write(`dd-flow: ${payload.error.message}\n`);
87
+ io.stderr.write(renderHumanError(payload));
64
88
  return isAppError(error) ? error.exitCode : 1;
65
89
  }
66
90
  }
91
+ function renderHumanError(payload) {
92
+ const lines = [`dd-flow: ${payload.error.message}`];
93
+ const details = recordObject(payload.error.details);
94
+ const compatibility = recordObject(details?.compatibility);
95
+ const remediation = recordObject(details?.remediation);
96
+ if (compatibility) {
97
+ lines.push(`Compatibility: ${String(compatibility.verdict ?? "unknown")} (${String(compatibility.engine_resolution ?? "unknown")})`);
98
+ if (compatibility.blocked_operation)
99
+ lines.push(`Blocked operation: ${String(compatibility.blocked_operation)}`);
100
+ if (compatibility.install_hint)
101
+ lines.push(`Install compatible engine: ${String(compatibility.install_hint)}`);
102
+ }
103
+ if (remediation?.mb_upgrade_mode)
104
+ lines.push(`Upgrade mode: ${String(remediation.mb_upgrade_mode)}`);
105
+ if (details?.preferred_command)
106
+ lines.push(`Preferred command: ${String(details.preferred_command)}`);
107
+ const related = Array.isArray(details?.related_commands) ? details.related_commands.map(String) : [];
108
+ if (related.length > 0) {
109
+ lines.push("", "Related commands:");
110
+ for (const command of related)
111
+ lines.push(` ${command}`);
112
+ }
113
+ return `${lines.join("\n")}\n`;
114
+ }
67
115
  function parseOutputOptions(args) {
68
116
  const stripped = [];
69
117
  let json = false;
@@ -86,7 +134,7 @@ function progressForCommand(args) {
86
134
  { phase: "done", message: "Merge queue wait finished", at: "" }
87
135
  ];
88
136
  }
89
- if (family === "lane" && command === "lock" && subcommand === "wait") {
137
+ if (family === "lane" && command === "lock" && ["wait", "wait-acquire"].includes(subcommand ?? "")) {
90
138
  return [
91
139
  { phase: "start", message: "Starting lane lock wait", at: "" },
92
140
  { phase: "running", message: "Waiting for lane lock", at: "" },
@@ -152,6 +200,18 @@ function renderHumanResult(args, result) {
152
200
  if (family === "status") {
153
201
  return renderStatusHuman(record);
154
202
  }
203
+ if (family === "version") {
204
+ const cli = recordObject(record.cli);
205
+ const engine = recordObject(record.engine);
206
+ const engineSuffix = engine ? ` (engine: ${String(engine.mode ?? "unknown")})` : "";
207
+ return `dd-flow ${stringValue(cli?.version) ?? "unknown"}${engineSuffix}\n`;
208
+ }
209
+ if (family === "engine") {
210
+ return renderEngineHuman(args, record);
211
+ }
212
+ if (family === "dashboard") {
213
+ return renderDashboardHuman(args, record);
214
+ }
155
215
  const lines = [`dd-flow ${family ?? ""}${command ? ` ${command}` : ""}: ${ok}`.trim()];
156
216
  const project = record.project && typeof record.project === "object" ? record.project : null;
157
217
  const protocol = record.protocol && typeof record.protocol === "object" ? record.protocol : null;
@@ -176,6 +236,7 @@ function renderHumanResult(args, result) {
176
236
  if (run.run_index_path)
177
237
  lines.push(`run_index: ${String(run.run_index_path)}`);
178
238
  }
239
+ appendFlowGuidanceHuman(lines, record);
179
240
  return `${lines.join("\n")}\n`;
180
241
  }
181
242
  function renderStatusHuman(record) {
@@ -183,6 +244,8 @@ function renderStatusHuman(record) {
183
244
  const cli = recordObject(record.cli);
184
245
  const cliBuild = recordObject(cli?.build);
185
246
  const builtWithCanon = recordObject(cliBuild?.built_with_canon);
247
+ const cliCompatibility = recordObject(cli?.compatibility);
248
+ const registry = recordObject(cli?.registry);
186
249
  const canon = recordObject(record.canon);
187
250
  const resolvedCanon = recordObject(canon?.resolved);
188
251
  const project = recordObject(record.project);
@@ -190,16 +253,105 @@ function renderStatusHuman(record) {
190
253
  const flowPack = recordObject(project?.flow_pack);
191
254
  const drift = recordObject(project?.drift);
192
255
  lines.push(`CLI package: ${stringValue(cli?.package_name) ?? "unknown"} ${stringValue(cli?.version) ?? "unknown"}`);
256
+ if (cliCompatibility) {
257
+ lines.push(`CLI compatibility: ${stringValue(cliCompatibility.verdict) ?? "unknown"} (${stringValue(cliCompatibility.reason) ?? "no_reason"})`);
258
+ if (cliCompatibility.update_command)
259
+ lines.push(`CLI update: ${String(cliCompatibility.update_command)}`);
260
+ }
261
+ if (registry) {
262
+ lines.push(`CLI registry: ${stringValue(registry.latest) ?? "unknown"} (${stringValue(registry.status) ?? "unknown"})`);
263
+ }
193
264
  lines.push(`CLI tested with canon: ${stringValue(builtWithCanon?.version) ?? "unknown"} @ ${shortSha(stringValue(builtWithCanon?.commit))}`);
194
- lines.push(`Active local canon: ${stringValue(resolvedCanon?.root) ?? "unresolved"} ${stringValue(resolvedCanon?.version) ?? "unknown"} @ ${shortSha(stringValue(resolvedCanon?.commit))}`);
265
+ lines.push(`Active local canon: ${stringValue(resolvedCanon?.root) ?? "unresolved"} ${stringValue(resolvedCanon?.version) ?? "unknown"} @ ${shortSha(stringValue(resolvedCanon?.commit))} (${stringValue(resolvedCanon?.layout) ?? "unknown"})`);
266
+ if (resolvedCanon?.memorybank_root)
267
+ lines.push(`Canon Memory Bank: ${String(resolvedCanon.memorybank_root)}`);
268
+ if (resolvedCanon?.flow_root)
269
+ lines.push(`Canon flow root: ${String(resolvedCanon.flow_root)}`);
195
270
  lines.push(`Project: ${stringValue(project?.root) ?? "unknown"}${project?.registered === true ? " (registered)" : " (unregistered)"}`);
196
271
  lines.push(`Project Memory Bank: ${stringValue(memoryBank?.version) ?? stringValue(memoryBank?.status) ?? "unknown"}`);
197
272
  lines.push(`Project flow pack: ${stringValue(flowPack?.pack_version) ?? stringValue(flowPack?.status) ?? "unknown"} @ ${shortSha(stringValue(flowPack?.source_commit))}`);
198
273
  lines.push(`Drift: ${stringValue(drift?.overall) ?? "unknown"}`);
199
274
  if (drift?.next_action)
200
275
  lines.push(`Next action: ${String(drift.next_action)}`);
276
+ appendFlowGuidanceHuman(lines, record);
201
277
  return `${lines.join("\n")}\n`;
202
278
  }
279
+ function renderDashboardHuman(args, record) {
280
+ const [, command] = args;
281
+ const ok = record.ok === false ? "failed" : "ok";
282
+ const target = recordObject(record.target);
283
+ const targetLabel = stringValue(target?.kind) ?? "unknown";
284
+ const lines = [`dd-flow dashboard ${command ?? ""}: ${ok}`.trim()];
285
+ lines.push(`target: ${targetLabel}`);
286
+ if (target?.project_id)
287
+ lines.push(`project: ${String(target.project_id)}`);
288
+ if (target?.protocol_id)
289
+ lines.push(`protocol: ${String(target.protocol_id)}`);
290
+ const dashboard = recordObject(record.dashboard);
291
+ if (dashboard?.path)
292
+ lines.push(`dashboard: ${String(dashboard.path)}`);
293
+ if (record.compatibility_alias)
294
+ lines.push(`compatibility_alias: true`);
295
+ if (record.preferred_command)
296
+ lines.push(`Preferred command: ${String(record.preferred_command)}`);
297
+ const related = Array.isArray(record.related_commands) ? record.related_commands.map(String) : [];
298
+ if (related.length > 0) {
299
+ lines.push("", "Related dashboard commands:");
300
+ for (const item of related)
301
+ lines.push(` ${item}`);
302
+ }
303
+ return `${lines.join("\n")}\n`;
304
+ }
305
+ function renderEngineHuman(args, record) {
306
+ const [, command] = args;
307
+ const lines = [`dd-flow engine ${command ?? ""}: ${record.ok === false ? "failed" : "ok"}`.trim()];
308
+ if (record.engine_store)
309
+ lines.push(`engine_store: ${String(record.engine_store)}`);
310
+ const engine = recordObject(record.engine);
311
+ if (engine) {
312
+ lines.push(`engine: ${String(engine.package_name ?? "unknown")}@${String(engine.package_version ?? "unknown")}`);
313
+ if (engine.entrypoint)
314
+ lines.push(`entrypoint: ${String(engine.entrypoint)}`);
315
+ if (engine.healthy !== undefined)
316
+ lines.push(`healthy: ${String(engine.healthy)}`);
317
+ }
318
+ const engines = Array.isArray(record.engines) ? record.engines : [];
319
+ if (engines.length > 0)
320
+ lines.push(`engines: ${engines.length}`);
321
+ const selection = recordObject(record.selection);
322
+ if (selection) {
323
+ lines.push(`selection: ${String(selection.status ?? "unknown")}`);
324
+ if (selection.selected && typeof selection.selected === "object") {
325
+ const selected = selection.selected;
326
+ lines.push(`selected: ${String(selected.package_name ?? "unknown")}@${String(selected.package_version ?? "unknown")}`);
327
+ }
328
+ if (selection.install_hint)
329
+ lines.push(`install_hint: ${String(selection.install_hint)}`);
330
+ }
331
+ const checks = Array.isArray(record.checks) ? record.checks : [];
332
+ if (checks.length > 0)
333
+ lines.push(`checks: ${checks.length}`);
334
+ return `${lines.join("\n")}\n`;
335
+ }
336
+ function appendFlowGuidanceHuman(lines, record) {
337
+ const guidance = recordObject(record.flow_guidance);
338
+ if (!guidance)
339
+ return;
340
+ const currentStage = stringValue(guidance.current_stage);
341
+ const nextAction = stringValue(guidance.recommended_next_action);
342
+ const prompt = stringValue(guidance.recommended_prompt);
343
+ if (currentStage)
344
+ lines.push(`stage: ${currentStage}`);
345
+ if (nextAction)
346
+ lines.push(`next: ${nextAction}${prompt && prompt !== "none" ? ` (${prompt})` : ""}`);
347
+ const guards = Array.isArray(guidance.guards) ? guidance.guards : [];
348
+ const firstGuard = guards.find((item) => item && typeof item === "object" && !Array.isArray(item));
349
+ if (firstGuard?.id)
350
+ lines.push(`guard: ${String(firstGuard.id)} ${String(firstGuard.status ?? "unknown")}`);
351
+ const missing = Array.isArray(guidance.blocked_if_missing) ? guidance.blocked_if_missing : [];
352
+ if (missing.length > 0)
353
+ lines.push(`missing: ${missing.map(String).join(", ")}`);
354
+ }
203
355
  function recordObject(value) {
204
356
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
205
357
  }
@@ -212,9 +364,18 @@ function shortSha(value) {
212
364
  async function dispatch(args, context, io) {
213
365
  const [family, command, ...rest] = args;
214
366
  const parsed = parseArgs(rest);
367
+ if (family === "engine") {
368
+ return dispatchEngine(context, command, parsed);
369
+ }
215
370
  if (family === "status") {
216
371
  const rootParsed = parseArgs([command ?? "", ...rest]);
217
- return getRuntimeStatus(context, { projectRoot: optionalOption(rootParsed, "project-root") ?? optionalOption(rootParsed, "root") });
372
+ return getRuntimeStatus(context, {
373
+ projectRoot: optionalOption(rootParsed, "project-root") ?? optionalOption(rootParsed, "root"),
374
+ checkRegistry: hasOption(rootParsed, "check-registry")
375
+ });
376
+ }
377
+ if (family === "version") {
378
+ return getCliVersionReport();
218
379
  }
219
380
  if (family === "canon") {
220
381
  return dispatchCanon(context, command, parsed);
@@ -225,6 +386,12 @@ async function dispatch(args, context, io) {
225
386
  if (family === "project" && command === "status") {
226
387
  return getProjectStatus(context, { root: requiredOption(parsed, "root") });
227
388
  }
389
+ if (family === "project" && command === "summary") {
390
+ return publishProjectSummary(context, {
391
+ project: requireProjectByRoot(context, resolveProjectRoot(requiredOption(parsed, "project-root"))),
392
+ write: parseOptionalBoolean(optionalOption(parsed, "write"), "write") ?? true
393
+ });
394
+ }
228
395
  if (family === "project" && command === "resolve") {
229
396
  return resolveProject(context, { idOrAlias: requiredPosition(parsed, 0, "id-or-alias") });
230
397
  }
@@ -255,9 +422,44 @@ async function dispatch(args, context, io) {
255
422
  if (family === "protocol" && command === "status") {
256
423
  return getProtocolStatus(context, { protocolId: requiredPosition(parsed, 0, "protocol-id") });
257
424
  }
425
+ if (family === "protocol" && command === "ready") {
426
+ return getReadyProtocols(context, { projectRoot: requiredOption(parsed, "project-root") });
427
+ }
428
+ if (family === "protocol" && command === "blockers") {
429
+ return getProtocolBlockers(context, {
430
+ protocolId: requiredPosition(parsed, 0, "protocol-id"),
431
+ projectRoot: requiredOption(parsed, "project-root")
432
+ });
433
+ }
434
+ if (family === "protocol" && command === "implement") {
435
+ const reason = optionalOption(parsed, "reason");
436
+ return implementProtocol(context, {
437
+ protocolId: requiredPosition(parsed, 0, "protocol-id"),
438
+ projectRoot: requiredOption(parsed, "project-root"),
439
+ force: hasOption(parsed, "force"),
440
+ ...(reason ? { reason } : {})
441
+ });
442
+ }
258
443
  if (family === "protocol" && command === "ready-for-merge") {
259
444
  return readyForMerge(context, { protocolId: requiredPosition(parsed, 0, "protocol-id") });
260
445
  }
446
+ if (family === "protocol" && command === "transition") {
447
+ const reason = optionalOption(parsed, "reason");
448
+ return transitionProtocol(context, {
449
+ protocolId: requiredPosition(parsed, 0, "protocol-id"),
450
+ to: requiredOption(parsed, "to"),
451
+ jsonFile: requiredOptionAlias(parsed, ["payload-file", "json-file"]),
452
+ force: hasOption(parsed, "force"),
453
+ ...(reason ? { reason } : {})
454
+ });
455
+ }
456
+ if (family === "protocol" && command === "sync-from-run") {
457
+ return syncProtocolFromRun(context, {
458
+ protocolId: requiredPosition(parsed, 0, "protocol-id"),
459
+ runId: requiredOption(parsed, "run"),
460
+ target: optionalOption(parsed, "target") ?? "auto"
461
+ });
462
+ }
261
463
  if (family === "protocol" && command === "cancel") {
262
464
  return cancelProtocol(context, {
263
465
  protocolId: requiredPosition(parsed, 0, "protocol-id"),
@@ -275,7 +477,7 @@ async function dispatch(args, context, io) {
275
477
  return transitionProtocol(context, {
276
478
  protocolId: requiredPosition(transitionParsed, 0, "protocol-id"),
277
479
  to: requiredOption(transitionParsed, "to"),
278
- jsonFile: requiredOption(transitionParsed, "json-file"),
480
+ jsonFile: requiredOptionAlias(transitionParsed, ["payload-file", "json-file"]),
279
481
  force: hasOption(transitionParsed, "force"),
280
482
  ...(reason ? { reason } : {})
281
483
  });
@@ -313,6 +515,9 @@ async function dispatch(args, context, io) {
313
515
  if (family === "run") {
314
516
  return dispatchRun(context, command, parsed);
315
517
  }
518
+ if (family === "id") {
519
+ return dispatchId(context, command, parsed);
520
+ }
316
521
  if (family === "integration" && command === "cmux") {
317
522
  return dispatchCmux(context, parsed);
318
523
  }
@@ -325,6 +530,9 @@ async function dispatch(args, context, io) {
325
530
  if (family === "memory") {
326
531
  return dispatchMemory(command, parsed);
327
532
  }
533
+ if (family === "migration") {
534
+ return dispatchMigration(context, command, parsed);
535
+ }
328
536
  if (family === "codex" && command === "hooks") {
329
537
  return dispatchCodexHooks(context, parsed);
330
538
  }
@@ -339,6 +547,41 @@ async function dispatch(args, context, io) {
339
547
  }
340
548
  throw new AppError("usage", `Unknown command: ${args.join(" ") || "<empty>"}`, 2);
341
549
  }
550
+ function dispatchEngine(context, command, parsed) {
551
+ if (command === "install") {
552
+ return installCurrentEngine(context, { force: hasOption(parsed, "force") });
553
+ }
554
+ if (command === "list") {
555
+ return listEngines(context);
556
+ }
557
+ if (command === "info") {
558
+ return engineInfo(context, { packageName: optionalOption(parsed, "package"), version: optionalOption(parsed, "version") });
559
+ }
560
+ if (command === "resolve") {
561
+ return resolveEngine(context, { projectRoot: optionalOption(parsed, "project-root") ?? optionalOption(parsed, "root") });
562
+ }
563
+ if (command === "doctor") {
564
+ return doctorEngines(context, { projectRoot: optionalOption(parsed, "project-root") ?? optionalOption(parsed, "root") });
565
+ }
566
+ throw new AppError("usage", `Unknown engine command: ${command ?? "<empty>"}`, 2);
567
+ }
568
+ function dispatchMigration(context, command, parsed) {
569
+ if (command === "plan" || command === "report") {
570
+ return planMigration(context, {
571
+ projectRoot: requiredOption(parsed, "project-root"),
572
+ sourceVersion: optionalOption(parsed, "source-version"),
573
+ targetVersion: optionalOption(parsed, "target-version"),
574
+ runId: optionalOption(parsed, "run"),
575
+ backupPath: optionalOption(parsed, "backup-path"),
576
+ backupCreatedAt: optionalOption(parsed, "backup-created-at"),
577
+ allowActive: parseOptionalBoolean(optionalOption(parsed, "allow-active"), "allow-active") ?? false
578
+ });
579
+ }
580
+ if (command === "verify") {
581
+ return verifyMigrationReport(context, { file: requiredOption(parsed, "file") });
582
+ }
583
+ throw new AppError("usage", `Unknown migration command: ${command ?? "<empty>"}`, 2);
584
+ }
342
585
  function dispatchCanon(context, command, parsed) {
343
586
  if (command === "register") {
344
587
  return registerCanonRoot(context, { root: requiredOption(parsed, "root") });
@@ -579,6 +822,16 @@ function dispatchRun(context, command, parsed) {
579
822
  }
580
823
  throw new AppError("usage", `Unknown run command: ${command ?? "<empty>"}`, 2);
581
824
  }
825
+ function dispatchId(context, command, parsed) {
826
+ if (command === "next") {
827
+ return previewNextEntityId(context, {
828
+ projectRoot: requiredOption(parsed, "project-root"),
829
+ type: requiredOption(parsed, "type"),
830
+ slug: requiredOption(parsed, "slug")
831
+ });
832
+ }
833
+ throw new AppError("usage", `Unknown id command: ${command ?? "<empty>"}`, 2);
834
+ }
582
835
  function dispatchCmux(context, parsed) {
583
836
  const action = requiredPosition(parsed, 0, "integration cmux action");
584
837
  if (action === "status") {
@@ -587,30 +840,149 @@ function dispatchCmux(context, parsed) {
587
840
  throw new AppError("usage", `Unknown integration cmux action: ${action}`, 2);
588
841
  }
589
842
  function dispatchDashboard(context, command, parsed) {
843
+ const action = dashboardAction(command);
844
+ if (command === "data") {
845
+ const target = resolveDashboardTarget(context, {
846
+ action,
847
+ project: optionalOption(parsed, "project"),
848
+ projectRoot: optionalOption(parsed, "project-root"),
849
+ protocol: optionalOption(parsed, "protocol"),
850
+ global: hasOption(parsed, "global")
851
+ });
852
+ return withDashboardGuidance(action, target, dashboardDataForResolvedTarget(context, target));
853
+ }
590
854
  if (command === "render") {
591
- return renderDashboard(context, { projectRoot: requiredOption(parsed, "project-root"), output: optionalOption(parsed, "output") });
855
+ const target = resolveDashboardTarget(context, {
856
+ action,
857
+ project: optionalOption(parsed, "project"),
858
+ projectRoot: optionalOption(parsed, "project-root"),
859
+ protocol: optionalOption(parsed, "protocol")
860
+ });
861
+ return withDashboardGuidance(action, target, renderDashboardForResolvedTarget(context, target, parsed));
592
862
  }
593
863
  if (command === "render-global") {
594
- return renderGlobalDashboard(context, { output: optionalOption(parsed, "output") });
864
+ const target = resolveDashboardTarget(context, { action: "render", global: true });
865
+ return withDashboardGuidance("render", target, renderGlobalDashboard(context, { output: optionalOption(parsed, "output"), format: optionalOption(parsed, "format") }), {
866
+ compatibilityAlias: true,
867
+ preferredCommand: "dd-flow dashboard render"
868
+ });
595
869
  }
596
870
  if (command === "open") {
597
- return openDashboard(context, { projectRoot: requiredOption(parsed, "project-root"), viewer: optionalOption(parsed, "viewer") });
871
+ const target = resolveDashboardTarget(context, {
872
+ action,
873
+ project: optionalOption(parsed, "project"),
874
+ projectRoot: optionalOption(parsed, "project-root"),
875
+ protocol: optionalOption(parsed, "protocol")
876
+ });
877
+ return withDashboardGuidance(action, target, openDashboardForResolvedTarget(context, target, parsed));
598
878
  }
599
879
  if (command === "refresh") {
600
880
  const open = optionalOption(parsed, "open");
601
881
  if (typeof open !== "undefined" && !["auto", "true", "false"].includes(open)) {
602
882
  throw new AppError("validation", "--open must be auto, true, or false", 2);
603
883
  }
604
- return refreshDashboard(context, {
605
- projectRoot: requiredOption(parsed, "project-root"),
606
- open: open
884
+ const target = resolveDashboardTarget(context, {
885
+ action,
886
+ project: optionalOption(parsed, "project"),
887
+ projectRoot: optionalOption(parsed, "project-root"),
888
+ protocol: optionalOption(parsed, "protocol"),
889
+ all: hasOption(parsed, "all")
607
890
  });
891
+ return withDashboardGuidance(action, target, refreshDashboardForResolvedTarget(context, target, parsed, open));
608
892
  }
609
893
  if (command === "refresh-global") {
610
- return refreshGlobalDashboard(context, { output: optionalOption(parsed, "output") });
894
+ const target = resolveDashboardTarget(context, { action: "refresh", global: true });
895
+ return withDashboardGuidance("refresh", target, refreshGlobalDashboard(context, { output: optionalOption(parsed, "output"), format: optionalOption(parsed, "format") }), {
896
+ compatibilityAlias: true,
897
+ preferredCommand: "dd-flow dashboard refresh"
898
+ });
611
899
  }
612
900
  throw new AppError("usage", `Unknown dashboard command: ${command ?? "<empty>"}`, 2);
613
901
  }
902
+ function dashboardAction(command) {
903
+ if (command === "data" || command === "render" || command === "open" || command === "refresh") {
904
+ return command;
905
+ }
906
+ if (command === "render-global")
907
+ return "render";
908
+ if (command === "refresh-global")
909
+ return "refresh";
910
+ throw new AppError("usage", `Unknown dashboard command: ${command ?? "<empty>"}`, 2);
911
+ }
912
+ function dashboardDataForResolvedTarget(context, target) {
913
+ if (target.kind === "all_projects") {
914
+ throw new AppError("validation", "dashboard data does not support --all", 2, { related_commands: relatedDashboardCommands("data") });
915
+ }
916
+ if (target.kind === "global") {
917
+ return dashboardData(context, { global: true });
918
+ }
919
+ if (target.kind === "protocol") {
920
+ return dashboardData(context, { projectRoot: target.project.root, protocol: target.protocolId });
921
+ }
922
+ return dashboardData(context, { projectRoot: target.project.root });
923
+ }
924
+ function renderDashboardForResolvedTarget(context, target, parsed) {
925
+ if (target.kind === "all_projects") {
926
+ throw new AppError("validation", "dashboard render does not support --all", 2, { related_commands: relatedDashboardCommands("render") });
927
+ }
928
+ if (target.kind === "global") {
929
+ return renderGlobalDashboard(context, { output: optionalOption(parsed, "output"), format: optionalOption(parsed, "format") ?? "html" });
930
+ }
931
+ return renderDashboard(context, {
932
+ projectRoot: target.project.root,
933
+ output: optionalOption(parsed, "output"),
934
+ format: optionalOption(parsed, "format") ?? (hasOption(parsed, "project") ? "html" : undefined),
935
+ ...(target.kind === "protocol" ? { protocol: target.protocolId } : {})
936
+ });
937
+ }
938
+ function openDashboardForResolvedTarget(context, target, parsed) {
939
+ if (target.kind === "all_projects") {
940
+ throw new AppError("validation", "dashboard open --all is not supported", 2, {
941
+ preferred_command: "dd-flow dashboard refresh --all",
942
+ related_commands: relatedDashboardCommands("open", "all_projects")
943
+ });
944
+ }
945
+ if (target.kind === "global") {
946
+ return openGlobalDashboard(context, { viewer: optionalOption(parsed, "viewer"), format: optionalOption(parsed, "format") ?? "html" });
947
+ }
948
+ if (target.kind === "protocol") {
949
+ return openProtocolDashboard(context, {
950
+ projectRoot: target.project.root,
951
+ protocol: target.protocolId,
952
+ viewer: optionalOption(parsed, "viewer"),
953
+ format: optionalOption(parsed, "format") ?? "html"
954
+ });
955
+ }
956
+ return openDashboard(context, {
957
+ projectRoot: target.project.root,
958
+ viewer: optionalOption(parsed, "viewer"),
959
+ format: optionalOption(parsed, "format") ?? (hasOption(parsed, "project") ? "html" : undefined)
960
+ });
961
+ }
962
+ function refreshDashboardForResolvedTarget(context, target, parsed, open) {
963
+ if (target.kind === "all_projects") {
964
+ return refreshAllDashboards(context, { format: optionalOption(parsed, "format") ?? "html" });
965
+ }
966
+ if (target.kind === "global") {
967
+ return refreshGlobalDashboard(context, { output: optionalOption(parsed, "output"), format: optionalOption(parsed, "format") ?? "html" });
968
+ }
969
+ return refreshDashboard(context, {
970
+ projectRoot: target.project.root,
971
+ open,
972
+ format: optionalOption(parsed, "format") ?? (hasOption(parsed, "project") ? "html" : undefined),
973
+ ...(target.kind === "protocol" ? { protocol: target.protocolId } : {})
974
+ });
975
+ }
976
+ function withDashboardGuidance(action, target, result, options = {}) {
977
+ const record = result && typeof result === "object" && !Array.isArray(result) ? result : { ok: true, result };
978
+ return {
979
+ ...record,
980
+ target: dashboardTargetSummary(target),
981
+ ...(options.compatibilityAlias ? { compatibility_alias: true } : {}),
982
+ preferred_command: options.preferredCommand ?? preferredDashboardCommand(action, target),
983
+ related_commands: relatedDashboardCommands(action, target.kind)
984
+ };
985
+ }
614
986
  function dispatchSchema(command, parsed) {
615
987
  if (command === "validate") {
616
988
  const schemaDir = optionalOption(parsed, "schema-dir");
@@ -628,6 +1000,21 @@ async function dispatchLane(context, command, parsed) {
628
1000
  if (command === "status") {
629
1001
  return getLaneStatus(context, { projectRoot: requiredOption(parsed, "project-root"), lane: optionalOption(parsed, "lane") });
630
1002
  }
1003
+ if (command === "waiters") {
1004
+ return getLaneWaiters(context, { projectRoot: requiredOption(parsed, "project-root"), lane: optionalOption(parsed, "lane") });
1005
+ }
1006
+ if (command === "waiter") {
1007
+ const action = requiredPosition(parsed, 0, "lane waiter action");
1008
+ if (action === "cancel") {
1009
+ return cancelLaneWaiter(context, {
1010
+ projectRoot: requiredOption(parsed, "project-root"),
1011
+ lane: requiredOption(parsed, "lane"),
1012
+ workerId: requiredOption(parsed, "worker-id"),
1013
+ reason: requiredOption(parsed, "reason")
1014
+ });
1015
+ }
1016
+ throw new AppError("usage", `Unknown lane waiter action: ${action}`, 2);
1017
+ }
631
1018
  if (command === "workspace") {
632
1019
  const action = requiredPosition(parsed, 0, "lane workspace action");
633
1020
  if (action === "set") {
@@ -692,6 +1079,18 @@ async function dispatchLane(context, command, parsed) {
692
1079
  pollIntervalSeconds: optionalNumber(parsed, "poll-interval")
693
1080
  });
694
1081
  }
1082
+ if (action === "wait-acquire") {
1083
+ return waitAcquireLaneLock(context, {
1084
+ projectRoot: requiredOption(parsed, "project-root"),
1085
+ lane: requiredOption(parsed, "lane"),
1086
+ workerId: requiredOption(parsed, "worker-id"),
1087
+ workspacePath: workspacePathOption(parsed),
1088
+ timeoutSeconds: optionalNumber(parsed, "timeout"),
1089
+ pollIntervalSeconds: optionalNumber(parsed, "poll-interval"),
1090
+ ttlSeconds: optionalNumber(parsed, "ttl"),
1091
+ reason: requiredOption(parsed, "reason")
1092
+ });
1093
+ }
695
1094
  throw new AppError("usage", `Unknown lane lock action: ${action}`, 2);
696
1095
  }
697
1096
  throw new AppError("usage", `Unknown lane command: ${command ?? "<empty>"}`, 2);
@@ -882,6 +1281,14 @@ function requiredOption(parsed, key) {
882
1281
  }
883
1282
  return value;
884
1283
  }
1284
+ function requiredOptionAlias(parsed, keys) {
1285
+ for (const key of keys) {
1286
+ const value = optionalOption(parsed, key);
1287
+ if (value)
1288
+ return value;
1289
+ }
1290
+ throw new AppError("usage", `Missing required option: ${keys.map((key) => `--${key}`).join(" or ")}`, 2);
1291
+ }
885
1292
  function hasOption(parsed, key) {
886
1293
  return parsed.options.has(key);
887
1294
  }
@@ -1018,11 +1425,14 @@ function projectRootForLaneMutation(command, parsed) {
1018
1425
  if (command === "workspace" && requiredPosition(parsed, 0, "lane workspace action") === "set") {
1019
1426
  return requiredOption(parsed, "project-root");
1020
1427
  }
1428
+ if (command === "waiter" && requiredPosition(parsed, 0, "lane waiter action") === "cancel") {
1429
+ return requiredOption(parsed, "project-root");
1430
+ }
1021
1431
  if (command !== "lock") {
1022
1432
  return undefined;
1023
1433
  }
1024
1434
  const action = requiredPosition(parsed, 0, "lane lock action");
1025
- return ["acquire", "heartbeat", "release"].includes(action) ? requiredOption(parsed, "project-root") : undefined;
1435
+ return ["acquire", "heartbeat", "release", "wait-acquire"].includes(action) ? requiredOption(parsed, "project-root") : undefined;
1026
1436
  }
1027
1437
  function projectRootForMergeQueueMutation(context, command, parsed) {
1028
1438
  if (["next", "wait-next"].includes(command ?? "")) {