@deksden-com/dd-flow-cli 0.6.0 → 0.8.0-beta.135

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 (78) hide show
  1. package/CHANGELOG.md +636 -0
  2. package/README.md +13 -1
  3. package/dist/build-info.json +10 -10
  4. package/dist/cli/help.js +108 -18
  5. package/dist/cli/run-cli.js +626 -49
  6. package/dist/domain/flow-contract.js +11 -0
  7. package/dist/domain/stage-catalog.js +22 -0
  8. package/dist/runtime/context.js +8 -2
  9. package/dist/schemas/code-review-decision.schema.json +26 -0
  10. package/dist/schemas/code-review-result.schema.json +14 -0
  11. package/dist/schemas/code-stage-report.schema.json +7 -2
  12. package/dist/schemas/code-verification.schema.json +14 -0
  13. package/dist/schemas/code-work-batch.schema.json +24 -0
  14. package/dist/schemas/code-work-result.schema.json +16 -0
  15. package/dist/schemas/engine-manifest.schema.json +22 -0
  16. package/dist/schemas/flow-contract.schema.json +6 -3
  17. package/dist/schemas/flow-run.schema.json +16 -122
  18. package/dist/schemas/mb-upgrade-migration-report.schema.json +3 -1
  19. package/dist/schemas/merge-stage-report-legacy-0.4.2.schema.json +24 -0
  20. package/dist/schemas/plan-aspect-map.schema.json +22 -0
  21. package/dist/schemas/plan-review-decision.schema.json +14 -0
  22. package/dist/schemas/plan-review-result.schema.json +42 -0
  23. package/dist/schemas/protocol-plan.schema.json +15 -182
  24. package/dist/schemas/run-engine-binding.schema.json +37 -0
  25. package/dist/schemas/stage-finish-input.schema.json +16 -2
  26. package/dist/schemas/stage-prompt.schema.json +4 -4
  27. package/dist/schemas/stage-report.schema.json +8 -7
  28. package/dist/schemas/vnext-protocol-plan.schema.json +37 -0
  29. package/dist/schemas/vnext-protocolize-result.schema.json +29 -0
  30. package/dist/schemas/vnext-specify.schema.json +45 -0
  31. package/dist/services/branch-context.js +1 -1
  32. package/dist/services/canon.js +15 -1
  33. package/dist/services/cleanup.js +8 -8
  34. package/dist/services/cli-operation-classifier.js +60 -8
  35. package/dist/services/code-checks.js +244 -0
  36. package/dist/services/compatibility-preflight.js +1 -1
  37. package/dist/services/config.js +7 -1
  38. package/dist/services/dashboard.js +14 -14
  39. package/dist/services/engines.js +408 -30
  40. package/dist/services/eval-snapshots.js +404 -0
  41. package/dist/services/hooks.js +775 -23
  42. package/dist/services/ids.js +16 -6
  43. package/dist/services/lanes.js +1 -5
  44. package/dist/services/merge-queue.js +53 -5
  45. package/dist/services/merge-worker.js +5 -6
  46. package/dist/services/migrations.js +307 -44
  47. package/dist/services/plan-runtime.js +5 -5
  48. package/dist/services/plans.js +5 -3
  49. package/dist/services/projects.js +4 -4
  50. package/dist/services/prompts.js +1 -1
  51. package/dist/services/protocols.js +31 -10
  52. package/dist/services/run-engine-bindings.js +157 -0
  53. package/dist/services/run-projection.js +49 -13
  54. package/dist/services/runs.js +525 -58
  55. package/dist/services/schema-validation.js +116 -2
  56. package/dist/services/sessions.js +51 -12
  57. package/dist/services/stage-blocker.js +57 -0
  58. package/dist/services/stage-context.js +90 -0
  59. package/dist/services/stage-lifecycle.js +288 -77
  60. package/dist/services/stage-pause.js +175 -0
  61. package/dist/services/stage-report-renderer.js +65 -0
  62. package/dist/services/status.js +8 -3
  63. package/dist/services/usage.js +526 -18
  64. package/dist/services/vnext-code-review.js +308 -0
  65. package/dist/services/vnext-code.js +616 -0
  66. package/dist/services/vnext-contracts.js +1 -0
  67. package/dist/services/vnext-execution-profile.js +27 -0
  68. package/dist/services/vnext-fanout.js +79 -0
  69. package/dist/services/vnext-plan-review.js +499 -0
  70. package/dist/services/vnext-plan.js +576 -0
  71. package/dist/services/vnext-protocolize.js +542 -0
  72. package/dist/services/vnext-specify.js +595 -0
  73. package/dist/services/vnext-workspace-policy.js +87 -0
  74. package/dist/services/work-registry.js +499 -0
  75. package/dist/services/worktrees.js +58 -37
  76. package/dist/storage/database.js +292 -42
  77. package/dist/storage/paths.js +47 -1
  78. package/package.json +12 -12
