@deksden-com/dd-flow-cli 0.5.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 (40) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +10 -3
  3. package/dist/build-info.json +5 -5
  4. package/dist/cli/help.js +24 -11
  5. package/dist/cli/run-cli.js +117 -28
  6. package/dist/domain/flow-contract.js +15 -4
  7. package/dist/domain/session-coverage.js +88 -0
  8. package/dist/runtime/context.js +8 -2
  9. package/dist/schemas/code-stage-report.schema.json +7 -2
  10. package/dist/schemas/engine-manifest.schema.json +22 -0
  11. package/dist/schemas/flow-contract.schema.json +15 -13
  12. package/dist/schemas/flow-run.schema.json +1 -0
  13. package/dist/schemas/mb-upgrade-migration-report.schema.json +3 -1
  14. package/dist/schemas/merge-stage-report-legacy-0.4.2.schema.json +24 -0
  15. package/dist/schemas/run-engine-binding.schema.json +37 -0
  16. package/dist/schemas/stage-prompt.schema.json +9 -5
  17. package/dist/schemas/stage-start-response.schema.json +6 -5
  18. package/dist/services/canon.js +15 -1
  19. package/dist/services/cleanup.js +77 -0
  20. package/dist/services/cli-operation-classifier.js +52 -8
  21. package/dist/services/compatibility-preflight.js +1 -1
  22. package/dist/services/dashboard.js +2 -2
  23. package/dist/services/engines.js +408 -30
  24. package/dist/services/hooks.js +28 -15
  25. package/dist/services/lanes.js +0 -4
  26. package/dist/services/merge-queue.js +48 -0
  27. package/dist/services/merge-worker.js +3 -4
  28. package/dist/services/migrations.js +307 -44
  29. package/dist/services/plan-runtime.js +4 -4
  30. package/dist/services/plans.js +5 -3
  31. package/dist/services/protocols.js +23 -2
  32. package/dist/services/run-engine-bindings.js +157 -0
  33. package/dist/services/run-projection.js +18 -4
  34. package/dist/services/runs.js +54 -7
  35. package/dist/services/schema-validation.js +96 -0
  36. package/dist/services/sessions.js +33 -73
  37. package/dist/services/stage-lifecycle.js +298 -45
  38. package/dist/services/status.js +8 -3
  39. package/dist/storage/database.js +32 -11
  40. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
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
+
9
+ ## 0.6.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 47cd270: Add SPC-006 bootstrap stage start/context packets and sealed stage finish, while repairing Memory Bank upgrade stage routing, Codex hook event identity, PATH-stable per-home hook installation, canonical Codex session registration, session coverage projections, terminal orchestrator closure, and safe reconciliation of runs whose legacy authority and workspace are gone.
14
+
3
15
  ## 0.5.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -75,10 +75,10 @@ dd-flow protocol register HANDSHAKE-001 --project-root "$PWD" --json
75
75
  dd-flow protocol status PRT-HANDSHAKE-001 --project-root "$PWD" --json
76
76
  ```
77
77
 
78
- Attach and update a plan:
78
+ Inspect the canonical plan and update its SQLite progress projection:
79
79
 
80
80
  ```bash
81
- dd-flow plan set PRT-HANDSHAKE-001 --project-root "$PWD" --file plan.json --json
81
+ dd-flow plan status PRT-HANDSHAKE-001 --project-root "$PWD" --json
82
82
  dd-flow plan item start PRT-HANDSHAKE-001 P1 --project-root "$PWD" --json
83
83
  dd-flow plan item done PRT-HANDSHAKE-001 P1 --project-root "$PWD" --summary "Implemented" --evidence "pnpm test" --json
