@deksden-com/dd-flow-cli 0.3.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 (37) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +74 -5
  3. package/dist/build-info.json +5 -5
  4. package/dist/cli/help.js +103 -15
  5. package/dist/cli/run-cli.js +307 -27
  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/cleanup.js +31 -0
  16. package/dist/services/cli-operation-classifier.js +104 -0
  17. package/dist/services/compatibility-preflight.js +124 -0
  18. package/dist/services/config.js +6 -0
  19. package/dist/services/dashboard-targets.js +95 -0
  20. package/dist/services/dashboard.js +376 -59
  21. package/dist/services/engines.js +532 -0
  22. package/dist/services/flow-guidance.js +8 -1
  23. package/dist/services/hooks.js +1 -1
  24. package/dist/services/lanes.js +333 -1
  25. package/dist/services/merge-queue.js +97 -15
  26. package/dist/services/merge-worker.js +36 -2
  27. package/dist/services/migrations.js +231 -0
  28. package/dist/services/project-summary.js +122 -0
  29. package/dist/services/projects.js +41 -6
  30. package/dist/services/protocol-lifecycle.js +144 -0
  31. package/dist/services/protocols.js +29 -7
  32. package/dist/services/sessions.js +21 -4
  33. package/dist/services/status.js +10 -0
  34. package/dist/services/version-status.js +39 -15
  35. package/dist/storage/database.js +25 -0
  36. package/dist/storage/paths.js +12 -0
  37. package/package.json +3 -2
@@ -10,19 +10,24 @@ import { getMergeWorkerStatus, oneShotMergeClaim, startMergeWorker, stopProjectM
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";
20
21
  import { attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunStatus, listFlowRuns, startFlowRun } from "../services/runs.js";
21
22
  import { previewNextEntityId } from "../services/ids.js";
22
23
  import { validateSchema } from "../services/schema-validation.js";
23
24
  import { preflightMemoryPermissions } from "../services/memory-permissions.js";
25
+ import { planMigration, verifyMigrationReport } from "../services/migrations.js";
26
+ import { publishProjectSummary } from "../services/project-summary.js";
24
27
  import { requireProjectByRoot } from "../services/projects.js";
25
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";
26
31
  const defaultIo = {
27
32
  stdout: process.stdout,
28
33
  stderr: process.stderr,
@@ -51,7 +56,16 @@ export async function runCli(args, io = defaultIo, env = process.env) {
51
56
  }
52
57
  const context = createContext(env);
53
58
  emitHumanProgress(io, output, progress, "start");
54
- 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);
55
69
  refreshDashboardsAfterMutation(context, output.args, result);
56
70
  emitHumanProgress(io, output, progress, "done");
57
71
  const payload = attachProgress(result, progress, output);
@@ -70,10 +84,34 @@ export async function runCli(args, io = defaultIo, env = process.env) {
70
84
  if (output.json)
71
85
  writeJson(io.stderr, errorPayload);
72
86
  else
73
- io.stderr.write(`dd-flow: ${payload.error.message}\n`);
87
+ io.stderr.write(renderHumanError(payload));
74
88
  return isAppError(error) ? error.exitCode : 1;
75
89
  }
76
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
+ }
77
115
  function parseOutputOptions(args) {
78
116
  const stripped = [];
79
117
  let json = false;
@@ -96,7 +134,7 @@ function progressForCommand(args) {
96
134
  { phase: "done", message: "Merge queue wait finished", at: "" }
97
135
  ];
98
136
  }
