@deksden-com/dd-flow-cli 0.3.0 → 0.4.0

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 (38) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +74 -5
  3. package/dist/build-info.json +5 -5
  4. package/dist/cli/help.js +116 -18
  5. package/dist/cli/run-cli.js +361 -29
  6. package/dist/schemas/compatibility.schema.json +81 -2
  7. package/dist/schemas/engine-manifest.schema.json +61 -0
  8. package/dist/schemas/flow-guidance.schema.json +17 -0
  9. package/dist/schemas/global-dashboard-data.schema.json +60 -2
  10. package/dist/schemas/mb-upgrade-migration-report.schema.json +93 -0
  11. package/dist/schemas/project-dashboard-data.schema.json +26 -2
  12. package/dist/schemas/project-summary.schema.json +73 -0
  13. package/dist/schemas/protocol-dashboard-data.schema.json +25 -2
  14. package/dist/schemas/status-report.schema.json +4 -2
  15. package/dist/services/branch-context.js +254 -0
  16. package/dist/services/cleanup.js +31 -0
  17. package/dist/services/cli-operation-classifier.js +104 -0
  18. package/dist/services/compatibility-preflight.js +127 -0
  19. package/dist/services/config.js +6 -0
  20. package/dist/services/dashboard-targets.js +95 -0
  21. package/dist/services/dashboard.js +376 -59
  22. package/dist/services/engines.js +532 -0
  23. package/dist/services/flow-guidance.js +8 -1
  24. package/dist/services/hooks.js +1 -1
  25. package/dist/services/lanes.js +333 -1
  26. package/dist/services/merge-queue.js +310 -15
  27. package/dist/services/merge-worker.js +44 -3
  28. package/dist/services/migrations.js +231 -0
  29. package/dist/services/project-summary.js +122 -0
  30. package/dist/services/projects.js +41 -6
  31. package/dist/services/protocol-lifecycle.js +144 -0
  32. package/dist/services/protocols.js +34 -7
  33. package/dist/services/sessions.js +21 -4
  34. package/dist/services/status.js +10 -0
  35. package/dist/services/version-status.js +39 -15
  36. package/dist/storage/database.js +25 -0
  37. package/dist/storage/paths.js +12 -0
  38. package/package.json +3 -2
@@ -5,24 +5,30 @@ import { writeJson } from "../shared/json.js";
5
5
  import { archiveProject, getProjectStatus, migrateProjectIds, registerProject, resolveProject } from "../services/projects.js";