@@ -9,6 +9,10 @@ import { AppError } from "../shared/errors.js";
9
9
  import { ensureDir, engineStoreRoot, engineVersionRoot, resolveProjectRoot } from "../storage/paths.js";
10
10
  import { classifyCliOperation } from "./cli-operation-classifier.js";
11
11
  import { requireProtocol } from "./protocols.js";
12
+ import { resolveCanonRootBeforeContext } from "./canon.js";
13
+ import { appendAudit } from "./audit.js";
14
+ import { findProjectByRoot } from "./projects.js";
15
+ import { allRunEngineBindings, findRunHome, readRunEngineBinding, runEngineBindingSchemaId, writeRunEngineBinding } from "./run-engine-bindings.js";
12
16
  export const engineManifestSchemaId = "dd-flow/engine-manifest@1";
13
17
  const routerNativeFamilies = new Set(["engine", "version", "schema"]);
14
18
  const sourceFile = fileURLToPath(import.meta.url);
@@ -24,7 +28,7 @@ export function isRouterNativeCommand(args) {
24
28
  return true;
25
29
  if (args.includes("--help") || args.includes("-h"))
26
30
  return true;
27
- return routerNativeFamilies.has(first);
31
+ return routerNativeFamilies.has(first) || first === "codex" && args[1] === "hook" && args[2] === "handle";
28
32
  }