84
84
  ```
@@ -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.5.0",
4
- "cli_commit": "64b75acbb2e62c9e80eb29c501f5f3481f86cf20",
5
- "built_at": "2026-08-11T10:20:58.455Z",
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.0.0",
8
- "commit": "1e8c39078ed7a34adde93315a9cdcf2bbfc15374",
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",
@@ -143,7 +147,7 @@ Usage:
143
147
  dd-flow id next --type protocol|run --project-root <root> --slug <slug> --json
144
148
  dd-flow run status <RUN-ID|RUN-short-id> --project-root <root> --json
145
149
  dd-flow run list --project-root <root> --json
146
- dd-flow run timeline <RUN-ID|RUN-short-id> --project-root <root> [--hide stages|events|sessions|usage|artifacts] --json
150
+ dd-flow run timeline <RUN-ID|RUN-short-id> --project-root <root> --json
147
151
  dd-flow run usage <RUN-ID|RUN-short-id> --project-root <root> [--group-by session|role|stage|aspect|plan-item] --json
148
152
  dd-flow run flags status <RUN-ID|RUN-short-id> --project-root <root> --json
149
153
  dd-flow run flags revise <RUN-ID|RUN-short-id> --project-root <root> --expected-revision <n> --idempotency-key <key> [--preset <name>] [--flag key=value] [--flag key=value] [--allow-downgrade --reason <text>] --json
@@ -165,9 +169,10 @@ Examples:
165
169
 
166
170
  Usage:
167
171
  dd-flow stage start <RUN-ID|RUN-short-id> --project-root <root> --stage <name> [--dir <NN-stage-slug>] --json
168
- dd-flow stage finish <RUN-ID|RUN-short-id> --project-root <root> --stage <name> [--status done|blocked|failed] [--semantic-file <RUN-local-json>] --json
172
+ dd-flow stage start --bootstrap --project-root <root> --stage specify --subject <safe-subject> [--intake-file <path>] --json
173
+ dd-flow stage finish <RUN-ID|RUN-short-id> --project-root <root> --stage <name> [--outcome done|blocked|failed] [--semantic-file <RUN-local-json>] --json
169
174
 
170
- stage start attaches the stage, archives an earlier attempt when needed and writes one RUN-local stage-prompt.md. stage finish derives a bounded report and always generates validated JSON, Markdown, template-backed HTML and a durable protocol-summary projection. Raw intake and telemetry remain RUN-local.`
175
+ stage start is the worker's first practical flow command. It performs deterministic setup and returns a ready-to-use, stage-specific work packet with trusted runtime facts, compatibility and permission preflight, aliases, write boundary and exact finish command. The same packet is written to RUN-local stage-prompt.md and stage-prompt.json. --bootstrap creates the initial protocol/RUN envelope and copies the supplied intake into the RUN home before starting specify. stage finish defaults to @stage/stage-input.json, accepts semantic --outcome and always generates validated JSON, Markdown, template-backed HTML and a durable protocol-summary projection. Raw intake and telemetry remain RUN-local.`
171
176
  ],
172
177
  [
173
178
  "id",
@@ -233,11 +238,12 @@ New registrations use full typed ids such as PRJ-001-dd-flow-playground and shor
233
238
  `dd-flow migration - plan and verify mb-upgrade runtime/home data migrations
234
239
 
235
240
  Usage:
236
- 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
237
- 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
238
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
239
245
 
240
- 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.
241
247
 
242
248
  Related commands:
243
249
  dd-flow status --project-root <root> --json
@@ -353,13 +359,14 @@ start is idempotent: if a worker, claimed job, or active merge lock already exis
353
359
  Usage:
354
360
  dd-flow merge-queue status --project-root <root> --json
355
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
356
363
  dd-flow merge-queue wait-next --project-root <root> --worker-id <id> [--path <workspace>] --timeout <seconds> --poll-interval <seconds> [--acquire-lock true] --json
357
364
  dd-flow merge-queue complete <protocol-id> [--project-root <root>] --worker-id <id> [--path <workspace>] --summary <text> --json
358
365
  dd-flow merge-queue note <protocol-id> [--project-root <root>] --worker-id <id> --summary <text> --json
359
366
  dd-flow merge-queue fail <protocol-id> [--project-root <root>] --worker-id <id> [--path <workspace>] --reason <text> --requeue true|false --json
360
367
  dd-flow merge-queue cancel <protocol-id> [--project-root <root>] --reason <text> [--worker-id <id>] [--path <workspace>] [--force] --json
361
368
 
362
- 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.
363
370
 
364
371
  For branch-level integration prefer:
365
372
  dd-flow merge bundle status --project-root "$PWD" --path "$PWD" --json
@@ -526,6 +533,12 @@ Usage:
526
533
  dd-flow codex home print-env --project-root <root> [--profile <name>] --json
527
534
  dd-flow codex home remove --project-root <root> [--profile <name>] --mode keep-shared|remove-owned --json
528
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
+
529
542
  Managed homes isolate dd-flow hooks while sharing selected Codex state from the source home.`
530
543
  ],