6
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
- import { cancelMergeQueueJob, claimNextMergeJob, completeMergeJob, failMergeJob, getMergeQueueStatus, noteMergeJob, waitNextMergeJob } from "../services/merge-queue.js";
8
+ import { cancelMergeQueueJob, claimNextMergeJob, claimMergeBundle, completeMergeBundle, completeMergeJob, failMergeBundle, failMergeJob, getMergeBundleStatus, 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
17
  import { getCliVersionReport } from "../services/build-info.js";
18
- import { autoRefreshDashboards, dashboardData, getCmuxStatus, openDashboard, refreshDashboard, refreshGlobalDashboard, renderDashboard, renderGlobalDashboard } from "../services/dashboard.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";
19
20
  import { getFlowSessionStatus, registerFlowSession, stopFlowSession, stopMergeWorker } from "../services/sessions.js";
21
+ import { getProtocolBranchContext } from "../services/branch-context.js";
20
22
  import { attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunStatus, listFlowRuns, startFlowRun } from "../services/runs.js";
21
23
  import { previewNextEntityId } from "../services/ids.js";
22
24
  import { validateSchema } from "../services/schema-validation.js";
23
25
  import { preflightMemoryPermissions } from "../services/memory-permissions.js";
26
+ import { planMigration, verifyMigrationReport } from "../services/migrations.js";
27
+ import { publishProjectSummary } from "../services/project-summary.js";
24
28
  import { requireProjectByRoot } from "../services/projects.js";
25
29
  import { resolveProjectRoot } from "../storage/paths.js";
30
+ import { doctorEngines, engineInfo, installCurrentEngine, listEngines, resolveEngine, routeArgsThroughEngine } from "../services/engines.js";
31
+ import { preflightCliCompatibility } from "../services/compatibility-preflight.js";
26
32
  const defaultIo = {
27
33
  stdout: process.stdout,
28
34
  stderr: process.stderr,
@@ -51,7 +57,16 @@ export async function runCli(args, io = defaultIo, env = process.env) {
51
57
  }
52
58
  const context = createContext(env);
53
59
  emitHumanProgress(io, output, progress, "start");
54
- const result = await dispatch(output.args, context, io);
60
+ const routed = await routeArgsThroughEngine(context, output.args, io, io.stdin, env);
61
+ if (routed?.routed && !routed.inProcess) {
62
+ emitHumanProgress(io, output, progress, "done");
63
+ return routed.exitCode;
64
+ }
65
+ const dispatchContext = routed?.inProcess
66
+ ? createContext({ ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: "in-process", DD_FLOW_ROUTED_FROM: getCliVersionReport().cli.version })
67
+ : context;
68
+ preflightCliCompatibility(dispatchContext, output.args, env);
69
+ const result = await dispatch(output.args, dispatchContext, io);
55
70
  refreshDashboardsAfterMutation(context, output.args, result);
56
71
  emitHumanProgress(io, output, progress, "done");
57
72
  const payload = attachProgress(result, progress, output);
@@ -70,10 +85,34 @@ export async function runCli(args, io = defaultIo, env = process.env) {
70
85
  if (output.json)
71
86
  writeJson(io.stderr, errorPayload);
72
87
  else
73
- io.stderr.write(`dd-flow: ${payload.error.message}\n`);
88
+ io.stderr.write(renderHumanError(payload));
74
89
  return isAppError(error) ? error.exitCode : 1;
75
90
  }
76
91
  }
92
+ function renderHumanError(payload) {
93
+ const lines = [`dd-flow: ${payload.error.message}`];
94
+ const details = recordObject(payload.error.details);
95
+ const compatibility = recordObject(details?.compatibility);
96
+ const remediation = recordObject(details?.remediation);
97
+ if (compatibility) {
98
+ lines.push(`Compatibility: ${String(compatibility.verdict ?? "unknown")} (${String(compatibility.engine_resolution ?? "unknown")})`);
99
+ if (compatibility.blocked_operation)
100
+ lines.push(`Blocked operation: ${String(compatibility.blocked_operation)}`);
101
+ if (compatibility.install_hint)
102
+ lines.push(`Install compatible engine: ${String(compatibility.install_hint)}`);
103
+ }
104
+ if (remediation?.mb_upgrade_mode)
105
+ lines.push(`Upgrade mode: ${String(remediation.mb_upgrade_mode)}`);
106
+ if (details?.preferred_command)
107
+ lines.push(`Preferred command: ${String(details.preferred_command)}`);
108
+ const related = Array.isArray(details?.related_commands) ? details.related_commands.map(String) : [];
109
+ if (related.length > 0) {
110
+ lines.push("", "Related commands:");
111
+ for (const command of related)
112
+ lines.push(` ${command}`);
113
+ }
114
+ return `${lines.join("\n")}\n`;
115
+ }
77
116
  function parseOutputOptions(args) {
78
117
  const stripped = [];
79
118
  let json = false;
@@ -96,7 +135,7 @@ function progressForCommand(args) {
96
135
  { phase: "done", message: "Merge queue wait finished", at: "" }
97
136
  ];
98
137
  }
99
- if (family === "lane" && command === "lock" && subcommand === "wait") {
138
+ if (family === "lane" && command === "lock" && ["wait", "wait-acquire"].includes(subcommand ?? "")) {
100
139
  return [
101
140
  { phase: "start", message: "Starting lane lock wait", at: "" },
102
141
  { phase: "running", message: "Waiting for lane lock", at: "" },
@@ -164,7 +203,15 @@ function renderHumanResult(args, result) {
164
203
  }
165
204
  if (family === "version") {
166
205
  const cli = recordObject(record.cli);
167
- return `dd-flow ${stringValue(cli?.version) ?? "unknown"}\n`;
206
+ const engine = recordObject(record.engine);
207
+ const engineSuffix = engine ? ` (engine: ${String(engine.mode ?? "unknown")})` : "";
208
+ return `dd-flow ${stringValue(cli?.version) ?? "unknown"}${engineSuffix}\n`;
209
+ }
210
+ if (family === "engine") {
211
+ return renderEngineHuman(args, record);
212
+ }
213
+ if (family === "dashboard") {
214
+ return renderDashboardHuman(args, record);
168
215
  }
169
216
  const lines = [`dd-flow ${family ?? ""}${command ? ` ${command}` : ""}: ${ok}`.trim()];
170
217
  const project = record.project && typeof record.project === "object" ? record.project : null;
@@ -177,6 +224,18 @@ function renderHumanResult(args, result) {
177
224
  const job = record.job;
178
225
  lines.push(`job: ${String(job.protocol_id ?? "none")} ${String(job.status ?? "")}`.trim());
179
226
  }
227
+ const branchContext = recordObject(record.branch_context);
228
+ const mergeBundle = recordObject(branchContext?.merge_bundle);
229
+ if (branchContext) {
230
+ lines.push(`branch: ${String(branchContext.feature_branch ?? branchContext.grouping_key ?? "unknown")}`);
231
+ const protocols = Array.isArray(branchContext.protocols) ? branchContext.protocols.length : 0;
232
+ lines.push(`branch_protocols: ${protocols}`);
233
+ if (mergeBundle) {
234
+ const eligible = Array.isArray(mergeBundle.eligible_protocols) ? mergeBundle.eligible_protocols.length : 0;
235
+ const notReady = Array.isArray(mergeBundle.not_ready_protocols) ? mergeBundle.not_ready_protocols.length : 0;
236
+ lines.push(`bundle: ${String(mergeBundle.reason ?? "unknown")} ready=${eligible} not_ready=${notReady}`);
237
+ }
238
+ }
180
239
  if (record.stopped)
181
240
  lines.push(`stopped: ${String(record.reason ?? "true")}`);
182
241
  if (record.timed_out)
@@ -230,6 +289,63 @@ function renderStatusHuman(record) {
230
289
  appendFlowGuidanceHuman(lines, record);
231
290
  return `${lines.join("\n")}\n`;
232
291
  }
292
+ function renderDashboardHuman(args, record) {
293
+ const [, command] = args;
294
+ const ok = record.ok === false ? "failed" : "ok";
295
+ const target = recordObject(record.target);
296
+ const targetLabel = stringValue(target?.kind) ?? "unknown";
297
+ const lines = [`dd-flow dashboard ${command ?? ""}: ${ok}`.trim()];
298
+ lines.push(`target: ${targetLabel}`);
299
+ if (target?.project_id)
300
+ lines.push(`project: ${String(target.project_id)}`);
301
+ if (target?.protocol_id)
302
+ lines.push(`protocol: ${String(target.protocol_id)}`);
303
+ const dashboard = recordObject(record.dashboard);
304
+ if (dashboard?.path)
305
+ lines.push(`dashboard: ${String(dashboard.path)}`);
306
+ if (record.compatibility_alias)
307
+ lines.push(`compatibility_alias: true`);
308
+ if (record.preferred_command)
309
+ lines.push(`Preferred command: ${String(record.preferred_command)}`);
310
+ const related = Array.isArray(record.related_commands) ? record.related_commands.map(String) : [];
311
+ if (related.length > 0) {
312
+ lines.push("", "Related dashboard commands:");
313
+ for (const item of related)
314
+ lines.push(` ${item}`);
315
+ }
316
+ return `${lines.join("\n")}\n`;
317
+ }
318
+ function renderEngineHuman(args, record) {
319
+ const [, command] = args;
320
+ const lines = [`dd-flow engine ${command ?? ""}: ${record.ok === false ? "failed" : "ok"}`.trim()];
321
+ if (record.engine_store)
322
+ lines.push(`engine_store: ${String(record.engine_store)}`);
323
+ const engine = recordObject(record.engine);
324
+ if (engine) {
325
+ lines.push(`engine: ${String(engine.package_name ?? "unknown")}@${String(engine.package_version ?? "unknown")}`);
326
+ if (engine.entrypoint)
327
+ lines.push(`entrypoint: ${String(engine.entrypoint)}`);
328
+ if (engine.healthy !== undefined)
329
+ lines.push(`healthy: ${String(engine.healthy)}`);
330
+ }
331
+ const engines = Array.isArray(record.engines) ? record.engines : [];
332
+ if (engines.length > 0)
333
+ lines.push(`engines: ${engines.length}`);
334
+ const selection = recordObject(record.selection);
335
+ if (selection) {
336
+ lines.push(`selection: ${String(selection.status ?? "unknown")}`);
337
+ if (selection.selected && typeof selection.selected === "object") {
338
+ const selected = selection.selected;
339
+ lines.push(`selected: ${String(selected.package_name ?? "unknown")}@${String(selected.package_version ?? "unknown")}`);
340
+ }
341
+ if (selection.install_hint)
342
+ lines.push(`install_hint: ${String(selection.install_hint)}`);
343
+ }
344
+ const checks = Array.isArray(record.checks) ? record.checks : [];
345
+ if (checks.length > 0)
346
+ lines.push(`checks: ${checks.length}`);
347
+ return `${lines.join("\n")}\n`;
348
+ }
233
349
  function appendFlowGuidanceHuman(lines, record) {
234
350
  const guidance = recordObject(record.flow_guidance);
235
351
  if (!guidance)
@@ -261,6 +377,9 @@ function shortSha(value) {
261
377
  async function dispatch(args, context, io) {
262
378
  const [family, command, ...rest] = args;
263
379
  const parsed = parseArgs(rest);
380
+ if (family === "engine") {
381
+ return dispatchEngine(context, command, parsed);
382
+ }
264
383
  if (family === "status") {
265
384
  const rootParsed = parseArgs([command ?? "", ...rest]);
266
385
  return getRuntimeStatus(context, {
@@ -280,6 +399,12 @@ async function dispatch(args, context, io) {
280
399
  if (family === "project" && command === "status") {
281
400
  return getProjectStatus(context, { root: requiredOption(parsed, "root") });
282
401
  }
402
+ if (family === "project" && command === "summary") {
403
+ return publishProjectSummary(context, {
404
+ project: requireProjectByRoot(context, resolveProjectRoot(requiredOption(parsed, "project-root"))),
405
+ write: parseOptionalBoolean(optionalOption(parsed, "write"), "write") ?? true
406
+ });
407
+ }
283
408
  if (family === "project" && command === "resolve") {
284
409
  return resolveProject(context, { idOrAlias: requiredPosition(parsed, 0, "id-or-alias") });
285
410
  }
@@ -310,6 +435,14 @@ async function dispatch(args, context, io) {
310
435
  if (family === "protocol" && command === "status") {
311
436
  return getProtocolStatus(context, { protocolId: requiredPosition(parsed, 0, "protocol-id") });
312
437
  }
438
+ if (family === "protocol" && command === "branch-status") {
439
+ const protocolId = parsed.positional[0];
440
+ return getProtocolBranchContext(context, {
441
+ ...(protocolId ? { protocolId } : {}),
442
+ projectRoot: optionalOption(parsed, "project-root"),
443
+ workspacePath: optionalOption(parsed, "path")
444
+ });
445
+ }
313
446
  if (family === "protocol" && command === "ready") {
314
447
  return getReadyProtocols(context, { projectRoot: requiredOption(parsed, "project-root") });
315
448
  }
@@ -418,6 +551,9 @@ async function dispatch(args, context, io) {
418
551
  if (family === "memory") {
419
552
  return dispatchMemory(command, parsed);
420
553
  }
554
+ if (family === "migration") {
555
+ return dispatchMigration(context, command, parsed);
556
+ }
421
557
  if (family === "codex" && command === "hooks") {
422
558
  return dispatchCodexHooks(context, parsed);
423
559
  }
@@ -432,6 +568,41 @@ async function dispatch(args, context, io) {
432
568
  }
433
569
  throw new AppError("usage", `Unknown command: ${args.join(" ") || "<empty>"}`, 2);
434
570
  }
571
+ function dispatchEngine(context, command, parsed) {
572
+ if (command === "install") {
573
+ return installCurrentEngine(context, { force: hasOption(parsed, "force") });
574
+ }
575
+ if (command === "list") {
576
+ return listEngines(context);
577
+ }
578
+ if (command === "info") {
579
+ return engineInfo(context, { packageName: optionalOption(parsed, "package"), version: optionalOption(parsed, "version") });
580
+ }
581
+ if (command === "resolve") {
582
+ return resolveEngine(context, { projectRoot: optionalOption(parsed, "project-root") ?? optionalOption(parsed, "root") });
583
+ }
584
+ if (command === "doctor") {
585
+ return doctorEngines(context, { projectRoot: optionalOption(parsed, "project-root") ?? optionalOption(parsed, "root") });
586
+ }
587
+ throw new AppError("usage", `Unknown engine command: ${command ?? "<empty>"}`, 2);
588
+ }
589
+ function dispatchMigration(context, command, parsed) {
590
+ if (command === "plan" || command === "report") {
591
+ return planMigration(context, {
592
+ projectRoot: requiredOption(parsed, "project-root"),
593
+ sourceVersion: optionalOption(parsed, "source-version"),
594
+ targetVersion: optionalOption(parsed, "target-version"),
595
+ runId: optionalOption(parsed, "run"),
596
+ backupPath: optionalOption(parsed, "backup-path"),
597
+ backupCreatedAt: optionalOption(parsed, "backup-created-at"),
598
+ allowActive: parseOptionalBoolean(optionalOption(parsed, "allow-active"), "allow-active") ?? false
599
+ });
600
+ }
601
+ if (command === "verify") {
602
+ return verifyMigrationReport(context, { file: requiredOption(parsed, "file") });
603
+ }
604
+ throw new AppError("usage", `Unknown migration command: ${command ?? "<empty>"}`, 2);
605
+ }
435
606
  function dispatchCanon(context, command, parsed) {
436
607
  if (command === "register") {
437
608
  return registerCanonRoot(context, { root: requiredOption(parsed, "root") });
@@ -505,7 +676,38 @@ function dispatchMergeQueue(context, command, parsed) {
505
676
  }
506
677
  function dispatchMerge(context, command, parsed) {
507
678
  if (command === "status") {
508
- return getMergeWorkerStatus(context, { projectRoot: requiredOption(parsed, "project-root") });
679
+ return getMergeWorkerStatus(context, { projectRoot: requiredOption(parsed, "project-root"), workspacePath: optionalOption(parsed, "path") });
680
+ }
681
+ if (command === "bundle") {
682
+ const action = requiredPosition(parsed, 0, "bundle action");
683
+ if (action === "status") {
684
+ return getMergeBundleStatus(context, { projectRoot: requiredOption(parsed, "project-root"), workspacePath: workspacePathOption(parsed) });
685
+ }
686
+ if (action === "claim") {
687
+ return claimMergeBundle(context, {
688
+ projectRoot: requiredOption(parsed, "project-root"),
689
+ workerId: requiredWorkerId(parsed),
690
+ workspacePath: workspacePathOption(parsed)
691
+ });
692
+ }
693
+ if (action === "complete") {
694
+ return completeMergeBundle(context, {
695
+ projectRoot: requiredOption(parsed, "project-root"),
696
+ workerId: requiredWorkerId(parsed),
697
+ workspacePath: workspacePathOption(parsed),
698
+ summary: requiredOption(parsed, "summary")
699
+ });
700
+ }
701
+ if (action === "fail") {
702
+ return failMergeBundle(context, {
703
+ projectRoot: requiredOption(parsed, "project-root"),
704
+ workerId: requiredWorkerId(parsed),
705
+ workspacePath: workspacePathOption(parsed),
706
+ reason: requiredOption(parsed, "reason"),
707
+ requeue: parseBoolean(requiredOption(parsed, "requeue"), "requeue")
708
+ });
709
+ }
710
+ throw new AppError("usage", `Unknown merge bundle action: ${action}`, 2);
509
711
  }
510
712
  if (command === "one-shot") {
511
713
  return oneShotMergeClaim(context, {
@@ -690,49 +892,149 @@ function dispatchCmux(context, parsed) {
690
892
  throw new AppError("usage", `Unknown integration cmux action: ${action}`, 2);
691
893
  }
692
894
  function dispatchDashboard(context, command, parsed) {
895
+ const action = dashboardAction(command);
693
896
  if (command === "data") {
694
- const global = hasOption(parsed, "global");
695
- return dashboardData(context, {
696
- global,
697
- projectRoot: global ? undefined : requiredOption(parsed, "project-root"),
698
- protocol: optionalOption(parsed, "protocol")
897
+ const target = resolveDashboardTarget(context, {
898
+ action,
899
+ project: optionalOption(parsed, "project"),
900
+ projectRoot: optionalOption(parsed, "project-root"),
901
+ protocol: optionalOption(parsed, "protocol"),
902
+ global: hasOption(parsed, "global")
699
903
  });
904
+ return withDashboardGuidance(action, target, dashboardDataForResolvedTarget(context, target));
700
905
  }
701
906
  if (command === "render") {
702
- return renderDashboard(context, {
703
- projectRoot: requiredOption(parsed, "project-root"),
704
- output: optionalOption(parsed, "output"),
705
- format: optionalOption(parsed, "format"),
907
+ const target = resolveDashboardTarget(context, {
908
+ action,
909
+ project: optionalOption(parsed, "project"),
910
+ projectRoot: optionalOption(parsed, "project-root"),
706
911
  protocol: optionalOption(parsed, "protocol")
707
912
  });
913
+ return withDashboardGuidance(action, target, renderDashboardForResolvedTarget(context, target, parsed));
708
914
  }
709
915
  if (command === "render-global") {
710
- return renderGlobalDashboard(context, { output: optionalOption(parsed, "output"), format: optionalOption(parsed, "format") });
916
+ const target = resolveDashboardTarget(context, { action: "render", global: true });
917
+ return withDashboardGuidance("render", target, renderGlobalDashboard(context, { output: optionalOption(parsed, "output"), format: optionalOption(parsed, "format") }), {
918
+ compatibilityAlias: true,
919
+ preferredCommand: "dd-flow dashboard render"
920
+ });
711
921
  }
712
922
  if (command === "open") {
713
- return openDashboard(context, {
714
- projectRoot: requiredOption(parsed, "project-root"),
715
- viewer: optionalOption(parsed, "viewer"),
716
- format: optionalOption(parsed, "format")
923
+ const target = resolveDashboardTarget(context, {
924
+ action,
925
+ project: optionalOption(parsed, "project"),
926
+ projectRoot: optionalOption(parsed, "project-root"),
927
+ protocol: optionalOption(parsed, "protocol")
717
928
  });
929
+ return withDashboardGuidance(action, target, openDashboardForResolvedTarget(context, target, parsed));
718
930
  }
719
931
  if (command === "refresh") {
720
932
  const open = optionalOption(parsed, "open");
721
933
  if (typeof open !== "undefined" && !["auto", "true", "false"].includes(open)) {
722
934
  throw new AppError("validation", "--open must be auto, true, or false", 2);
723
935
  }
724
- return refreshDashboard(context, {
725
- projectRoot: requiredOption(parsed, "project-root"),
726
- open: open,
727
- format: optionalOption(parsed, "format"),
728
- protocol: optionalOption(parsed, "protocol")
936
+ const target = resolveDashboardTarget(context, {
937
+ action,
938
+ project: optionalOption(parsed, "project"),
939
+ projectRoot: optionalOption(parsed, "project-root"),
940
+ protocol: optionalOption(parsed, "protocol"),
941
+ all: hasOption(parsed, "all")
729
942
  });
943
+ return withDashboardGuidance(action, target, refreshDashboardForResolvedTarget(context, target, parsed, open));
730
944
  }
731
945
  if (command === "refresh-global") {
732
- return refreshGlobalDashboard(context, { output: optionalOption(parsed, "output"), format: optionalOption(parsed, "format") });
946
+ const target = resolveDashboardTarget(context, { action: "refresh", global: true });
947
+ return withDashboardGuidance("refresh", target, refreshGlobalDashboard(context, { output: optionalOption(parsed, "output"), format: optionalOption(parsed, "format") }), {
948
+ compatibilityAlias: true,
949
+ preferredCommand: "dd-flow dashboard refresh"
950
+ });
733
951
  }
734
952
  throw new AppError("usage", `Unknown dashboard command: ${command ?? "<empty>"}`, 2);
735
953
  }
954
+ function dashboardAction(command) {
955
+ if (command === "data" || command === "render" || command === "open" || command === "refresh") {
956
+ return command;
957
+ }
958
+ if (command === "render-global")
959
+ return "render";
960
+ if (command === "refresh-global")
961
+ return "refresh";
962
+ throw new AppError("usage", `Unknown dashboard command: ${command ?? "<empty>"}`, 2);
963
+ }
964
+ function dashboardDataForResolvedTarget(context, target) {
965
+ if (target.kind === "all_projects") {
966
+ throw new AppError("validation", "dashboard data does not support --all", 2, { related_commands: relatedDashboardCommands("data") });
967
+ }
968
+ if (target.kind === "global") {
969
+ return dashboardData(context, { global: true });
970
+ }
971
+ if (target.kind === "protocol") {
972
+ return dashboardData(context, { projectRoot: target.project.root, protocol: target.protocolId });
973
+ }
974
+ return dashboardData(context, { projectRoot: target.project.root });
975
+ }
976
+ function renderDashboardForResolvedTarget(context, target, parsed) {
977
+ if (target.kind === "all_projects") {
978
+ throw new AppError("validation", "dashboard render does not support --all", 2, { related_commands: relatedDashboardCommands("render") });
979
+ }
980
+ if (target.kind === "global") {
981
+ return renderGlobalDashboard(context, { output: optionalOption(parsed, "output"), format: optionalOption(parsed, "format") ?? "html" });
982
+ }
983
+ return renderDashboard(context, {
984
+ projectRoot: target.project.root,
985
+ output: optionalOption(parsed, "output"),
986
+ format: optionalOption(parsed, "format") ?? (hasOption(parsed, "project") ? "html" : undefined),
987
+ ...(target.kind === "protocol" ? { protocol: target.protocolId } : {})
988
+ });
989
+ }
990
+ function openDashboardForResolvedTarget(context, target, parsed) {
991
+ if (target.kind === "all_projects") {
992
+ throw new AppError("validation", "dashboard open --all is not supported", 2, {
993
+ preferred_command: "dd-flow dashboard refresh --all",
994
+ related_commands: relatedDashboardCommands("open", "all_projects")
995
+ });
996
+ }
997
+ if (target.kind === "global") {
998
+ return openGlobalDashboard(context, { viewer: optionalOption(parsed, "viewer"), format: optionalOption(parsed, "format") ?? "html" });
999
+ }
1000
+ if (target.kind === "protocol") {
1001
+ return openProtocolDashboard(context, {
1002
+ projectRoot: target.project.root,
1003
+ protocol: target.protocolId,
1004
+ viewer: optionalOption(parsed, "viewer"),
1005
+ format: optionalOption(parsed, "format") ?? "html"
1006
+ });
1007
+ }
1008
+ return openDashboard(context, {
1009
+ projectRoot: target.project.root,
1010
+ viewer: optionalOption(parsed, "viewer"),
1011
+ format: optionalOption(parsed, "format") ?? (hasOption(parsed, "project") ? "html" : undefined)
1012
+ });
1013
+ }
1014
+ function refreshDashboardForResolvedTarget(context, target, parsed, open) {
1015
+ if (target.kind === "all_projects") {
1016
+ return refreshAllDashboards(context, { format: optionalOption(parsed, "format") ?? "html" });
1017
+ }
1018
+ if (target.kind === "global") {
1019
+ return refreshGlobalDashboard(context, { output: optionalOption(parsed, "output"), format: optionalOption(parsed, "format") ?? "html" });
1020
+ }
1021
+ return refreshDashboard(context, {
1022
+ projectRoot: target.project.root,
1023
+ open,
1024
+ format: optionalOption(parsed, "format") ?? (hasOption(parsed, "project") ? "html" : undefined),
1025
+ ...(target.kind === "protocol" ? { protocol: target.protocolId } : {})
1026
+ });
1027
+ }
1028
+ function withDashboardGuidance(action, target, result, options = {}) {
1029
+ const record = result && typeof result === "object" && !Array.isArray(result) ? result : { ok: true, result };
1030
+ return {
1031
+ ...record,
1032
+ target: dashboardTargetSummary(target),
1033
+ ...(options.compatibilityAlias ? { compatibility_alias: true } : {}),
1034
+ preferred_command: options.preferredCommand ?? preferredDashboardCommand(action, target),
1035
+ related_commands: relatedDashboardCommands(action, target.kind)
1036
+ };
1037
+ }
736
1038
  function dispatchSchema(command, parsed) {
737
1039
  if (command === "validate") {
738
1040
  const schemaDir = optionalOption(parsed, "schema-dir");
@@ -750,6 +1052,21 @@ async function dispatchLane(context, command, parsed) {
750
1052
  if (command === "status") {
751
1053
  return getLaneStatus(context, { projectRoot: requiredOption(parsed, "project-root"), lane: optionalOption(parsed, "lane") });
752
1054
  }
1055
+ if (command === "waiters") {
1056
+ return getLaneWaiters(context, { projectRoot: requiredOption(parsed, "project-root"), lane: optionalOption(parsed, "lane") });
1057
+ }
1058
+ if (command === "waiter") {
1059
+ const action = requiredPosition(parsed, 0, "lane waiter action");
1060
+ if (action === "cancel") {
1061
+ return cancelLaneWaiter(context, {
1062
+ projectRoot: requiredOption(parsed, "project-root"),
1063
+ lane: requiredOption(parsed, "lane"),
1064
+ workerId: requiredOption(parsed, "worker-id"),
1065
+ reason: requiredOption(parsed, "reason")
1066
+ });
1067
+ }
1068
+ throw new AppError("usage", `Unknown lane waiter action: ${action}`, 2);
1069
+ }
753
1070
  if (command === "workspace") {
754
1071
  const action = requiredPosition(parsed, 0, "lane workspace action");
755
1072
  if (action === "set") {
@@ -814,6 +1131,18 @@ async function dispatchLane(context, command, parsed) {
814
1131
  pollIntervalSeconds: optionalNumber(parsed, "poll-interval")
815
1132
  });
816
1133
  }
1134
+ if (action === "wait-acquire") {
1135
+ return waitAcquireLaneLock(context, {
1136
+ projectRoot: requiredOption(parsed, "project-root"),
1137
+ lane: requiredOption(parsed, "lane"),
1138
+ workerId: requiredOption(parsed, "worker-id"),
1139
+ workspacePath: workspacePathOption(parsed),
1140
+ timeoutSeconds: optionalNumber(parsed, "timeout"),
1141
+ pollIntervalSeconds: optionalNumber(parsed, "poll-interval"),
1142
+ ttlSeconds: optionalNumber(parsed, "ttl"),
1143
+ reason: requiredOption(parsed, "reason")
1144
+ });
1145
+ }
817
1146
  throw new AppError("usage", `Unknown lane lock action: ${action}`, 2);
818
1147
  }
819
1148
  throw new AppError("usage", `Unknown lane command: ${command ?? "<empty>"}`, 2);
@@ -1148,11 +1477,14 @@ function projectRootForLaneMutation(command, parsed) {
1148
1477
  if (command === "workspace" && requiredPosition(parsed, 0, "lane workspace action") === "set") {
1149
1478
  return requiredOption(parsed, "project-root");
1150
1479
  }
1480
+ if (command === "waiter" && requiredPosition(parsed, 0, "lane waiter action") === "cancel") {
1481
+ return requiredOption(parsed, "project-root");
1482
+ }
1151
1483
  if (command !== "lock") {
1152
1484
  return undefined;
1153
1485
  }
1154
1486
  const action = requiredPosition(parsed, 0, "lane lock action");
1155
- return ["acquire", "heartbeat", "release"].includes(action) ? requiredOption(parsed, "project-root") : undefined;
1487
+ return ["acquire", "heartbeat", "release", "wait-acquire"].includes(action) ? requiredOption(parsed, "project-root") : undefined;
1156
1488
  }
1157
1489
  function projectRootForMergeQueueMutation(context, command, parsed) {
1158
1490
  if (["next", "wait-next"].includes(command ?? "")) {
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "dd-flow/compatibility@1",
4
4
  "title": "dd-flow compatibility manifest",
5
- "description": "Canonical compatibility map between a Memory Bank release and the dd-flow CLI package required by that release.",
5
+ "description": "Canonical compatibility map between a Memory Bank release, tool packages, routed CLI engine expectations and machine data contracts required or recommended by that release.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
- "required": ["schema_id", "memory_bank_version", "dd_flow_cli"],
8
+ "required": ["schema_id", "memory_bank_version", "dd_flow_cli", "router", "engine", "contracts", "migrations", "mb_lint"],
9
9
  "properties": {
10
10
  "schema_id": { "const": "dd-flow/compatibility@1" },
11
11
  "memory_bank_version": { "type": "string", "minLength": 1 },
@@ -21,6 +21,85 @@
21
21
  "version_contract": { "type": "string", "minLength": 1 },
22
22
  "flow_contract": { "type": "string", "minLength": 1 }
23
23
  }
24
+ },
25
+ "router": {
26
+ "type": "object",
27
+ "additionalProperties": false,
28
+ "required": ["package_name", "min_version", "recommended_version", "routing_model", "engine_store"],
29
+ "properties": {
30
+ "package_name": { "type": "string", "minLength": 1 },
31
+ "min_version": { "type": "string", "minLength": 1 },
32
+ "recommended_version": { "type": "string", "minLength": 1 },
33
+ "routing_model": { "type": "string", "minLength": 1 },
34
+ "engine_store": { "type": "string", "minLength": 1 },
35
+ "notes": { "type": "string" }
36
+ }
37
+ },
38
+ "engine": {
39
+ "type": "object",
40
+ "additionalProperties": false,
41
+ "required": ["package_name", "version_range", "recommended_version", "install_hint", "old_engine_install_hint", "first_engine_installable_version", "legacy_direct_cli_before", "manifest_contract", "status"],
42
+ "properties": {
43
+ "package_name": { "type": "string", "minLength": 1 },
44
+ "version_range": { "type": "string", "minLength": 1 },
45
+ "recommended_version": { "type": "string", "minLength": 1 },
46
+ "install_hint": { "type": "string", "minLength": 1 },
47
+ "old_engine_install_hint": { "type": "string", "minLength": 1 },
48
+ "first_engine_installable_version": { "type": "string", "minLength": 1 },
49
+ "legacy_direct_cli_before": { "type": "string", "minLength": 1 },
50
+ "manifest_contract": { "type": "string", "minLength": 1 },
51
+ "status": { "type": "string", "minLength": 1 }
52
+ }
53
+ },
54
+ "contracts": {
55
+ "type": "object",
56
+ "additionalProperties": false,
57
+ "required": ["storage_contract_set", "project_summary_contract", "project_dashboard_contract", "global_dashboard_contract", "status_contract", "version_contract"],
58
+ "properties": {
59
+ "storage_contract_set": { "type": "string", "minLength": 1 },
60
+ "project_summary_contract": { "type": "string", "minLength": 1 },
61
+ "project_dashboard_contract": { "type": "string", "minLength": 1 },
62
+ "global_dashboard_contract": { "type": "string", "minLength": 1 },
63
+ "status_contract": { "type": "string", "minLength": 1 },
64
+ "version_contract": { "type": "string", "minLength": 1 }
65
+ }
66
+ },
67
+ "global_dashboard": {
68
+ "type": "object",
69
+ "additionalProperties": false,
70
+ "required": ["supported_project_summary_contracts", "unsupported_project_visibility", "summary_version_groups"],
71
+ "properties": {
72
+ "supported_project_summary_contracts": {
73
+ "type": "array",
74
+ "minItems": 1,
75
+ "items": { "type": "string", "minLength": 1 }
76
+ },
77
+ "unsupported_project_visibility": { "type": "boolean" },
78
+ "summary_version_groups": { "type": "boolean" }
79
+ }
80
+ },
81
+ "migrations": {
82
+ "type": "object",
83
+ "additionalProperties": false,
84
+ "required": ["mode", "from_previous", "to_this", "adjacent_only", "normal_commands_may_migrate"],
85
+ "properties": {
86
+ "mode": { "const": "mb-upgrade-only" },
87
+ "from_previous": { "type": ["string", "null"] },
88
+ "to_this": { "type": "string", "minLength": 1 },
89
+ "adjacent_only": { "type": "boolean" },
90
+ "normal_commands_may_migrate": { "const": false }
91
+ }
92
+ },
93
+ "mb_lint": {
94
+ "type": "object",
95
+ "additionalProperties": false,
96
+ "required": ["package_name", "min_version", "recommended_version", "command_contract"],
97
+ "properties": {
98
+ "package_name": { "type": "string", "minLength": 1 },
99
+ "min_version": { "type": "string", "minLength": 1 },
100
+ "recommended_version": { "type": "string", "minLength": 1 },
101
+ "command_contract": { "type": "string", "minLength": 1 }
102
+ }
24
103
  }
25
104
  }
26
105
  }