@deksden-com/dd-flow-cli 0.6.0 → 0.7.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 (34) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +8 -1
  3. package/dist/build-info.json +5 -5
  4. package/dist/cli/help.js +20 -8
  5. package/dist/cli/run-cli.js +104 -24
  6. package/dist/domain/flow-contract.js +11 -0
  7. package/dist/runtime/context.js +8 -2
  8. package/dist/schemas/code-stage-report.schema.json +7 -2
  9. package/dist/schemas/engine-manifest.schema.json +22 -0
  10. package/dist/schemas/flow-run.schema.json +1 -0
  11. package/dist/schemas/mb-upgrade-migration-report.schema.json +3 -1
  12. package/dist/schemas/merge-stage-report-legacy-0.4.2.schema.json +24 -0
  13. package/dist/schemas/run-engine-binding.schema.json +37 -0
  14. package/dist/schemas/stage-prompt.schema.json +4 -4
  15. package/dist/services/canon.js +15 -1
  16. package/dist/services/cli-operation-classifier.js +52 -8
  17. package/dist/services/compatibility-preflight.js +1 -1
  18. package/dist/services/dashboard.js +2 -2
  19. package/dist/services/engines.js +408 -30
  20. package/dist/services/hooks.js +1 -5
  21. package/dist/services/lanes.js +0 -4
  22. package/dist/services/merge-queue.js +48 -0
  23. package/dist/services/merge-worker.js +3 -4
  24. package/dist/services/migrations.js +307 -44
  25. package/dist/services/plan-runtime.js +4 -4
  26. package/dist/services/plans.js +5 -3
  27. package/dist/services/protocols.js +23 -2
  28. package/dist/services/run-engine-bindings.js +157 -0
  29. package/dist/services/runs.js +21 -7
  30. package/dist/services/schema-validation.js +96 -0
  31. package/dist/services/stage-lifecycle.js +98 -10
  32. package/dist/services/status.js +8 -3
  33. package/dist/storage/database.js +32 -11
  34. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @deksden-com/dd-flow-cli
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add target-driven engine routing, explicit storage migration safety, immutable RUN engine bindings, engine schema registry validation, and self-contained plan execution guidance.
8
+
3
9
  ## 0.6.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -355,9 +355,16 @@ Codex integration is explicit and auditable. Managed Codex homes and hooks are l
355
355
  ```bash
356
356
  dd-flow codex home plan --project-root "$PROJECT_ROOT" --json
357
357
  dd-flow codex hooks status --project-root "$PROJECT_ROOT" --target isolated --json
358
- dd-flow codex hook handle --event Stop --project-root "$PROJECT_ROOT" --json
358
+ dd-flow codex hook handle --event Stop --json
359
359
  ```
360
360
 
361
+ Installed Codex hooks intentionally omit `--project-root`: Codex supplies the
362
+ session `cwd` in the hook JSON on stdin, and the handler resolves the registered
363
+ project from that path. The option remains available as a manual override. If
364
+ the CLI is upgraded, reinstall the hook in each active `CODEX_HOME`, verify it
365
+ with `dd-flow codex hooks status`, and start a new Codex session;
366
+ `--project-root` belongs to installer/status commands, not the stored handler.
367
+
361
368
  ## Verification
362
369
 