531
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]);
@@ -541,23 +577,28 @@ async function dispatch(args, context, io, scopeProjectRoot = null) {
541
577
  }
542
578
  if (family === "stage" && command === "start") {
543
579
  const dir = optionalOption(parsed, "dir");
580
+ const bootstrap = hasOption(parsed, "bootstrap");
581
+ const subject = optionalOption(parsed, "subject");
582
+ const intakeFile = optionalOption(parsed, "intake-file");
583
+ const sessionId = optionalOption(parsed, "session-id");
544
584
  return startStage(context, {
545
585
  projectRoot: requiredOption(parsed, "project-root"),
546
- runId: requiredPosition(parsed, 0, "run-id"),
586
+ ...(bootstrap ? { bootstrap: { subject: subject ?? requiredOption(parsed, "subject"), ...(intakeFile ? { intakeFile } : {}) } } : { runId: requiredPosition(parsed, 0, "run-id") }),
547
587
  stage: requiredOption(parsed, "stage"),
588
+ ...(sessionId ? { sessionId } : {}),
548
589
  ...(dir ? { dir } : {})
549
590
  });
550
591
  }
551
592
  if (family === "stage" && command === "finish") {
552
593
  const dir = optionalOption(parsed, "dir");
553
- const status = optionalOption(parsed, "status");
594
+ const outcome = optionalOption(parsed, "outcome") ?? optionalOption(parsed, "status");
554
595
  const semanticFile = optionalOption(parsed, "semantic-file") ?? optionalOption(parsed, "data");
555
596
  return finishStage(context, {
556
597
  projectRoot: requiredOption(parsed, "project-root"),
557
598
  runId: requiredPosition(parsed, 0, "run-id"),
558
599
  stage: requiredOption(parsed, "stage"),
559
600
  ...(dir ? { dir } : {}),
560
- ...(status ? { status } : {}),
601
+ ...(outcome ? { outcome } : {}),
561
602
  ...(semanticFile ? { semanticFile } : {})
562
603
  });
563
604
  }
@@ -592,13 +633,13 @@ async function dispatch(args, context, io, scopeProjectRoot = null) {
592
633
  return dispatchDashboard(context, command, parsed);
593
634
  }
594
635
  if (family === "schema") {
595
- return dispatchSchema(command, parsed);
636
+ return dispatchSchema(context, command, parsed);
596
637
  }
597
638
  if (family === "memory") {
598
639
  return dispatchMemory(command, parsed);
599
640
  }
600
641
  if (family === "migration") {
601
- return dispatchMigration(context, command, parsed);
642
+ return dispatchMigration(context, command, parsed, classification);
602
643
  }
603
644
  if (family === "codex" && command === "hooks") {
604
645
  return dispatchCodexHooks(context, parsed);
@@ -614,7 +655,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null) {
614
655
  }
615
656
  throw new AppError("usage", `Unknown command: ${args.join(" ") || "<empty>"}`, 2);
616
657
  }
617
- function dispatchEngine(context, command, parsed) {
658
+ function dispatchEngine(context, command, parsed, classification) {
618
659
  if (command === "install") {
619
660
  return installCurrentEngine(context, { force: hasOption(parsed, "force") });
620
661
  }
@@ -625,14 +666,32 @@ function dispatchEngine(context, command, parsed) {
625
666
  return engineInfo(context, { packageName: optionalOption(parsed, "package"), version: optionalOption(parsed, "version") });
626
667
  }
627
668
  if (command === "resolve") {
628
- 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
+ });
629
674
  }
630
675
  if (command === "doctor") {
631
- 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
+ });
632
691
  }