99
- if (family === "lane" && command === "lock" && subcommand === "wait") {
137
+ if (family === "lane" && command === "lock" && ["wait", "wait-acquire"].includes(subcommand ?? "")) {
100
138
  return [
101
139
  { phase: "start", message: "Starting lane lock wait", at: "" },
102
140
  { phase: "running", message: "Waiting for lane lock", at: "" },
@@ -164,7 +202,15 @@ function renderHumanResult(args, result) {
164
202
  }
165
203
  if (family === "version") {
166
204
  const cli = recordObject(record.cli);
167
- return `dd-flow ${stringValue(cli?.version) ?? "unknown"}\n`;
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);
168
214
  }
169
215
  const lines = [`dd-flow ${family ?? ""}${command ? ` ${command}` : ""}: ${ok}`.trim()];
170
216
  const project = record.project && typeof record.project === "object" ? record.project : null;
@@ -230,6 +276,63 @@ function renderStatusHuman(record) {
230
276
  appendFlowGuidanceHuman(lines, record);
231
277
  return `${lines.join("\n")}\n`;
232
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
+ }
233
336
  function appendFlowGuidanceHuman(lines, record) {
234
337
  const guidance = recordObject(record.flow_guidance);
235
338
  if (!guidance)
@@ -261,6 +364,9 @@ function shortSha(value) {
261
364
  async function dispatch(args, context, io) {
262
365
  const [family, command, ...rest] = args;
263
366
  const parsed = parseArgs(rest);
367
+ if (family === "engine") {
368
+ return dispatchEngine(context, command, parsed);
369
+ }
264
370
  if (family === "status") {
265
371
  const rootParsed = parseArgs([command ?? "", ...rest]);
266
372
  return getRuntimeStatus(context, {
@@ -280,6 +386,12 @@ async function dispatch(args, context, io) {
280
386
  if (family === "project" && command === "status") {
281
387
  return getProjectStatus(context, { root: requiredOption(parsed, "root") });
282
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
+ }
283
395
  if (family === "project" && command === "resolve") {
284
396
  return resolveProject(context, { idOrAlias: requiredPosition(parsed, 0, "id-or-alias") });
285
397
  }
@@ -418,6 +530,9 @@ async function dispatch(args, context, io) {
418
530
  if (family === "memory") {
419
531
  return dispatchMemory(command, parsed);
420
532
  }
533
+ if (family === "migration") {
534
+ return dispatchMigration(context, command, parsed);
535
+ }
421
536
  if (family === "codex" && command === "hooks") {
422
537
  return dispatchCodexHooks(context, parsed);
423
538
  }
@@ -432,6 +547,41 @@ async function dispatch(args, context, io) {
432
547
  }
433
548
  throw new AppError("usage", `Unknown command: ${args.join(" ") || "<empty>"}`, 2);
434
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
+ }
435
585
  function dispatchCanon(context, command, parsed) {
436
586
  if (command === "register") {
437
587
  return registerCanonRoot(context, { root: requiredOption(parsed, "root") });
@@ -690,49 +840,149 @@ function dispatchCmux(context, parsed) {
690
840
  throw new AppError("usage", `Unknown integration cmux action: ${action}`, 2);
691
841
  }
692
842
  function dispatchDashboard(context, command, parsed) {
843
+ const action = dashboardAction(command);
693
844
  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")
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")
699
851
  });
852
+ return withDashboardGuidance(action, target, dashboardDataForResolvedTarget(context, target));
700
853
  }
701
854
  if (command === "render") {
702
- return renderDashboard(context, {
703
- projectRoot: requiredOption(parsed, "project-root"),
704
- output: optionalOption(parsed, "output"),
705
- format: optionalOption(parsed, "format"),
855
+ const target = resolveDashboardTarget(context, {
856
+ action,
857
+ project: optionalOption(parsed, "project"),
858
+ projectRoot: optionalOption(parsed, "project-root"),
706
859
  protocol: optionalOption(parsed, "protocol")
707
860
  });
861
+ return withDashboardGuidance(action, target, renderDashboardForResolvedTarget(context, target, parsed));
708
862
  }
709
863
  if (command === "render-global") {
710
- return renderGlobalDashboard(context, { output: optionalOption(parsed, "output"), format: optionalOption(parsed, "format") });
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
+ });
711
869
  }
712
870
  if (command === "open") {
713
- return openDashboard(context, {
714
- projectRoot: requiredOption(parsed, "project-root"),
715
- viewer: optionalOption(parsed, "viewer"),
716
- format: optionalOption(parsed, "format")
871
+ const target = resolveDashboardTarget(context, {
872
+ action,
873
+ project: optionalOption(parsed, "project"),
874
+ projectRoot: optionalOption(parsed, "project-root"),
875
+ protocol: optionalOption(parsed, "protocol")
717
876
  });
877
+ return withDashboardGuidance(action, target, openDashboardForResolvedTarget(context, target, parsed));
718
878
  }
719
879
  if (command === "refresh") {
720
880
  const open = optionalOption(parsed, "open");
721
881
  if (typeof open !== "undefined" && !["auto", "true", "false"].includes(open)) {
722
882
  throw new AppError("validation", "--open must be auto, true, or false", 2);
723
883
  }
724
- return refreshDashboard(context, {
725
- projectRoot: requiredOption(parsed, "project-root"),
726
- open: open,
727
- format: optionalOption(parsed, "format"),
728
- protocol: optionalOption(parsed, "protocol")
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")
729
890
  });
891
+ return withDashboardGuidance(action, target, refreshDashboardForResolvedTarget(context, target, parsed, open));
730
892
  }
731
893
  if (command === "refresh-global") {
732
- return refreshGlobalDashboard(context, { output: optionalOption(parsed, "output"), format: optionalOption(parsed, "format") });
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
+ });
733
899
  }
734
900
  throw new AppError("usage", `Unknown dashboard command: ${command ?? "<empty>"}`, 2);
735
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
+ }
736
986
  function dispatchSchema(command, parsed) {
737
987
  if (command === "validate") {
738
988
  const schemaDir = optionalOption(parsed, "schema-dir");
@@ -750,6 +1000,21 @@ async function dispatchLane(context, command, parsed) {
750
1000
  if (command === "status") {
751
1001
  return getLaneStatus(context, { projectRoot: requiredOption(parsed, "project-root"), lane: optionalOption(parsed, "lane") });
752
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
+ }
753
1018
  if (command === "workspace") {
754
1019
  const action = requiredPosition(parsed, 0, "lane workspace action");
755
1020
  if (action === "set") {
@@ -814,6 +1079,18 @@ async function dispatchLane(context, command, parsed) {
814
1079
  pollIntervalSeconds: optionalNumber(parsed, "poll-interval")
815
1080
  });
816
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
+ }
817
1094
  throw new AppError("usage", `Unknown lane lock action: ${action}`, 2);
818
1095
  }
819
1096
  throw new AppError("usage", `Unknown lane command: ${command ?? "<empty>"}`, 2);
@@ -1148,11 +1425,14 @@ function projectRootForLaneMutation(command, parsed) {
1148
1425
  if (command === "workspace" && requiredPosition(parsed, 0, "lane workspace action") === "set") {
1149
1426
  return requiredOption(parsed, "project-root");
1150
1427
  }
1428
+ if (command === "waiter" && requiredPosition(parsed, 0, "lane waiter action") === "cancel") {
1429
+ return requiredOption(parsed, "project-root");
1430
+ }
1151
1431
  if (command !== "lock") {
1152
1432
  return undefined;
1153
1433
  }
1154
1434
  const action = requiredPosition(parsed, 0, "lane lock action");
1155
- 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;
1156
1436
  }
1157
1437
  function projectRootForMergeQueueMutation(context, command, parsed) {
1158
1438
  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
  }
@@ -0,0 +1,61 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "dd-flow/engine-manifest@1",
4
+ "title": "dd-flow engine manifest",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schema_id",
9
+ "package_name",
10
+ "package_version",
11
+ "engine_id",
12
+ "engine_version",
13
+ "installed_at",
14
+ "install_source",
15
+ "package_root",
16
+ "snapshot_root",
17
+ "entrypoint",
18
+ "supports_memorybank",
19
+ "supports_contracts",
20
+ "integrity"
21
+ ],
22
+ "properties": {
23
+ "schema_id": { "const": "dd-flow/engine-manifest@1" },
24
+ "package_name": { "type": "string", "minLength": 1 },
25
+ "package_version": { "type": "string", "minLength": 1 },
26
+ "engine_id": { "const": "dd-flow-engine" },
27
+ "engine_version": { "type": "string", "minLength": 1 },
28
+ "installed_at": { "type": "string", "format": "date-time" },
29
+ "install_source": {
30
+ "type": "string",
31
+ "enum": ["bundled_package", "npx_package", "local_development"]
32
+ },
33
+ "package_root": { "type": "string", "minLength": 1 },
34
+ "snapshot_root": { "type": "string", "minLength": 1 },
35
+ "entrypoint": { "type": "string", "minLength": 1 },
36
+ "supports_memorybank": {
37
+ "type": "array",
38
+ "items": { "type": "string", "minLength": 1 }
39
+ },
40
+ "supports_contracts": {
41
+ "type": "object",
42
+ "additionalProperties": {
43
+ "type": "array",
44
+ "items": { "type": "string", "minLength": 1 }
45
+ }
46
+ },
47
+ "integrity": {
48
+ "type": "object",
49
+ "additionalProperties": false,
50
+ "required": ["source", "checksum", "mode"],
51
+ "properties": {
52
+ "source": { "const": "package_snapshot" },
53
+ "checksum": { "type": ["string", "null"] },
54
+ "mode": {
55
+ "type": "string",
56
+ "enum": ["file_list", "full_content", "package_manifest", "degraded_local_development"]
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
@@ -18,6 +18,23 @@
18
18
  "type": "string",
19
19
  "minLength": 1
20
20
  },
21
+ "lifecycle": {
22
+ "type": "object",
23
+ "additionalProperties": true,
24
+ "required": ["flow", "stage", "status", "terminal", "raw_stage", "raw_status"],
25
+ "properties": {
26
+ "flow": { "type": "string", "minLength": 1 },
27
+ "stage": { "type": "string", "minLength": 1 },
28
+ "substage": { "type": ["string", "null"] },
29
+ "status": { "type": "string", "minLength": 1 },
30
+ "terminal": { "type": "boolean" },
31
+ "legacy_stage": { "type": ["string", "null"] },
32
+ "legacy_status": { "type": ["string", "null"] },
33
+ "raw_stage": { "type": "string", "minLength": 1 },
34
+ "raw_status": { "type": "string", "minLength": 1 },
35
+ "queue_status": { "type": ["string", "null"] }
36
+ }
37
+ },
21
38
  "allowed_next_stages": {
22
39
  "type": "array",
23
40
  "items": {