363
370
  Local verification for this repository:
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.6.0",
4
- "cli_commit": "22cf9c8f5293168dae76212c9a4b74b90a1efeab",
5
- "built_at": "2026-08-12T05:09:56.163Z",
3
+ "cli_version": "0.7.0",
4
+ "cli_commit": "a09f6b663231d3179bea64b2fd9bb4ffa30096c2",
5
+ "built_at": "2026-08-13T04:31:51.147Z",
6
6
  "built_with_canon": {
7
- "version": "3.1.0",
8
- "commit": "c201167b896a039ff4881d6d816b9735fba2f799",
7
+ "version": "3.2.0",
8
+ "commit": "2a1aaec84ee7d62b9f5a6549de5d1f0cb88082c0",
9
9
  "flow_contract": "dd-flow-canonical-2026-08",
10
10
  "repo_root": "/Users/deksden/Documents/_Projects/dd-memorybank",
11
11
  "memorybank_root": "/Users/deksden/Documents/_Projects/dd-memorybank/.memory-bank",
package/dist/cli/help.js CHANGED
@@ -12,13 +12,13 @@ Core commands:
12
12
  version Print the installed dd-flow CLI package version.
13
13
  status Inspect dd-flow runtime, project and canon roots.
14
14
  engine install/list/info Manage locally installed dd-flow engine snapshots.
15
- engine resolve/doctor Inspect router engine selection and health.
15
+ engine resolve/doctor/bind-run Inspect router selection, health, and recover a legacy RUN binding.
16
16
  canon register/status/resolve Register and resolve the canonical Memory Bank.
17
17
  project register/status/summary Register a project, inspect state, and publish project summaries.
18
18
  project resolve/archive Resolve typed ids and archive stale project roots.
19
19
  project migrate-ids Migrate old project ids.
20
20
  protocol register/status/transition Register, inspect, transition and repair protocol state.
21
- migration impact/plan/report/verify Assess release impact and prepare mb-upgrade migration evidence.
21
+ migration impact/plan/report/verify/apply Assess release impact and explicitly apply mb-upgrade migration evidence.
22
22
  cleanup scan/apply Detect and repair stale local runtime state.
23
23
  run start/status/list/timeline/usage/flags Track concrete flow executions, timing, source-aware usage and RUN-local flow flags.
24
24
  stage start/finish Run the canonical two-command stage lifecycle and generate evidence.
@@ -28,7 +28,7 @@ Core commands:
28
28
  lane workspace/lock/status Manage shared workspace lanes and leases.
29
29
  merge status/one-shot/bundle Inspect, claim, or complete merge work from the current session.
30
30
  merge-worker status/start/stop Manage long-lived project merge workers.
31
- merge-queue status/next Claim, wait for, complete, or fail merge jobs.
31
+ merge-queue status/claim/next Claim, wait for, complete, or fail merge jobs.
32
32
  session register/status/stop Register flow sessions and stop workers.
33
33
  dashboard data/render/open/refresh Render, refresh, open, and inspect project/global dashboards.
34
34
  schema validate Validate canonical dd-flow JSON data contracts.
@@ -86,6 +86,7 @@ Usage:
86
86
  dd-flow engine info [--package <name>] [--version <version>] --json
87
87
  dd-flow engine resolve [--project-root <root>] --json
88
88
  dd-flow engine doctor [--project-root <root>] --json
89
+ dd-flow engine bind-run <RUN-ID> --project-root <root> --engine-version <version> --reason <text> [--integrity-checksum <sha256>] --json
89
90
 
90
91
  The globally invoked dd-flow command is the stable router surface. Engine snapshots live under DD_FLOW_HOME/engines and are selected by project Memory Bank compatibility data. Running an old package through npx can install that package's own engine without replacing the global router:
91
92
 
@@ -93,11 +94,14 @@ The globally invoked dd-flow command is the stable router surface. Engine snapsh
93
94
 
94
95
  Router-native engine commands never mutate project runtime data. Normal project commands are routed to a compatible engine or fail closed with a missing-engine diagnostic.
95
96
 
97
+ engine bind-run is the one recovery command for a legacy RUN without an engine-binding sidecar. It probes the requested installed snapshot with run status before atomically writing an immutable binding; later RUN commands route through that snapshot.
98
+
96
99
  Examples:
97
100
  dd-flow engine install --json
98
101
  dd-flow engine list --json
99
102
  dd-flow engine resolve --project-root "$PWD" --json
100
- dd-flow engine doctor --project-root "$PWD" --json`
103
+ dd-flow engine doctor --project-root "$PWD" --json
104
+ dd-flow engine bind-run RUN-193 --project-root "$PWD" --engine-version 0.4.2 --reason "legacy flow-run@1" --json`
101
105
  ],
102
106
  [
103
107
  "status",
@@ -234,11 +238,12 @@ New registrations use full typed ids such as PRJ-001-dd-flow-playground and shor
234
238
  `dd-flow migration - plan and verify mb-upgrade runtime/home data migrations
235
239
 
236
240
  Usage:
237
- dd-flow migration plan --project-root <root> [--source-version <version>] [--target-version <version>] [--run <RUN-ID>] [--backup-path <path>] [--backup-created-at <iso>] [--allow-active true|false] --json
238
- dd-flow migration report --project-root <root> [--source-version <version>] [--target-version <version>] [--run <RUN-ID>] [--backup-path <path>] [--backup-created-at <iso>] [--allow-active true|false] --json
241
+ dd-flow migration plan --project-root <root> [--source-version <version>] [--target-version <version>] [--run <RUN-ID>] [--backup-path <path>] [--backup-created-at <iso>] --json
242
+ dd-flow migration report --project-root <root> [--source-version <version>] [--target-version <version>] [--run <RUN-ID>] [--backup-path <path>] [--backup-created-at <iso>] --json
239
243
  dd-flow migration verify --file <mb-upgrade-migration-report.json> --json
244
+ dd-flow migration apply --project-root <root> --file <mb-upgrade-migration-report.json> --json
240
245
 
241
- Migration commands are evidence and guardrail helpers for canonical mb-upgrade. They do not silently migrate runtime/home data during ordinary CLI use. Applying migrations remains an explicit mb-upgrade contour with backup, adjacent migration chain, active-state handling, derived artifact regeneration and post-upgrade verification.
246
+ Migration commands are evidence and guardrail helpers for canonical mb-upgrade. They do not silently migrate runtime/home data during ordinary CLI use. Applying migrations is an explicit contour requiring a real verified backup, adjacent migration chain, quiescent shared home, recovery evidence and idempotent rerun.
242
247
 
243
248
  Related commands:
244
249
  dd-flow status --project-root <root> --json
@@ -354,13 +359,14 @@ start is idempotent: if a worker, claimed job, or active merge lock already exis
354
359
  Usage:
355
360
  dd-flow merge-queue status --project-root <root> --json
356
361
  dd-flow merge-queue next --project-root <root> --worker-id <id> [--path <workspace>] --json
362
+ dd-flow merge-queue claim <protocol-id> [--project-root <root>] --worker-id <id> [--path <workspace>] --json
357
363
  dd-flow merge-queue wait-next --project-root <root> --worker-id <id> [--path <workspace>] --timeout <seconds> --poll-interval <seconds> [--acquire-lock true] --json
358
364
  dd-flow merge-queue complete <protocol-id> [--project-root <root>] --worker-id <id> [--path <workspace>] --summary <text> --json
359
365
  dd-flow merge-queue note <protocol-id> [--project-root <root>] --worker-id <id> --summary <text> --json
360
366
  dd-flow merge-queue fail <protocol-id> [--project-root <root>] --worker-id <id> [--path <workspace>] --reason <text> --requeue true|false --json
361
367
  dd-flow merge-queue cancel <protocol-id> [--project-root <root>] --reason <text> [--worker-id <id>] [--path <workspace>] [--force] --json
362
368
 
363
- The worker must own the merge lane from the registered merge workspace before claiming, completing, or failing queued protocols. Outputs use queue_item/protocol/claim as the primary contract and retain job as a compatibility alias. note updates the final completion summary after post-complete cleanup and does not require a lane lock. --path defaults to the current working directory. Use --worker-id as the owner identity.
369
+ The worker must own the merge lane from the registered merge workspace before claiming, completing, or failing queued protocols. next remains FIFO; claim <protocol-id> atomically claims only that ready/requeued job. Outputs use queue_item/protocol/claim as the primary contract and retain job as a compatibility alias. note updates the final completion summary after post-complete cleanup and does not require a lane lock. --path defaults to the current working directory. Use --worker-id as the owner identity.
364
370
 
365
371
  For branch-level integration prefer:
366
372
  dd-flow merge bundle status --project-root "$PWD" --path "$PWD" --json
@@ -527,6 +533,12 @@ Usage:
527
533
  dd-flow codex home print-env --project-root <root> [--profile <name>] --json
528
534
  dd-flow codex home remove --project-root <root> [--profile <name>] --mode keep-shared|remove-owned --json
529
535
 
536
+ dd-flow codex hook handle --event <HookEventName> [--project-root <root>] --json
537
+
538
+ Managed hooks omit --project-root. Codex supplies cwd in the stdin payload; the
539
+ handler resolves the registered project from cwd. The option is only a manual
540
+ override.
541
+
530
542
  Managed homes isolate dd-flow hooks while sharing selected Codex state from the source home.`
531
543
  ],