633
692
  throw new AppError("usage", `Unknown engine command: ${command ?? "<empty>"}`, 2);
634
693
  }
635
- function dispatchMigration(context, command, parsed) {
694
+ function dispatchMigration(context, command, parsed, classification) {
636
695
  if (command === "impact")
637
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") } : {}) });
638
697
  if (command === "plan" || command === "report") {
@@ -643,12 +702,26 @@ function dispatchMigration(context, command, parsed) {
643
702
  runId: optionalOption(parsed, "run"),
644
703
  backupPath: optionalOption(parsed, "backup-path"),
645
704
  backupCreatedAt: optionalOption(parsed, "backup-created-at"),
646
- allowActive: parseOptionalBoolean(optionalOption(parsed, "allow-active"), "allow-active") ?? false
705
+ allowActive: false,
706
+ classification
647
707
  });
648
708
  }
649
709
  if (command === "verify") {
650
710
  return verifyMigrationReport(context, { file: requiredOption(parsed, "file") });
651
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
+ }
652
725
  throw new AppError("usage", `Unknown migration command: ${command ?? "<empty>"}`, 2);
653
726
  }
654
727
  function dispatchCanon(context, command, parsed) {
@@ -677,6 +750,14 @@ function dispatchMergeQueue(context, command, parsed, scopeProjectRoot) {
677
750
  workspacePath: workspacePathOption(parsed)
678
751
  });