29
33
  export function installCurrentEngine(context, input = {}) {
30
34
  const build = getCliBuildInfo();
@@ -63,24 +67,48 @@ export function installCurrentEngine(context, input = {}) {
63
67
  fs.rmSync(tmp, { recursive: true, force: true });
64
68
  ensureDir(tmp);
65
69
  copyPackageSnapshot(packageRoot, tmp);
66
- const manifest = buildManifest(context, build.package_name, build.version, packageRoot, target, tmp);
70
+ const stagedManifest = buildManifest(context, build.package_name, build.version, packageRoot, target, tmp);
71
+ const stagedChecksum = stagedManifest.integrity.checksum;
72
+ const activationTarget = input.force
73
+ && existing
74
+ && stagedChecksum
75
+ && stagedChecksum !== existing.integrity.checksum
76
+ && engineIsBound(context.ddFlowHome, existing)
77
+ ? `${target}--${stagedChecksum}`
78
+ : target;
79
+ const duplicate = stagedChecksum ? findInstalledManifest(context.ddFlowHome, {
80
+ packageName: build.package_name,
81
+ packageVersion: build.version,
82
+ integrityChecksum: stagedChecksum
83
+ }) : null;
84
+ if (duplicate) {
85
+ fs.rmSync(tmp, { recursive: true, force: true });
86
+ return {
87
+ ok: true,
88
+ action: "engine_install",
89
+ changed: false,
90
+ ...(input.forRouting ? {} : { engine: manifestSummary(duplicate) }),
91
+ path: duplicate.snapshot_root
92
+ };
93
+ }
94
+ const manifest = { ...stagedManifest, package_root: activationTarget, snapshot_root: activationTarget };
67
95
  fs.writeFileSync(path.join(tmp, "engine.json"), `${JSON.stringify(manifest, null, 2)}\n`);
68
- const stagedManifest = requireManifest(path.join(tmp, "engine.json"));
69
- const stagedDiagnostics = manifestDiagnostics({ ...stagedManifest, package_root: tmp, snapshot_root: tmp }, { checkEntrypoint: !input.forRouting });
96
+ const checkedManifest = requireManifest(path.join(tmp, "engine.json"));
97
+ const stagedDiagnostics = manifestDiagnostics({ ...checkedManifest, package_root: tmp, snapshot_root: tmp }, { checkEntrypoint: !input.forRouting });
70
98
  if (stagedDiagnostics.length > 0) {
71
99
  throw new AppError("engine_install_failed", "Staged dd-flow engine failed its integrity or dependency health check", 1, {
72
100
  diagnostics: stagedDiagnostics,
73
101
  path: tmp
74
102
  });
75
103
  }
76
- activateEngineAtomically(tmp, target);
77
- const installed = requireManifest(path.join(target, "engine.json"));
104
+ activateEngineAtomically(tmp, activationTarget);
105
+ const installed = requireManifest(path.join(activationTarget, "engine.json"));
78
106
  return {
79
107
  ok: true,
80
108
  action: "engine_install",
81
109
  changed: true,
82
110
  ...(input.forRouting ? {} : { engine: manifestSummary(installed) }),
83
- path: target
111
+ path: activationTarget
84
112
  };
85
113
  }
86
114
  finally {
@@ -100,15 +128,14 @@ export function engineInfo(context, input) {
100
128
  return { ok: true, schema_id: "dd-flow/engine-info@1", engine: manifest };
101
129
  }
102
130
  export function resolveEngine(context, input = {}) {
103
- installCurrentEngine(context);
104
- const selection = selectEngine(context, input);
105
- if (selection.status === "missing") {
106
- return { ok: false, error: missingEngineError(selection), selection, exit_code: 1 };
131
+ const classification = input.classification ?? classifyCliOperation(["engine", "resolve"], input.env ?? context.env);
132
+ const selection = selectEngine(context, input, {}, classification);
133
+ if (selection.status !== "selected") {
134
+ return { ok: false, error: missingEngineError(selection, classification), selection, contexts: compatibilityContexts(context, { ...input, classification }), exit_code: 1 };
107
135
  }
108
- return { ok: true, schema_id: "dd-flow/engine-resolve@1", selection };
136
+ return { ok: true, schema_id: "dd-flow/engine-resolve@1", selection, contexts: compatibilityContexts(context, { ...input, classification }) };
109
137
  }
110
138
  export function doctorEngines(context, input = {}) {
111
- installCurrentEngine(context);
112
139
  const root = engineStoreRoot(context.ddFlowHome);
113
140
  const installed = readInstalledManifests(root);
114
141
  const checks = installed.map((manifest) => ({
@@ -116,24 +143,30 @@ export function doctorEngines(context, input = {}) {
116
143
  status: manifestHealthy(manifest) ? "ok" : "corrupt",
117
144
  diagnostics: manifestDiagnostics(manifest)
118
145
  }));
119
- const selection = selectEngine(context, input);
146
+ const classification = input.classification ?? classifyCliOperation(["engine", "doctor"], input.env ?? context.env);
147
+ const selection = selectEngine(context, input, {}, classification);
120
148
  return {
121
- ok: checks.every((check) => check.status === "ok") && selection.status !== "missing",
149
+ ok: checks.every((check) => check.status === "ok") && selection.status === "selected",
122
150
  schema_id: "dd-flow/engine-doctor@1",
123
151
  engine_store: root,
124
152
  checks,
125
153
  selection,
126
- exit_code: checks.every((check) => check.status === "ok") && selection.status !== "missing" ? 0 : 1
154
+ contexts: compatibilityContexts(context, { ...input, classification }),
155
+ exit_code: checks.every((check) => check.status === "ok") && selection.status === "selected" ? 0 : 1
127
156
  };
128
157
  }
129
158
  export function routeArgsThroughEngine(context, args, io, stdin, env, resolvedProjectRoot) {
130
159
  if (isEngineMode(env) || isRouterNativeCommand(args))
131
160
  return Promise.resolve(null);
132
- installCurrentEngine(context, { forRouting: true });
133
- const selection = selectEngine(context, { projectRoot: resolvedProjectRoot ?? resolveOperationProjectRoot(context, args) ?? undefined }, { allowCurrentInProcess: true });
134
- if (selection.status === "missing") {
135
- const classification = classifyCliOperation(args, env);
136
- if (classification.mode === "read_only_diagnostics" || classification.mode === "mb_upgrade") {
161
+ const classification = classifyCliOperation(args, env);
162
+ const bound = boundRunEngineForArgs(context, args, resolvedProjectRoot ?? resolveExplicitProjectRoot(args) ?? undefined);
163
+ if (bound) {
164
+ const entrypoint = resolveManifestEntrypoint(bound.manifest);
165
+ return spawnEngine(bound.manifest, entrypoint, args, io, stdin, env, getCliBuildInfo().version);
166
+ }
167
+ const selection = selectEngine(context, { projectRoot: resolvedProjectRoot ?? resolveExplicitProjectRoot(args) ?? undefined, env }, { allowCurrentInProcess: true }, classification);
168
+ if (selection.status !== "selected") {
169
+ if (classification.mode === "read_only_diagnostics" || (classification.mode === "mb_upgrade" && isUpgradeDiagnostic(args))) {
137
170
  return Promise.resolve(null);
138
171
  }
139
172
  throw new AppError("missing_engine", "Required dd-flow engine is not installed", 1, missingEngineDetails(selection, classification));
@@ -147,6 +180,179 @@ export function routeArgsThroughEngine(context, args, io, stdin, env, resolvedPr
147
180
  const entrypoint = resolveManifestEntrypoint(selection.selected);
148
181
  return spawnEngine(selection.selected, entrypoint, args, io, stdin, env, current.version);
149
182
  }
183
+ export function bindRunEngine(context, input) {
184
+ const projectRoot = resolveProjectRoot(input.projectRoot);
185
+ const located = findRunHome(context.ddFlowHome, projectRoot, input.runId);
186
+ if (!located)
187
+ throw new AppError("not_found", `RUN is not registered in router storage: ${input.runId}`, 1, { run_id: input.runId, project_root: projectRoot });
188
+ const existing = readRunEngineBinding(located.binding_path, { allowMissing: true });
189
+ const manifest = requireBoundManifest(context.ddFlowHome, {
190
+ packageName: input.packageName ?? getCliBuildInfo().package_name,
191
+ packageVersion: input.engineVersion,
192
+ ...(input.integrityChecksum ? { integrityChecksum: input.integrityChecksum } : {})
193
+ });
194
+ const requested = engineIdentity(manifest);
195
+ if (existing) {
196
+ if (sameEngineIdentity(existing.engine, requested))
197
+ return { ok: true, action: "run_engine_bind", changed: false, binding: existing };
198
+ throw new AppError("run_engine_binding_immutable", "RUN engine binding cannot be changed", 1, {
199
+ run_id: located.run_id,
200
+ current: existing.engine,
201
+ requested
202
+ });
203
+ }
204
+ const probeArgs = ["run", "status", located.run_id, "--project-root", projectRoot, "--json"];
205
+ const probe = spawnSync(process.execPath, [resolveManifestEntrypoint(manifest), ...probeArgs], {
206
+ cwd: manifest.snapshot_root,
207
+ env: { ...process.env, ...context.env, DD_FLOW_HOME: context.ddFlowHome, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: manifest.snapshot_root },
208
+ encoding: "utf8",
209
+ timeout: 30_000
210
+ });
211
+ if (probe.status !== 0) {
212
+ throw new AppError("run_engine_probe_failed", "Selected engine cannot read the RUN authority", 1, {
213
+ run_id: located.run_id,
214
+ engine: manifestSummary(manifest),
215
+ command: `dd-flow ${probeArgs.join(" ")}`,
216
+ exit_code: probe.status,
217
+ stderr: probe.stderr.slice(0, 2000)
218
+ });
219
+ }
220
+ const binding = {
221
+ schema_id: runEngineBindingSchemaId,
222
+ run_id: located.run_id,
223
+ project_root: projectRoot,
224
+ bound_at: context.now(),
225
+ source: "legacy_recovery",
226
+ reason: requiredText(input.reason, "reason"),
227
+ engine: requested,
228
+ probe: { status: "passed", command: `dd-flow ${probeArgs.join(" ")}` }
229
+ };
230
+ const written = writeRunEngineBinding(located.binding_path, binding);
231
+ const project = context.db.writable ? findProjectByRoot(context, projectRoot) : null;
232
+ if (project) {
233
+ appendAudit(context, {
234
+ projectId: project.id,
235
+ eventType: "flow_run.engine_bound",
236
+ payload: { run_id: located.run_id, source: binding.source, engine: binding.engine, probe: binding.probe }
237
+ });
238
+ }
239
+ return { ok: true, action: "run_engine_bind", changed: written.changed, binding: written.binding };
240
+ }
241
+ export function bindCurrentEngineToRun(context, input) {
242
+ const manifest = executionManifest(context);
243
+ const binding = {
244
+ schema_id: runEngineBindingSchemaId,
245
+ run_id: input.runId,
246
+ project_root: resolveProjectRoot(input.projectRoot),
247
+ bound_at: context.now(),
248
+ source: "run_creation",
249
+ reason: "RUN created by the selected execution engine.",
250
+ engine: engineIdentity(manifest),
251
+ probe: { status: "not_required", command: null }
252
+ };
253
+ return writeRunEngineBinding(path.join(input.runHome, "engine-binding.json"), binding).binding;
254
+ }
255
+ function boundRunEngineForArgs(context, args, projectRoot) {
256
+ const runId = runIdForArgs(args);
257
+ if (!runId || !projectRoot)
258
+ return null;
259
+ const located = findRunHome(context.ddFlowHome, projectRoot, runId);
260
+ if (!located)
261
+ return null;
262
+ const binding = readRunEngineBinding(located.binding_path, { allowMissing: true });
263
+ if (!binding)
264
+ return null;
265
+ const manifest = requireBoundManifest(context.ddFlowHome, {
266
+ packageName: binding.engine.package_name,
267
+ packageVersion: binding.engine.package_version,
268
+ integrityChecksum: binding.engine.integrity_checksum,
269
+ snapshotRoot: binding.engine.snapshot_root
270
+ });
271
+ return { binding, manifest };
272
+ }
273
+ function runIdForArgs(args) {
274
+ const [family, command, ...rest] = args;
275
+ const parsed = parseLightArgs(rest);
276
+ if (family === "run" && command !== "start" && command !== "list") {
277
+ if (command === "flags")
278
+ return positional(parsed, 1);
279
+ return positional(parsed, 0);
280
+ }
281
+ if (family === "stage" && command !== "start")
282
+ return positional(parsed, 0);
283
+ if (family === "stage" && command === "start" && !parsed.options.has("bootstrap"))
284
+ return positional(parsed, 0);
285
+ if (family === "prompt" && command === "render")
286
+ return option(parsed, "run");
287
+ if (family === "migration" && ["plan", "report", "verify", "apply"].includes(command ?? ""))
288
+ return option(parsed, "run");
289
+ if (family === "protocol" && command === "sync-from-run")
290
+ return option(parsed, "run");
291
+ return null;
292
+ }
293
+ function requireBoundManifest(ddFlowHome, input) {
294
+ const manifest = input.snapshotRoot
295
+ ? requireManifest(path.join(input.snapshotRoot, "engine.json"))
296
+ : findInstalledManifest(ddFlowHome, {
297
+ packageName: input.packageName,
298
+ packageVersion: input.packageVersion,
299
+ ...(input.integrityChecksum ? { integrityChecksum: input.integrityChecksum } : {})
300
+ });
301
+ if (!manifest
302
+ || manifest.package_name !== input.packageName
303
+ || manifest.package_version !== input.packageVersion
304
+ || input.integrityChecksum !== undefined && manifest.integrity.checksum !== input.integrityChecksum
305
+ || input.snapshotRoot !== undefined && path.resolve(manifest.snapshot_root) !== path.resolve(input.snapshotRoot)
306
+ || !manifestHealthy(manifest)) {
307
+ throw new AppError("run_engine_binding_invalid", "RUN engine binding points to a missing or unhealthy engine snapshot", 1, {
308
+ package_name: input.packageName,
309
+ package_version: input.packageVersion,
310
+ snapshot_root: input.snapshotRoot ?? null,
311
+ integrity_checksum: input.integrityChecksum ?? null
312
+ });
313
+ }
314
+ return manifest;
315
+ }
316
+ function engineIdentity(manifest) {
317
+ if (!manifest.integrity.checksum) {
318
+ throw new AppError("engine_integrity_missing", "Cannot bind a RUN to an engine without a content checksum", 1, {
319
+ engine: manifestSummary(manifest)
320
+ });
321
+ }
322
+ return {
323
+ package_name: manifest.package_name,
324
+ package_version: manifest.package_version,
325
+ engine_version: manifest.engine_version,
326
+ integrity_checksum: manifest.integrity.checksum,
327
+ snapshot_root: path.resolve(manifest.snapshot_root)
328
+ };
329
+ }
330
+ function sameEngineIdentity(left, right) {
331
+ return left.package_name === right.package_name
332
+ && left.package_version === right.package_version
333
+ && left.engine_version === right.engine_version
334
+ && left.integrity_checksum === right.integrity_checksum
335
+ && path.resolve(left.snapshot_root) === path.resolve(right.snapshot_root);
336
+ }
337
+ function executionManifest(context) {
338
+ const home = context.env.DD_FLOW_ENGINE_HOME;
339
+ if (home && home !== "in-process")
340
+ return requireBoundManifest(context.ddFlowHome, { packageName: getCliBuildInfo().package_name, packageVersion: getCliBuildInfo().version, snapshotRoot: home });
341
+ // Refresh the same-semver snapshot when this build differs. The installer
342
+ // deduplicates identical checksums and keeps any bound snapshot immutable.
343
+ const installed = installCurrentEngine(context, { force: true, forRouting: true });
344
+ return requireManifest(path.join(String(installed.path), "engine.json"));
345
+ }
346
+ function requiredText(value, label) {
347
+ const normalized = value.trim();
348
+ if (!normalized)
349
+ throw new AppError("usage", `Missing required value: ${label}`, 2);
350
+ return normalized;
351
+ }
352
+ function engineIsBound(ddFlowHome, manifest) {
353
+ const snapshotRoot = path.resolve(manifest.snapshot_root);
354
+ return allRunEngineBindings(ddFlowHome).some((binding) => path.resolve(binding.engine.snapshot_root) === snapshotRoot);
355
+ }
150
356
  export function engineRoutingMetadata(env) {
151
357
  if (env.DD_FLOW_ENGINE_MODE !== "1")
152
358
  return null;
@@ -156,25 +362,54 @@ export function engineRoutingMetadata(env) {
156
362
  routed_from: env.DD_FLOW_ROUTED_FROM ?? null
157
363
  };
158
364
  }
159
- export function selectEngine(context, input, options = {}) {
160
- const compatibility = readCompatibilityForProject(input.projectRoot);
365
+ export function selectEngine(context, input, options = {}, classification = input.classification ?? classifyCliOperation(["status"], input.env ?? process.env)) {
366
+ const projectCompatibility = readCompatibilityForProject(input.projectRoot);
367
+ const targetCanon = classification.mode === "mb_upgrade" ? resolveCanonRootBeforeContext(input.env ?? process.env) : null;
368
+ const compatibility = classification.mode === "mb_upgrade"
369
+ ? readCompatibilityForCanon(targetCanon)
370
+ : projectCompatibility;
161
371
  const build = getCliBuildInfo();
162
- const packageName = stringValue(compatibility?.engine?.package_name) ?? build.package_name;
163
- const requiredRange = stringValue(compatibility?.engine?.version_range) ?? `>=${build.version} <9999.0.0`;
372
+ const targetBlockers = classification.mode === "mb_upgrade"
373
+ ? [
374
+ ...(targetCanon && !targetCanon.ok ? targetCanon.blockers : []),
375
+ ...(targetCanon?.ok && !compatibility ? ["upgrade target compatibility manifest is unavailable"] : []),
376
+ ...targetPinBlockers(input.env ?? process.env, targetCanon)
377
+ ]
378
+ : [];
379
+ const packageName = stringValue(compatibility?.engine?.package_name) ?? (classification.mode === "mb_upgrade" ? build.package_name : build.package_name);
380
+ const requiredRange = stringValue(compatibility?.engine?.version_range) ?? (classification.mode === "mb_upgrade" ? null : `>=${build.version} <9999.0.0`);
164
381
  const recommended = stringValue(compatibility?.engine?.recommended_version) ?? build.version;
165
382
  const installHintTemplate = stringValue(compatibility?.engine?.old_engine_install_hint) ?? `npx ${packageName}@<version> engine install`;
166
383
  const manifests = readInstalledManifests(engineStoreRoot(context.ddFlowHome))
167
384
  .filter((manifest) => manifest.package_name === packageName)
168
- .filter((manifest) => satisfiesRange(manifest.package_version, requiredRange))
385
+ .filter((manifest) => requiredRange !== null && satisfiesRange(manifest.package_version, requiredRange))
169
386
  .filter((manifest) => options.allowCurrentInProcess
170
387
  && manifest.package_name === build.package_name
171
388
  && manifest.package_version === build.version
172
389
  ? true
173
390
  : manifestHealthy(manifest))
174
- .sort((a, b) => compareSemverStrings(b.package_version, a.package_version));
175
- const selected = manifests[0] ?? null;
391
+ .sort((a, b) => {
392
+ const versionOrder = compareSemverStrings(b.package_version, a.package_version);
393
+ if (versionOrder !== 0)
394
+ return versionOrder;
395
+ const installedOrder = Date.parse(b.installed_at) - Date.parse(a.installed_at);
396
+ if (Number.isFinite(installedOrder) && installedOrder !== 0)
397
+ return installedOrder;
398
+ return b.snapshot_root.localeCompare(a.snapshot_root);
399
+ });
400
+ const selected = manifests[0] ?? (options.allowCurrentInProcess &&
401
+ targetBlockers.length === 0 &&
402
+ requiredRange !== null &&
403
+ packageName === build.package_name &&
404
+ satisfiesRange(build.version, requiredRange)
405
+ ? currentBuildManifest(context, build)
406
+ : null);
407
+ const diagnostics = [
408
+ ...targetBlockers,
409
+ ...(selected ? [] : [`No installed engine satisfies ${packageName} ${requiredRange ?? "the pinned target range"}`])
410
+ ];
176
411
  return {
177
- status: selected ? "selected" : "missing",
412
+ status: targetBlockers.length > 0 ? "unsupported" : selected ? "selected" : "missing",
178
413
  package_name: packageName,
179
414
  required_range: requiredRange,
180
415
  recommended_version: recommended,
@@ -182,9 +417,38 @@ export function selectEngine(context, input, options = {}) {
182
417
  install_hint: selected ? null : installHintTemplate.replace("<version>", recommended),
183
418
  project_root: input.projectRoot ? path.resolve(input.projectRoot) : null,
184
419
  memory_bank_version: compatibility?.memory_bank_version ?? null,
185
- diagnostics: selected ? [] : [`No installed engine satisfies ${packageName} ${requiredRange}`]
420
+ diagnostics
421
+ };
422
+ }
423
+ function currentBuildManifest(context, build) {
424
+ const packageRoot = findPackageRoot(path.dirname(sourceFile)) ?? process.cwd();
425
+ return buildManifest(context, build.package_name, build.version, packageRoot, packageRoot, packageRoot);
426
+ }
427
+ export function compatibilityContexts(context, input = {}) {
428
+ const env = input.env ?? process.env;
429
+ const classification = input.classification ?? classifyCliOperation(["status"], env);
430
+ const currentSelection = selectEngine(context, { projectRoot: input.projectRoot, env }, { allowCurrentInProcess: true }, { mode: "normal_write", operation: "project.current", family: "project", command: "current", action: null });
431
+ const targetCanon = classification.mode === "mb_upgrade" ? resolveCanonRootBeforeContext(env) : null;
432
+ const targetSelection = classification.mode === "mb_upgrade"
433
+ ? selectEngine(context, { projectRoot: input.projectRoot, env }, {}, classification)
434
+ : null;
435
+ return {
436
+ project_current: compatibilityContextSummary("project_current", currentSelection, null),
437
+ upgrade_target: classification.mode === "mb_upgrade"
438
+ ? compatibilityContextSummary("upgrade_target", targetSelection ?? currentSelection, targetCanon)
439
+ : null,
440
+ effective_execution: {
441
+ operation: classification.operation,
442
+ mode: classification.mode,
443
+ package_name: (targetSelection ?? currentSelection).package_name,
444
+ engine_version: (targetSelection ?? currentSelection).selected?.package_version ?? null,
445
+ reason: classification.mode === "mb_upgrade" ? "explicit_upgrade_allowlist_target_canon" : "project_current"
446
+ }
186
447
  };
187
448
  }
449
+ export function resolveExplicitProjectRoot(args) {
450
+ return option(parseLightArgs(args), "project-root") ?? option(parseLightArgs(args), "root");
451
+ }
188
452
  export function resolveOperationProjectRoot(context, args) {
189
453
  const [family, command, ...rest] = args;
190
454
  const parsed = parseLightArgs(rest);
@@ -234,6 +498,68 @@ function readCompatibilityForProject(projectRoot) {
234
498
  }
235
499
  return null;
236
500
  }
501
+ function isUpgradeDiagnostic(args) {
502
+ const [family, command] = args;
503
+ return family === "status" || family === "engine" && command === "resolve";
504
+ }
505
+ function targetPinBlockers(env, resolution) {
506
+ if (!resolution?.ok || !resolution.canon)
507
+ return [];
508
+ const blockers = [];
509
+ const expectedVersion = env.DD_FLOW_MB_UPGRADE_TARGET_VERSION;
510
+ const expectedCommit = env.DD_FLOW_MB_UPGRADE_TARGET_COMMIT;
511
+ if (expectedVersion && expectedVersion !== resolution.canon.version)
512
+ blockers.push("target canon version pin does not match resolved canon");
513
+ if (expectedCommit && expectedCommit !== resolution.canon.commit)
514
+ blockers.push("target canon commit pin does not match resolved canon");
515
+ const expectedSha = env.DD_FLOW_MB_UPGRADE_COMPATIBILITY_SHA256;
516
+ if (expectedSha && expectedSha !== canonCompatibilitySha(resolution.canon.memorybank_root))
517
+ blockers.push("target compatibility SHA pin does not match resolved canon");
518
+ return blockers;
519
+ }
520
+ function readCompatibilityForCanon(resolution) {
521
+ if (!resolution?.ok || !resolution.canon)
522
+ return null;
523
+ return readCompatibilityFile(path.join(resolution.canon.memorybank_root, "dd-flow", "compatibility.json"));
524
+ }
525
+ function readCompatibilityFile(file) {
526
+ try {
527
+ return JSON.parse(fs.readFileSync(file, "utf8"));
528
+ }
529
+ catch {
530
+ return null;
531
+ }
532
+ }
533
+ function compatibilityContextSummary(name, selection, canon) {
534
+ return {
535
+ name,
536
+ canon: canon?.canon ? {
537
+ root: canon.canon.root,
538
+ version: canon.canon.version,
539
+ commit: canon.canon.commit,
540
+ flow_contract: canon.canon.flow_contract,
541
+ compatibility_sha256: canonCompatibilitySha(canon.canon.memorybank_root)
542
+ } : {
543
+ source: "project_current"
544
+ },
545
+ memory_bank_version: selection.memory_bank_version,
546
+ package_name: selection.package_name,
547
+ required_engine_range: selection.required_range,
548
+ recommended_engine_version: selection.recommended_version,
549
+ resolution: selection.status,
550
+ engine_version: selection.selected?.package_version ?? null,
551
+ diagnostics: selection.diagnostics
552
+ };
553
+ }
554
+ function canonCompatibilitySha(memorybankRoot) {
555
+ const file = path.join(memorybankRoot, "dd-flow", "compatibility.json");
556
+ try {
557
+ return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
558
+ }
559
+ catch {
560
+ return null;
561
+ }
562
+ }
237
563
  function parseLightArgs(args) {
238
564
  const positional = [];
239
565
  const options = new Map();
@@ -285,6 +611,7 @@ function buildManifest(context, packageName, version, packageRoot, snapshotRoot,
285
611
  version: ["dd-flow/version-report@1"],
286
612
  status: ["dd-flow/status-report@1"]
287
613
  },
614
+ schema_registry: buildSchemaRegistry(checksumRoot),
288
615
  integrity: {
289
616
  source: "package_snapshot",
290
617
  checksum: engineSnapshotChecksum(checksumRoot),
@@ -430,6 +757,11 @@ function readInstalledManifests(root) {
430
757
  }
431
758
  return manifests;
432
759
  }
760
+ function findInstalledManifest(ddFlowHome, input) {
761
+ return readInstalledManifests(engineStoreRoot(ddFlowHome)).find((manifest) => (manifest.package_name === input.packageName
762
+ && manifest.package_version === input.packageVersion
763
+ && (input.integrityChecksum === undefined || manifest.integrity.checksum === input.integrityChecksum))) ?? null;
764
+ }
433
765
  function requireManifest(file) {
434
766
  const manifest = readManifest(file);
435
767
  if (!manifest)
@@ -479,6 +811,24 @@ function manifestDiagnostics(manifest, options = {}) {
479
811
  diagnostics.push("snapshot_unreadable");
480
812
  }
481
813
  }
814
+ if (manifest.schema_registry) {
815
+ for (const entry of manifest.schema_registry.entries) {
816
+ const schemaPath = safeResolveSnapshotPath(manifest, entry.path);
817
+ if (!schemaPath || !fs.existsSync(schemaPath))
818
+ diagnostics.push(`schema_registry_missing:${entry.name}`);
819
+ else if (fileChecksum(schemaPath) !== entry.checksum)
820
+ diagnostics.push(`schema_registry_checksum_mismatch:${entry.name}`);
821
+ else {
822
+ try {
823
+ if (JSON.parse(fs.readFileSync(schemaPath, "utf8")).$id !== entry.id)
824
+ diagnostics.push(`schema_registry_id_mismatch:${entry.name}`);
825
+ }
826
+ catch {
827
+ diagnostics.push(`schema_registry_unreadable:${entry.name}`);
828
+ }
829
+ }
830
+ }
831
+ }
482
832
  if (options.checkEntrypoint !== false && entrypoint && fs.existsSync(entrypoint) && !diagnostics.includes("entrypoint_outside_snapshot")) {
483
833
  const health = spawnSync(process.execPath, [entrypoint, "--version", "--json"], {
484
834
  cwd: snapshotRoot,
@@ -503,6 +853,7 @@ function manifestSummary(manifest) {
503
853
  package_name: manifest.package_name,
504
854
  package_version: manifest.package_version,
505
855
  engine_version: manifest.engine_version,
856
+ ...(manifest.schema_registry ? { schema_registry: manifest.schema_registry } : {}),
506
857
  install_source: manifest.install_source,
507
858
  entrypoint: manifest.entrypoint,
508
859
  snapshot_root: manifest.snapshot_root,
@@ -530,6 +881,33 @@ function safeResolveManifestEntrypoint(manifest) {
530
881
  return null;
531
882
  return candidate;
532
883
  }
884
+ function safeResolveSnapshotPath(manifest, relativePath) {
885
+ if (path.isAbsolute(relativePath))
886
+ return null;
887
+ const snapshotRoot = path.resolve(manifest.snapshot_root || manifest.package_root);
888
+ const candidate = path.resolve(snapshotRoot, relativePath);
889
+ return isInside(snapshotRoot, candidate) ? candidate : null;
890
+ }
891
+ function buildSchemaRegistry(snapshotRoot) {
892
+ const schemaRoot = path.join(snapshotRoot, "dist", "schemas");
893
+ const entries = !fs.existsSync(schemaRoot) ? [] : fs.readdirSync(schemaRoot, { withFileTypes: true })
894
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".schema.json") && !entry.name.includes("-legacy-"))
895
+ .map((entry) => {
896
+ const file = path.join(schemaRoot, entry.name);
897
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
898
+ return {
899
+ name: entry.name.slice(0, -".schema.json".length),
900
+ id: typeof parsed.$id === "string" ? parsed.$id : entry.name,
901
+ path: path.join("dist", "schemas", entry.name),
902
+ checksum: fileChecksum(file)
903
+ };
904
+ })
905
+ .sort((left, right) => left.name.localeCompare(right.name));
906
+ return { schema_id: "dd-flow/engine-schema-registry@1", entries };
907
+ }
908
+ function fileChecksum(file) {
909
+ return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
910
+ }
533
911
  function isInside(root, candidate) {
534
912
  const relative = path.relative(root, candidate);
535
913
  return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));