532
544
  [
@@ -1,12 +1,12 @@
1
1
  import fs from "node:fs";
2
- import { createContext } from "../runtime/context.js";
2
+ import { createContext, createRouterContext } from "../runtime/context.js";
3
3
  import { helpForArgs } from "./help.js";
4
4
  import { AppError, isAppError } from "../shared/errors.js";
5
5
  import { writeJson } from "../shared/json.js";
6
6
  import { archiveProject, getProjectStatus, migrateProjectIds, registerProject, resolveProject } from "../services/projects.js";
7
7
  import { cancelProtocol, getProtocolBlockers, getProtocolStatus, getReadyProtocols, implementProtocol, readyForMerge, registerProtocol, requireProtocol, syncProtocolFromRun, transitionProtocol } from "../services/protocols.js";
8
8
  import { blockPlanItem, completePlanItem, getPlanStatus, skipPlanItem, startPlanItem } from "../services/plans.js";
9
- import { cancelMergeQueueJob, claimNextMergeJob, claimMergeBundle, completeMergeBundle, completeMergeJob, failMergeBundle, failMergeJob, getMergeBundleStatus, getMergeQueueStatus, noteMergeJob, waitNextMergeJob } from "../services/merge-queue.js";
9
+ import { cancelMergeQueueJob, claimNextMergeJob, claimMergeJob, claimMergeBundle, completeMergeBundle, completeMergeJob, failMergeBundle, failMergeJob, getMergeBundleStatus, getMergeQueueStatus, noteMergeJob, waitNextMergeJob } from "../services/merge-queue.js";
10
10
  import { getMergeWorkerStatus, oneShotMergeClaim, startMergeWorker, stopProjectMergeWorker } from "../services/merge-worker.js";
11
11
  import { cleanupApply, cleanupScan } from "../services/cleanup.js";
12
12
  import { getCodexHomeStatus, getCodexHooksStatus, handleCodexHook, initCodexHome, installCodexHooks, planCodexHome, printCodexHomeEnv, printCodexHooks, removeCodexHome, removeCodexHooks } from "../services/hooks.js";
@@ -24,12 +24,13 @@ import { attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunTi
24
24
  import { previewNextEntityId } from "../services/ids.js";
25
25
  import { validateSchema } from "../services/schema-validation.js";
26
26
  import { preflightMemoryPermissions } from "../services/memory-permissions.js";
27
- import { assessMigrationImpact, planMigration, verifyMigrationReport } from "../services/migrations.js";
27
+ import { applyMigration, assessMigrationImpact, assertUpgradeRunStorageReady, planMigration, verifyMigrationReport } from "../services/migrations.js";
28
28
  import { publishProjectSummary } from "../services/project-summary.js";
29
29
  import { requireProjectByRoot } from "../services/projects.js";
30
30
  import { resolveProjectRoot } from "../storage/paths.js";
31
- import { doctorEngines, engineInfo, installCurrentEngine, listEngines, resolveEngine, resolveOperationProjectRoot, routeArgsThroughEngine } from "../services/engines.js";
31
+ import { doctorEngines, bindRunEngine, engineInfo, installCurrentEngine, listEngines, resolveEngine, resolveExplicitProjectRoot, resolveOperationProjectRoot, routeArgsThroughEngine } from "../services/engines.js";
32
32
  import { preflightCliCompatibility } from "../services/compatibility-preflight.js";
33
+ import { classifyCliOperation, requiresExplicitUpgradeAuthorization } from "../services/cli-operation-classifier.js";
33
34
  import { renderWorkerPrompt } from "../services/prompts.js";
34
35
  import { finishStage, startStage } from "../services/stage-lifecycle.js";
35
36
  const defaultIo = {
@@ -58,20 +59,27 @@ export async function runCli(args, io = defaultIo, env = process.env) {
58
59
  io.stdout.write(`${help}\n`);
59
60
  return 0;
60
61
  }
61
- const context = createContext(env);
62
+ const classification = classifyCliOperation(output.args, env);
63
+ if (requiresExplicitUpgradeAuthorization(output.args) && classification.mode !== "mb_upgrade") {
64
+ throw new AppError("upgrade_authorization_required", "This mb-upgrade command requires the explicit mb-upgrade authorization marker and allowlisted target context", 1, {
65
+ operation: classification.operation,
66
+ required_marker: "DD_FLOW_COMPATIBILITY_MODE=mb-upgrade"
67
+ });
68
+ }
69
+ const routerContext = createRouterContext(env);
62
70
  emitHumanProgress(io, output, progress, "start");
63
- const scopeProjectRoot = resolveOperationProjectRoot(context, output.args);
64
- const routed = await routeArgsThroughEngine(context, output.args, io, io.stdin, env, scopeProjectRoot);
71
+ const scopeProjectRoot = resolveProjectRootBeforeRouting(output.args, env);
72
+ const routed = await routeArgsThroughEngine(routerContext, output.json ? [...output.args, "--json"] : output.args, io, io.stdin, env, scopeProjectRoot);
65
73
  if (routed?.routed && !routed.inProcess) {
66
74
  emitHumanProgress(io, output, progress, "done");
67
75
  return routed.exitCode;
68
76
  }
69
77
  const dispatchContext = routed?.inProcess
70
- ? createContext({ ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: "in-process", DD_FLOW_ROUTED_FROM: getCliVersionReport().cli.version })
71
- : context;
78
+ ? createContext({ ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: "in-process", DD_FLOW_ROUTED_FROM: getCliVersionReport().cli.version }, databaseModeForCommand(output.args, classification))
79
+ : createContext(env, databaseModeForCommand(output.args, classification));
72
80
  preflightCliCompatibility(dispatchContext, output.args, env, scopeProjectRoot);
73
- const result = await dispatch(output.args, dispatchContext, io, scopeProjectRoot);
74
- const refreshWarnings = refreshDashboardsAfterMutation(context, output.args, result, scopeProjectRoot);
81
+ const result = await dispatch(output.args, dispatchContext, io, scopeProjectRoot, classification);
82
+ const refreshWarnings = refreshDashboardsAfterMutation(dispatchContext, output.args, result, scopeProjectRoot);
75
83
  const resultWithWarnings = addWarnings(result, refreshWarnings);
76
84
  emitHumanProgress(io, output, progress, "done");
77
85
  const payload = attachProgress(resultWithWarnings, progress, output);
@@ -94,6 +102,34 @@ export async function runCli(args, io = defaultIo, env = process.env) {
94
102
  return isAppError(error) ? error.exitCode : 1;
95
103
  }
96
104
  }
105
+ function resolveProjectRootBeforeRouting(args, env) {
106
+ const explicit = resolveExplicitProjectRoot(args);
107
+ if (explicit)
108
+ return explicit;
109
+ try {
110
+ // Router identity lookup is read-only; it must not initialize or migrate runtime storage.
111
+ return resolveOperationProjectRoot(createContext(env, "read_existing"), args);
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ }
117
+ function databaseModeForCommand(args, classification) {
118
+ const [family, command] = args;
119
+ if (family === "engine" ||
120
+ family === "version" ||
121
+ family === "schema" ||
122
+ family === "canon" && command === "resolve" ||
123
+ classification.mode === "read_only_diagnostics" ||
124
+ classification.mode === "mb_upgrade" && (family === "status" ||
125
+ family === "engine" ||
126
+ family === "migration" && ["impact", "plan", "report", "verify", "apply"].includes(command ?? "") ||
127
+ family === "memory" && command === "permissions" ||
128
+ family === "dashboard" && ["data", "open"].includes(command ?? "") ||
129
+ family === "session" && command === "status"))
130
+ return "read_existing";
131
+ return "initialize";
132
+ }
97
133
  function renderHumanError(payload) {
98
134
  const lines = [`dd-flow: ${payload.error.message}`];
99
135
  const details = recordObject(payload.error.details);
@@ -380,11 +416,11 @@ function stringValue(value) {
380
416
  function shortSha(value) {
381
417
  return value ? value.slice(0, 12) : "unknown";
382
418
  }
383
- async function dispatch(args, context, io, scopeProjectRoot = null) {
419
+ async function dispatch(args, context, io, scopeProjectRoot = null, classification = classifyCliOperation(args, context.env)) {
384
420
  const [family, command, ...rest] = args;
385
421
  const parsed = parseArgs(rest);
386
422
  if (family === "engine") {
387
- return dispatchEngine(context, command, parsed);
423
+ return dispatchEngine(context, command, parsed, classification);
388
424
  }
389
425
  if (family === "status") {
390
426
  const rootParsed = parseArgs([command ?? "", ...rest]);
@@ -597,13 +633,13 @@ async function dispatch(args, context, io, scopeProjectRoot = null) {
597
633
  return dispatchDashboard(context, command, parsed);
598
634
  }
599
635
  if (family === "schema") {
600
- return dispatchSchema(command, parsed);
636
+ return dispatchSchema(context, command, parsed);
601
637
  }
602
638
  if (family === "memory") {
603
639
  return dispatchMemory(command, parsed);
604
640
  }
605
641
  if (family === "migration") {
606
- return dispatchMigration(context, command, parsed);
642
+ return dispatchMigration(context, command, parsed, classification);
607
643
  }
608
644
  if (family === "codex" && command === "hooks") {
609
645
  return dispatchCodexHooks(context, parsed);
@@ -619,7 +655,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null) {
619
655
  }
620
656
  throw new AppError("usage", `Unknown command: ${args.join(" ") || "<empty>"}`, 2);
621
657
  }
622
- function dispatchEngine(context, command, parsed) {
658
+ function dispatchEngine(context, command, parsed, classification) {
623
659
  if (command === "install") {
624
660
  return installCurrentEngine(context, { force: hasOption(parsed, "force") });
625
661
  }
@@ -630,14 +666,32 @@ function dispatchEngine(context, command, parsed) {
630
666
  return engineInfo(context, { packageName: optionalOption(parsed, "package"), version: optionalOption(parsed, "version") });
631
667
  }
632
668
  if (command === "resolve") {
633
- return resolveEngine(context, { projectRoot: optionalOption(parsed, "project-root") ?? optionalOption(parsed, "root") });
669
+ return resolveEngine(context, {
670
+ projectRoot: optionalOption(parsed, "project-root") ?? optionalOption(parsed, "root"),
671
+ env: context.env,
672
+ classification
673
+ });
634
674
  }
635
675
  if (command === "doctor") {
636
- return doctorEngines(context, { projectRoot: optionalOption(parsed, "project-root") ?? optionalOption(parsed, "root") });
676
+ return doctorEngines(context, {
677
+ projectRoot: optionalOption(parsed, "project-root") ?? optionalOption(parsed, "root"),
678
+ env: context.env,
679
+ classification
680
+ });
681
+ }
682
+ if (command === "bind-run") {
683
+ return bindRunEngine(context, {
684
+ projectRoot: requiredOption(parsed, "project-root"),
685
+ runId: requiredPosition(parsed, 0, "run-id"),
686
+ packageName: optionalOption(parsed, "package"),
687
+ engineVersion: requiredOption(parsed, "engine-version"),
688
+ integrityChecksum: optionalOption(parsed, "integrity-checksum"),
689
+ reason: requiredOption(parsed, "reason")
690
+ });
637
691
  }
638
692
  throw new AppError("usage", `Unknown engine command: ${command ?? "<empty>"}`, 2);
639
693
  }
640
- function dispatchMigration(context, command, parsed) {
694
+ function dispatchMigration(context, command, parsed, classification) {
641
695
  if (command === "impact")
642
696
  return assessMigrationImpact(context, { projectRoot: requiredOption(parsed, "project-root"), ...(optionalOption(parsed, "target-version") ? { targetVersion: optionalOption(parsed, "target-version") } : {}), ...(optionalOption(parsed, "mode") ? { mode: optionalOption(parsed, "mode") } : {}) });
643
697
  if (command === "plan" || command === "report") {
@@ -648,12 +702,26 @@ function dispatchMigration(context, command, parsed) {
648
702
  runId: optionalOption(parsed, "run"),
649
703
  backupPath: optionalOption(parsed, "backup-path"),
650
704
  backupCreatedAt: optionalOption(parsed, "backup-created-at"),
651
- allowActive: parseOptionalBoolean(optionalOption(parsed, "allow-active"), "allow-active") ?? false
705
+ allowActive: false,
706
+ classification
652
707
  });
653
708
  }
654
709
  if (command === "verify") {
655
710
  return verifyMigrationReport(context, { file: requiredOption(parsed, "file") });
656
711
  }
712
+ if (command === "apply") {
713
+ return applyMigration(context, {
714
+ projectRoot: requiredOption(parsed, "project-root"),
715
+ sourceVersion: optionalOption(parsed, "source-version"),
716
+ targetVersion: optionalOption(parsed, "target-version"),
717
+ runId: optionalOption(parsed, "run"),
718
+ backupPath: optionalOption(parsed, "backup-path"),
719
+ backupCreatedAt: optionalOption(parsed, "backup-created-at"),
720
+ reportFile: optionalOption(parsed, "file"),
721
+ allowActive: false,
722
+ classification
723
+ });
724
+ }
657
725
  throw new AppError("usage", `Unknown migration command: ${command ?? "<empty>"}`, 2);
658
726
  }
659
727
  function dispatchCanon(context, command, parsed) {
@@ -682,6 +750,14 @@ function dispatchMergeQueue(context, command, parsed, scopeProjectRoot) {
682
750
  workspacePath: workspacePathOption(parsed)
683
751
  });
684
752
  }
753
+ if (command === "claim") {
754
+ return claimMergeJob(context, {
755
+ protocolId: requiredPosition(parsed, 0, "protocol-id"),
756
+ projectRoot: requiredOptionOrScope(parsed, scopeProjectRoot, "project-root"),
757
+ workerId: requiredWorkerId(parsed),
758
+ workspacePath: workspacePathOption(parsed)
759
+ });
760
+ }
685
761
  if (command === "wait-next") {
686
762
  return waitNextMergeJob(context, {
687
763
  projectRoot: requiredOptionOrScope(parsed, scopeProjectRoot, "project-root"),
@@ -887,6 +963,9 @@ function dispatchSession(context, command, parsed) {
887
963
  }
888
964
  function dispatchRun(context, command, parsed) {
889
965
  if (command === "start") {
966
+ if (optionalOption(parsed, "flow-kind") === "mb-upgrade") {
967
+ assertUpgradeRunStorageReady(context, requiredOption(parsed, "project-root"));
968
+ }
890
969
  const protocolOverrides = readOptionalJsonFile(optionalOption(parsed, "protocol-override-file"));
891
970
  return startFlowRun(context, {
892
971
  projectRoot: requiredOption(parsed, "project-root"),
@@ -1144,7 +1223,7 @@ function withDashboardGuidance(action, target, result, options = {}) {
1144
1223
  related_commands: relatedDashboardCommands(action, target.kind)
1145
1224
  };
1146
1225
  }
1147
- function dispatchSchema(command, parsed) {
1226
+ function dispatchSchema(context, command, parsed) {
1148
1227
  if (command === "validate") {
1149
1228
  const schemaDir = optionalOption(parsed, "schema-dir");
1150
1229
  const projectRoot = optionalOption(parsed, "project-root");
@@ -1152,7 +1231,8 @@ function dispatchSchema(command, parsed) {
1152
1231
  schemaName: requiredOption(parsed, "schema"),
1153
1232
  file: requiredOption(parsed, "file"),
1154
1233
  ...(schemaDir ? { schemaDir } : {}),
1155
- ...(projectRoot ? { projectRoot } : {})
1234
+ ...(projectRoot ? { projectRoot } : {}),
1235
+ ddFlowHome: context.ddFlowHome
1156
1236
  });
1157
1237
  }
1158
1238
  throw new AppError("usage", `Unknown schema command: ${command ?? "<empty>"}`, 2);
@@ -1679,7 +1759,7 @@ function projectRootForLaneMutation(command, parsed) {
1679
1759
  return ["acquire", "heartbeat", "release", "wait-acquire"].includes(action) ? requiredOption(parsed, "project-root") : undefined;
1680
1760
  }
1681
1761
  function projectRootForMergeQueueMutation(context, command, parsed) {
1682
- if (["next", "wait-next"].includes(command ?? "")) {
1762
+ if (["next", "wait-next", "claim"].includes(command ?? "")) {
1683
1763
  return requiredOption(parsed, "project-root");
1684
1764
  }
1685
1765
  if (["complete", "fail", "cancel"].includes(command ?? "")) {
@@ -1700,7 +1780,7 @@ function isScopedProtocolMutation(family, command) {
1700
1780
  if (family === "worktree")
1701
1781
  return ["create", "bootstrap", "close"].includes(command ?? "");
1702
1782
  if (family === "merge-queue")
1703
- return ["complete", "note", "fail", "cancel"].includes(command ?? "");
1783
+ return ["claim", "complete", "note", "fail", "cancel"].includes(command ?? "");
1704
1784
  return false;
1705
1785
  }
1706
1786
  function projectRootFromResult(result) {
@@ -64,6 +64,17 @@ export function loadProjectFlowContract(projectRoot) {
64
64
  if (!contractPath) {
65
65
  return defaultFlowContract;
66
66
  }
67
+ return loadFlowContractFile(contractPath);
68
+ }
69
+ /**
70
+ * Canonical-only flows execute against the pinned canonical contract before
71
+ * the target project flow pack is synchronized. Keep this path explicit so a
72
+ * stale project-local contract cannot become the upgrade RUN authority.
73
+ */
74
+ export function loadCanonicalFlowContract(flowRoot) {
75
+ return loadFlowContractFile(path.join(flowRoot, "flow-contract.json"));
76
+ }
77
+ function loadFlowContractFile(contractPath) {
67
78
  try {
68
79
  const value = readFlowContractObject(contractPath);
69
80
  validateFlowContractStructure(value);
@@ -1,10 +1,16 @@
1
1
  import { getDatabase } from "../storage/database.js";
2
2
  import { resolveDdFlowHome } from "../storage/paths.js";
3
- export function createContext(env) {
3
+ export function createRouterContext(env) {
4
+ return {
5
+ ddFlowHome: resolveDdFlowHome(env),
6
+ now: () => new Date().toISOString()
7
+ };
8
+ }
9
+ export function createContext(env, mode = "initialize") {
4
10
  const ddFlowHome = resolveDdFlowHome(env);
5
11
  return {
6
12
  ddFlowHome,
7
- db: getDatabase(ddFlowHome),
13
+ db: getDatabase(ddFlowHome, mode),
8
14
  env,
9
15
  now: () => new Date().toISOString()
10
16
  };
@@ -124,10 +124,15 @@
124
124
  "run": {
125
125
  "type": "object",
126
126
  "additionalProperties": true,
127
- "required": ["run_id", "run_state"],
127
+ "required": ["run_id"],
128
+ "anyOf": [
129
+ {"required": ["run_state"]},
130
+ {"required": ["run_index"]}
131
+ ],
128
132
  "properties": {
129
133
  "run_id": { "type": "string", "pattern": "^RUN-[0-9]{3}-[a-z0-9]+(?:-[a-z0-9]+)*$" },
130
- "run_state": { "type": "string", "minLength": 1 }
134
+ "run_state": { "type": "string", "minLength": 1 },
135
+ "run_index": { "type": "string", "minLength": 1 }
131
136
  }
132
137
  },
133
138
  "stage": {
@@ -44,6 +44,28 @@
44
44
  "items": { "type": "string", "minLength": 1 }
45
45
  }
46
46
  },
47
+ "schema_registry": {
48
+ "type": "object",
49
+ "additionalProperties": false,
50
+ "required": ["schema_id", "entries"],
51
+ "properties": {
52
+ "schema_id": { "const": "dd-flow/engine-schema-registry@1" },
53
+ "entries": {
54
+ "type": "array",
55
+ "items": {
56
+ "type": "object",
57
+ "additionalProperties": false,
58
+ "required": ["name", "id", "path", "checksum"],
59
+ "properties": {
60
+ "name": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
61
+ "id": { "type": "string", "minLength": 1 },
62
+ "path": { "type": "string", "pattern": "^dist/schemas/[a-z0-9][a-z0-9-]*\\.schema\\.json$" },
63
+ "checksum": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ },
47
69
  "integrity": {
48
70
  "type": "object",
49
71
  "additionalProperties": false,
@@ -123,6 +123,7 @@
123
123
  "expected": {"type": "array", "items": {"type": "string"}},
124
124
  "observed": {"type": "array", "items": {"type": "string"}},
125
125
  "missing": {"type": "array", "items": {"type": "string"}},
126
+ "diagnostics": {"type": "array", "items": {"type": "string"}},
126
127
  "reason": {"type": "string"}
127
128
  }
128
129
  },
@@ -70,12 +70,14 @@
70
70
  },
71
71
  "backup": {
72
72
  "type": "object",
73
- "required": ["status", "path", "created_at", "rollback_route"],
73
+ "required": ["status", "path", "created_at", "rollback_route", "size_bytes", "sha256"],
74
74
  "additionalProperties": true,
75
75
  "properties": {
76
76
  "status": { "enum": ["present", "planned", "missing"] },
77
77
  "path": { "type": ["string", "null"] },
78
78
  "created_at": { "type": ["string", "null"] },
79
+ "size_bytes": { "type": ["integer", "null"], "minimum": 1 },
80
+ "sha256": { "type": ["string", "null"], "pattern": "^[a-f0-9]{64}$" },
79
81
  "rollback_route": { "type": "string", "minLength": 1 }
80
82
  }
81
83
  },
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "dd-flow/merge-stage-report@2/engine-0.4.2",
4
+ "title": "Merge stage report emitted by dd-flow engine 0.4.2",
5
+ "type": "object",
6
+ "additionalProperties": true,
7
+ "required": ["schema_id", "run", "stage", "project", "subject", "overall", "breadcrumbs", "implemented_goals", "acceptance_scenarios", "changed_files", "checks", "review", "review_gates", "defs"],
8
+ "properties": {
9
+ "schema_id": { "const": "dd-flow/merge-stage-report@2" },
10
+ "run": { "type": "object", "required": ["run_id", "run_index"], "properties": { "run_id": { "type": "string", "pattern": "^RUN-[0-9]{3}-[a-z0-9]+(?:-[a-z0-9]+)*$" }, "run_index": { "type": "string", "minLength": 1 } } },
11
+ "stage": { "type": "object", "required": ["name", "dir", "status"] },
12
+ "project": { "type": "object", "required": ["id", "title"] },
13
+ "subject": { "type": "object", "required": ["id", "title"] },
14
+ "overall": { "type": "object", "required": ["verdict", "summary", "next_action"], "properties": { "verdict": { "enum": ["accepted", "blocked", "failed"] }, "summary": { "type": "string", "minLength": 1 }, "next_action": { "type": "string", "minLength": 1 } } },
15
+ "breadcrumbs": { "type": "array" },
16
+ "implemented_goals": { "type": "array" },
17
+ "acceptance_scenarios": { "type": "array" },
18
+ "changed_files": { "type": "array" },
19
+ "checks": { "type": "array" },
20
+ "review": { "type": "object" },
21
+ "review_gates": { "type": "array" },
22
+ "defs": { "type": "array" }
23
+ }
24
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "dd-flow/run-engine-binding@1",
4
+ "title": "Immutable RUN engine binding",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schema_id", "run_id", "project_root", "bound_at", "source", "reason", "engine", "probe"],
8
+ "properties": {
9
+ "schema_id": {"const": "dd-flow/run-engine-binding@1"},
10
+ "run_id": {"type": "string", "pattern": "^RUN-[0-9]+-[a-z0-9][a-z0-9-]*$"},
11
+ "project_root": {"type": "string", "minLength": 1},
12
+ "bound_at": {"type": "string", "format": "date-time"},
13
+ "source": {"enum": ["run_creation", "legacy_recovery"]},
14
+ "reason": {"type": "string", "minLength": 1},
15
+ "engine": {
16
+ "type": "object",
17
+ "additionalProperties": false,
18
+ "required": ["package_name", "package_version", "engine_version", "integrity_checksum", "snapshot_root"],
19
+ "properties": {
20
+ "package_name": {"type": "string", "minLength": 1},
21
+ "package_version": {"type": "string", "minLength": 1},
22
+ "engine_version": {"type": "string", "minLength": 1},
23
+ "integrity_checksum": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
24
+ "snapshot_root": {"type": "string", "minLength": 1}
25
+ }
26
+ },
27
+ "probe": {
28
+ "type": "object",
29
+ "additionalProperties": false,
30
+ "required": ["status", "command"],
31
+ "properties": {
32
+ "status": {"enum": ["not_required", "passed"]},
33
+ "command": {"type": ["string", "null"]}
34
+ }
35
+ }
36
+ }
37
+ }
@@ -23,13 +23,13 @@
23
23
  "stage": {"type": "string", "minLength": 1},
24
24
  "generated_at": {"type": "string", "format": "date-time"},
25
25
  "prompt_path": {"type": "string", "minLength": 1},
26
- "sections": {"type": "array", "minItems": 8, "maxItems": 8, "items": {"enum": ["stage_identity", "authoritative_runtime_facts", "preflight", "task_intake", "applicable_instructions", "required_context", "work_contract", "completion_contract"]}},
26
+ "sections": {"const": ["stage_identity", "authoritative_runtime_facts", "preflight", "task_intake", "applicable_instructions", "required_context", "work_contract", "completion_contract"]},
27
27
  "aliases": {"type": "object", "required": ["project", "workspace", "run", "stage", "protocol", "intake"], "additionalProperties": {"type": "string"}},
28
28
  "write_boundary": {"type": "object", "required": ["current", "archive", "archive_writable"], "properties": {"current": {"const": "@stage"}, "archive": {"pattern": "try-[0-9]{3}"}, "archive_writable": {"const": false}}, "additionalProperties": false},
29
29
  "source_fragments": {"type": "array", "items": {"type": "string", "minLength": 1}},
30
- "authoritative_facts": {"type": "object", "additionalProperties": true},
31
- "preflight": {"type": "object", "additionalProperties": true},
32
- "required_context": {"type": "array", "items": {"type": "string", "minLength": 1}},
30
+ "authoritative_facts": {"type": "object", "required": ["git", "runtime"], "additionalProperties": true},
31
+ "preflight": {"type": "object", "required": ["compatibility", "permissions", "session_binding"], "additionalProperties": true},
32
+ "required_context": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["path", "reason", "stop_condition"], "properties": {"path": {"type": "string", "minLength": 1}, "reason": {"type": "string", "minLength": 1}, "stop_condition": {"type": "string", "minLength": 1}}}},
33
33
  "worker_prompt_markdown": {"type": "string", "minLength": 1}
34
34
  }
35
35
  }
@@ -36,7 +36,21 @@ export function getCanonStatus(context, input = {}) {
36
36
  };
37
37
  }
38
38
  export function resolveCanonRoot(context, input = {}) {
39
- const candidates = canonCandidates(context, input);
39
+ return resolveCanonCandidates(canonCandidates(context, input));
40
+ }
41
+ /** Read only: resolve an explicit/environment/nearby canon without opening DD_FLOW_HOME. */
42
+ export function resolveCanonRootBeforeContext(env, input = {}) {
43
+ const candidates = [];
44
+ if (input.explicitRoot)
45
+ candidates.push(validateCanonCandidate("explicit", input.explicitRoot));
46
+ else if (env.DD_MEMORYBANK)
47
+ candidates.push(validateCanonCandidate("env", env.DD_MEMORYBANK));
48
+ else
49
+ for (const known of knownCanonRoots())
50
+ candidates.push(validateCanonCandidate("known", known));
51
+ return resolveCanonCandidates(uniqueBy(candidates, (candidate) => `${candidate.source}:${candidate.root}`));
52
+ }
53
+ function resolveCanonCandidates(candidates) {
40
54
  const validCandidates = candidates.filter((candidate) => candidate.valid && candidate.realpath);
41
55
  const uniqueValid = uniqueBy(validCandidates, (candidate) => candidate.realpath ?? candidate.root);
42
56
  const blockers = candidates.flatMap((candidate) => candidate.blockers);