679
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
+ }
680
761
  if (command === "wait-next") {
681
762
  return waitNextMergeJob(context, {
682
763
  projectRoot: requiredOptionOrScope(parsed, scopeProjectRoot, "project-root"),
@@ -882,6 +963,9 @@ function dispatchSession(context, command, parsed) {
882
963
  }
883
964
  function dispatchRun(context, command, parsed) {
884
965
  if (command === "start") {
966
+ if (optionalOption(parsed, "flow-kind") === "mb-upgrade") {
967
+ assertUpgradeRunStorageReady(context, requiredOption(parsed, "project-root"));
968
+ }
885
969
  const protocolOverrides = readOptionalJsonFile(optionalOption(parsed, "protocol-override-file"));
886
970
  return startFlowRun(context, {
887
971
  projectRoot: requiredOption(parsed, "project-root"),
@@ -1139,7 +1223,7 @@ function withDashboardGuidance(action, target, result, options = {}) {
1139
1223
  related_commands: relatedDashboardCommands(action, target.kind)
1140
1224
  };
1141
1225
  }
1142
- function dispatchSchema(command, parsed) {
1226
+ function dispatchSchema(context, command, parsed) {
1143
1227
  if (command === "validate") {
1144
1228
  const schemaDir = optionalOption(parsed, "schema-dir");
1145
1229
  const projectRoot = optionalOption(parsed, "project-root");
@@ -1147,7 +1231,8 @@ function dispatchSchema(command, parsed) {
1147
1231
  schemaName: requiredOption(parsed, "schema"),
1148
1232
  file: requiredOption(parsed, "file"),
1149
1233
  ...(schemaDir ? { schemaDir } : {}),
1150
- ...(projectRoot ? { projectRoot } : {})
1234
+ ...(projectRoot ? { projectRoot } : {}),
1235
+ ddFlowHome: context.ddFlowHome
1151
1236
  });
1152
1237
  }
1153
1238
  throw new AppError("usage", `Unknown schema command: ${command ?? "<empty>"}`, 2);
@@ -1327,11 +1412,15 @@ function dispatchCodexHome(context, parsed) {
1327
1412
  function dispatchCodexHook(context, parsed, stdin) {
1328
1413
  const action = requiredPosition(parsed, 0, "codex hook action");
1329
1414
  if (action === "handle") {
1330
- return handleCodexHook(context, {
1415
+ const result = handleCodexHook(context, {
1331
1416
  event: requiredOption(parsed, "event"),
1332
1417
  projectRoot: optionalOption(parsed, "project-root"),
1333
1418
  stdin
1334
1419
  });
1420
+ if (result && typeof result === "object" && !Array.isArray(result) && "hookSpecificOutput" in result) {
1421
+ return { hookSpecificOutput: result.hookSpecificOutput };
1422
+ }
1423
+ return {};
1335
1424
  }
1336
1425
  throw new AppError("usage", `Unknown codex hook action: ${action}`, 2);
1337
1426
  }
@@ -1670,7 +1759,7 @@ function projectRootForLaneMutation(command, parsed) {
1670
1759
  return ["acquire", "heartbeat", "release", "wait-acquire"].includes(action) ? requiredOption(parsed, "project-root") : undefined;
1671
1760
  }
1672
1761
  function projectRootForMergeQueueMutation(context, command, parsed) {
1673
- if (["next", "wait-next"].includes(command ?? "")) {
1762
+ if (["next", "wait-next", "claim"].includes(command ?? "")) {
1674
1763
  return requiredOption(parsed, "project-root");
1675
1764
  }
1676
1765
  if (["complete", "fail", "cancel"].includes(command ?? "")) {
@@ -1691,7 +1780,7 @@ function isScopedProtocolMutation(family, command) {
1691
1780
  if (family === "worktree")
1692
1781
  return ["create", "bootstrap", "close"].includes(command ?? "");
1693
1782
  if (family === "merge-queue")
1694
- return ["complete", "note", "fail", "cancel"].includes(command ?? "");
1783
+ return ["claim", "complete", "note", "fail", "cancel"].includes(command ?? "");
1695
1784
  return false;
1696
1785
  }
1697
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);
@@ -332,7 +343,6 @@ function flagStrength(key, value, definition) {
332
343
  "subagents.route": ["self_check", "grouped_subagent", "focused_subagent", "mixed"],
333
344
  "subagents.grouping": ["off", "allowlisted"],
334
345
  "subagents.pool_fallback": ["one", "runtime"],
335
- "observability.detail": ["compact", "normal", "full"],
336
346
  "knowledge.extract": ["skip", "conditional", "required"],
337
347
  "knowledge.promote": ["skip", "conditional", "required"],
338
348
  "workspace.bootstrap.mode": ["not_required", "revalidate", "required"],
@@ -362,10 +372,10 @@ function numberValue(value) {
362
372
  export function normalizeFlowContract(value) {
363
373
  const version = requireNumber(value.version, "/version");
364
374
  const id = requireString(value.id, "/id");
365
- if (version === 4 || version === 5) {
375
+ if (version === 4 || version === 5 || version === 6) {
366
376
  return normalizeCanonicalRuntimeContract(value, id, version);
367
377
  }
368
- if (version > 5)
378
+ if (version > 6)
369
379
  fail("/version", `unsupported flow-contract version: ${version}`);
370
380
  if (version === 3 && !/^dd-flow-canonical-[0-9]{4}-[0-9]{2}$/.test(id)) {
371
381
  fail("/id", "must identify a canonical version 3 flow contract");
@@ -462,7 +472,8 @@ function normalizeCanonicalRuntimeContract(value, id, version) {
462
472
  "flow_run_snapshot.v1",
463
473
  "session_coverage_units.v1",
464
474
  "privacy_safe_observability.v1",
465
- ...(version === 5 ? ["canonical_protocol_plan.v1"] : [])
475
+ ...(version >= 5 ? ["canonical_protocol_plan.v1"] : []),
476
+ ...(version >= 6 ? ["stage_bootstrap_context_packet.v1"] : [])
466
477
  ],
467
478
  stages,
468
479
  transitions,
@@ -0,0 +1,88 @@
1
+ export function reconcileSessionCoverageRows(sessions) {
2
+ const sessionCounts = countValues(sessions.map((session) => session.session_id));
3
+ const duplicateSessionIds = duplicates(sessionCounts);
4
+ const invalidSessionIds = [];
5
+ const parsed = sessions.map((session) => {
6
+ try {
7
+ return { session, units: normalizeCoverageUnits(JSON.parse(session.coverage_units_json || "[]")) };
8
+ }
9
+ catch {
10
+ invalidSessionIds.push(session.session_id);
11
+ return { session, units: [] };
12
+ }
13
+ });
14
+ const orchestrators = parsed.filter(({ session }) => session.session_kind === "orchestrator");
15
+ const workers = parsed.filter(({ session }) => session.session_kind !== "orchestrator");
16
+ const expectedCounts = countValues(orchestrators.flatMap(({ units }) => units.map((unit) => unit.unit_id)));
17
+ const expectedUnitIds = [...expectedCounts.keys()].sort();
18
+ const duplicateExpectedUnitIds = duplicates(expectedCounts);
19
+ const observedOwners = new Map();
20
+ for (const { session, units } of workers) {
21
+ for (const unit of units) {
22
+ const owners = observedOwners.get(unit.unit_id) ?? [];
23
+ owners.push(session.session_id);
24
+ observedOwners.set(unit.unit_id, owners);
25
+ }
26
+ }
27
+ const observedUnitIds = [...observedOwners.keys()].sort();
28
+ const missingUnitIds = expectedUnitIds.filter((unitId) => !observedOwners.has(unitId));
29
+ const duplicateUnitIds = [...observedOwners.entries()]
30
+ .filter(([, owners]) => new Set(owners).size > 1)
31
+ .map(([id]) => id)
32
+ .sort();
33
+ const diagnostics = [];
34
+ if (orchestrators.length === 0)
35
+ diagnostics.push("orchestrator_session_missing");
36
+ if (orchestrators.length > 1)
37
+ diagnostics.push("multiple_orchestrator_sessions");
38
+ if (workers.length > 0 && expectedUnitIds.length === 0)
39
+ diagnostics.push("expected_worker_units_missing");
40
+ if (missingUnitIds.length > 0)
41
+ diagnostics.push("expected_worker_units_unobserved");
42
+ if (duplicateUnitIds.length > 0 || duplicateExpectedUnitIds.length > 0)
43
+ diagnostics.push("duplicate_coverage_binding");
44
+ if (duplicateSessionIds.length > 0)
45
+ diagnostics.push("duplicate_session_binding");
46
+ if (invalidSessionIds.length > 0)
47
+ diagnostics.push("invalid_coverage_units");
48
+ const unavailable = orchestrators.length === 0 || invalidSessionIds.length > 0;
49
+ const partial = orchestrators.length !== 1
50
+ || (workers.length > 0 && expectedUnitIds.length === 0)
51
+ || missingUnitIds.length > 0
52
+ || duplicateUnitIds.length > 0
53
+ || duplicateExpectedUnitIds.length > 0
54
+ || duplicateSessionIds.length > 0;
55
+ return {
56
+ status: unavailable ? "unavailable" : partial ? "partial" : "complete",
57
+ expected_unit_ids: expectedUnitIds,
58
+ observed_unit_ids: observedUnitIds,
59
+ missing_unit_ids: missingUnitIds,
60
+ duplicate_unit_ids: duplicateUnitIds,
61
+ duplicate_expected_unit_ids: duplicateExpectedUnitIds,
62
+ duplicate_session_ids: duplicateSessionIds,
63
+ orchestrator_session_ids: orchestrators.map(({ session }) => session.session_id).sort(),
64
+ invalid_session_ids: [...new Set(invalidSessionIds)].sort(),
65
+ diagnostics
66
+ };
67
+ }
68
+ function normalizeCoverageUnits(value) {
69
+ if (!Array.isArray(value))
70
+ throw new Error("coverage_units must be an array");
71
+ return value.map((item) => {
72
+ if (!item || typeof item !== "object" || Array.isArray(item))
73
+ throw new Error("coverage unit must be an object");
74
+ const unitId = item.unit_id;
75
+ if (typeof unitId !== "string" || unitId.length === 0)
76
+ throw new Error("coverage unit requires unit_id");
77
+ return { unit_id: unitId };
78
+ });
79
+ }
80
+ function countValues(values) {
81
+ const counts = new Map();
82
+ for (const value of values)
83
+ counts.set(value, (counts.get(value) ?? 0) + 1);
84
+ return counts;
85
+ }
86
+ function duplicates(counts) {
87
+ return [...counts.entries()].filter(([, count]) => count > 1).map(([id]) => id).sort();
88
+ }