@deksden-com/dd-flow-cli 0.9.0-beta.144 → 0.9.0-beta.149

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 (254) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +13 -8
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/command-inputs.js +13 -3
  5. package/dist/cli/command-metadata.js +51 -16
  6. package/dist/cli/diagnostic-invocation.js +46 -0
  7. package/dist/cli/diagnostic.js +120 -0
  8. package/dist/cli/help.js +67 -1
  9. package/dist/cli/hook-ingress.js +27 -3
  10. package/dist/cli/memory-flow-admission.js +9 -3
  11. package/dist/cli/memory-init-invocation.js +10 -3
  12. package/dist/cli/memory-init.js +77 -68
  13. package/dist/cli/memory-storage-admission.js +3 -2
  14. package/dist/cli/memory-storage-qualification.js +105 -0
  15. package/dist/cli/memory-storage-transition.js +123 -0
  16. package/dist/cli/memory-upgrade.js +189 -0
  17. package/dist/cli/native-hook-ingress.js +14 -4
  18. package/dist/cli/native-hook-preparation.js +55 -0
  19. package/dist/cli/response-file.js +2 -1
  20. package/dist/cli/run-cli.js +79 -40
  21. package/dist/cli/setup.js +2 -1
  22. package/dist/cli.js +18 -0
  23. package/dist/domain/diagnostic-aspects.js +103 -0
  24. package/dist/domain/diagnostic-flows.js +8 -0
  25. package/dist/domain/stage-handler-identities.js +8 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
  31. package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
  32. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  33. package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
  34. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +366 -52
  35. package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
  36. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
  37. package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
  38. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
  39. package/dist/harness-runtime/lib/dd-droid.mjs +71 -29
  40. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +73 -16
  41. package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
  42. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
  43. package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
  44. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
  45. package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
  46. package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
  47. package/dist/harness-runtime/lib/driver-recovery.mjs +9 -8
  48. package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
  49. package/dist/harness-runtime/lib/entity-references.mjs +9 -0
  50. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
  51. package/dist/harness-runtime/lib/managed-daemon.mjs +24 -9
  52. package/dist/harness-runtime/lib/native-children.d.mts +8 -1
  53. package/dist/harness-runtime/lib/native-children.mjs +114 -8
  54. package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
  55. package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
  56. package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
  57. package/dist/harness-runtime/lib/observation-clock.mjs +7 -4
  58. package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
  59. package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
  60. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +5 -3
  61. package/dist/harness-runtime/lib/provider-overload-policy.mjs +20 -7
  62. package/dist/harness-runtime/lib/runner-events.mjs +3 -1
  63. package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
  64. package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
  65. package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
  66. package/dist/schemas/diagnostic-work-result.schema.json +296 -0
  67. package/dist/schemas/execution-bundle.schema.json +7 -1
  68. package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
  69. package/dist/schemas/memory-repair-work-result.schema.json +9 -2
  70. package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
  71. package/dist/schemas/release-impact.schema.json +3 -3
  72. package/dist/schemas/stage-start-response.schema.json +4 -0
  73. package/dist/services/accepted-intent.js +13 -1
  74. package/dist/services/canon.js +43 -7
  75. package/dist/services/check-environment.js +21 -0
  76. package/dist/services/check-runner-evidence.js +267 -15
  77. package/dist/services/cli-operation-classifier.js +1 -1
  78. package/dist/services/cli-upgrade-allowlist.js +1 -1
  79. package/dist/services/code-checks.js +34 -19
  80. package/dist/services/compatibility-preflight.js +5 -2
  81. package/dist/services/controller-execution.js +10 -4
  82. package/dist/services/controller-fanout.js +34 -10
  83. package/dist/services/controller-memory-task.js +45 -0
  84. package/dist/services/controller-memory-worker.js +49 -0
  85. package/dist/services/dashboard.js +2 -2
  86. package/dist/services/descriptor-run-settlement.js +5 -3
  87. package/dist/services/descriptor-run-start.js +46 -20
  88. package/dist/services/descriptor-stage-coordinator.js +66 -13
  89. package/dist/services/descriptor-stage-settlement.js +2 -1
  90. package/dist/services/descriptor-stage-start.js +40 -4
  91. package/dist/services/diagnostic-evidence.js +73 -0
  92. package/dist/services/diagnostic-report-publication.js +72 -0
  93. package/dist/services/diagnostic-report.js +67 -0
  94. package/dist/services/diagnostic-run-bundle.js +74 -0
  95. package/dist/services/diagnostic-snapshot.js +159 -0
  96. package/dist/services/diagnostic-stage-handlers.js +108 -0
  97. package/dist/services/diagnostic-stage-results.js +132 -0
  98. package/dist/services/diagnostic-start.js +84 -0
  99. package/dist/services/diagnostic-work-batch.js +34 -0
  100. package/dist/services/diagnostic-work-collection.js +97 -0
  101. package/dist/services/diagnostic-work-registration.js +101 -0
  102. package/dist/services/diagnostic-work-results.js +93 -0
  103. package/dist/services/engines.js +64 -14
  104. package/dist/services/eval-snapshots.js +186 -119
  105. package/dist/services/execution-bundles.js +19 -7
  106. package/dist/services/external-work-launch.js +5 -3
  107. package/dist/services/flow-catalog.js +13 -4
  108. package/dist/services/frozen-engine.js +148 -11
  109. package/dist/services/hook-memory-routing.js +188 -0
  110. package/dist/services/hook-recovery-guard.js +14 -0
  111. package/dist/services/hook-session-observation.js +24 -0
  112. package/dist/services/hooks.js +27 -8
  113. package/dist/services/hybrid-contracts.js +4 -13
  114. package/dist/services/hybrid-digest.js +14 -0
  115. package/dist/services/installed-flow-source.js +47 -4
  116. package/dist/services/lifecycle-invocations.js +179 -38
  117. package/dist/services/memory-accepted-composition.js +5 -4
  118. package/dist/services/memory-completed-correction-wave.js +23 -15
  119. package/dist/services/memory-compose-handler.js +163 -21
  120. package/dist/services/memory-composition-acceptance.js +186 -10
  121. package/dist/services/memory-composition-graph.js +42 -11
  122. package/dist/services/memory-composition-registration.js +23 -8
  123. package/dist/services/memory-composition-work-custody.js +14 -9
  124. package/dist/services/memory-composition-work-results.js +15 -3
  125. package/dist/services/memory-corrected-review-batch.js +10 -3
  126. package/dist/services/memory-corrected-review-findings.js +4 -2
  127. package/dist/services/memory-corrected-review-inputs.js +10 -5
  128. package/dist/services/memory-corrected-review-receipt.js +17 -9
  129. package/dist/services/memory-corrected-review-registration.js +3 -2
  130. package/dist/services/memory-correction-check-receipt.js +20 -4
  131. package/dist/services/memory-custody-snapshot.js +36 -0
  132. package/dist/services/memory-finish-evidence.js +44 -22
  133. package/dist/services/memory-finish-handler.js +37 -23
  134. package/dist/services/memory-finish-integration.js +218 -48
  135. package/dist/services/memory-finish-readiness.js +66 -19
  136. package/dist/services/memory-git-integration.js +87 -18
  137. package/dist/services/memory-historical-corrected-review.js +21 -15
  138. package/dist/services/memory-historical-original-review.js +22 -9
  139. package/dist/services/memory-historical-repair-registration.js +14 -8
  140. package/dist/services/memory-historical-second-review.js +30 -11
  141. package/dist/services/memory-init-contexts.js +44 -2
  142. package/dist/services/memory-init-controller-start.js +27 -8
  143. package/dist/services/memory-init-git-policy.js +50 -21
  144. package/dist/services/memory-init-lint-capability.js +24 -6
  145. package/dist/services/memory-init-native-capacity.js +17 -4
  146. package/dist/services/memory-init-preflight.js +21 -17
  147. package/dist/services/memory-init-request-candidate.js +73 -17
  148. package/dist/services/memory-init-request-input.js +23 -7
  149. package/dist/services/memory-init-run-bundle.js +49 -12
  150. package/dist/services/memory-init-storage.js +17 -7
  151. package/dist/services/memory-init-worktrunk-capability.js +3 -7
  152. package/dist/services/memory-layer.js +71 -6
  153. package/dist/services/memory-lint-config.js +111 -2
  154. package/dist/services/memory-native-review-copy.js +5 -67
  155. package/dist/services/memory-normal-flow-entry.js +12 -0
  156. package/dist/services/memory-ordinary-defaults.js +22 -5
  157. package/dist/services/memory-preflight-handler.js +15 -0
  158. package/dist/services/memory-repair-current-view.js +1 -1
  159. package/dist/services/memory-repair-document-delta.js +70 -33
  160. package/dist/services/memory-repair-history.js +12 -7
  161. package/dist/services/memory-repair-physical-binding.js +1 -119
  162. package/dist/services/memory-repair-publication-plan.js +5 -3
  163. package/dist/services/memory-repair-publication-source.js +1 -1
  164. package/dist/services/memory-repair-publication.js +3 -2
  165. package/dist/services/memory-repair-registration.js +2 -1
  166. package/dist/services/memory-repair-work-baseline.js +4 -2
  167. package/dist/services/memory-repair-work-completion.js +32 -12
  168. package/dist/services/memory-repair-work-results.js +21 -2
  169. package/dist/services/memory-repair-workspace.js +11 -2
  170. package/dist/services/memory-repair-writer-copy.js +2 -1
  171. package/dist/services/memory-result-binding.js +78 -12
  172. package/dist/services/memory-retained-review-authority.js +17 -8
  173. package/dist/services/memory-retained-second-baseline.js +1 -1
  174. package/dist/services/memory-retained-second-repairs.js +12 -6
  175. package/dist/services/memory-review-correction-plan.js +47 -12
  176. package/dist/services/memory-review-handler.js +17 -17
  177. package/dist/services/memory-review-inputs.js +55 -9
  178. package/dist/services/memory-review-limits.js +10 -0
  179. package/dist/services/memory-review-receipt.js +2 -2
  180. package/dist/services/memory-review-registration.js +13 -4
  181. package/dist/services/memory-review-work-custody.js +3 -1
  182. package/dist/services/memory-review-work-input.js +26 -7
  183. package/dist/services/memory-review-work-results.js +26 -4
  184. package/dist/services/memory-second-corrected-review-inputs.js +5 -3
  185. package/dist/services/memory-second-correction-plan.js +2 -1
  186. package/dist/services/memory-second-repair-launch.js +4 -4
  187. package/dist/services/memory-second-repair-source.js +1 -1
  188. package/dist/services/memory-stage-dispatch.js +38 -1
  189. package/dist/services/memory-stage-results.js +113 -22
  190. package/dist/services/memory-tool-probe.js +25 -0
  191. package/dist/services/memory-upgrade-analysis-handler.js +243 -0
  192. package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
  193. package/dist/services/memory-upgrade-evidence.js +100 -0
  194. package/dist/services/memory-upgrade-inventory.js +442 -0
  195. package/dist/services/memory-upgrade-publication-custody.js +73 -0
  196. package/dist/services/memory-upgrade-publication.js +413 -0
  197. package/dist/services/memory-upgrade-selection.js +430 -0
  198. package/dist/services/memory-upgrade-start.js +60 -0
  199. package/dist/services/memory-upgrade-storage-readiness.js +340 -0
  200. package/dist/services/memory-verification-acceptance.js +38 -17
  201. package/dist/services/memory-verification-checks.js +92 -27
  202. package/dist/services/memory-verification-receipt.js +6 -5
  203. package/dist/services/memory-verify-handler.js +15 -13
  204. package/dist/services/memory-work-result-json.js +12 -0
  205. package/dist/services/memory-work-result-preparation.js +5 -4
  206. package/dist/services/merge-server.js +4 -2
  207. package/dist/services/migrations.js +127 -98
  208. package/dist/services/native-daemon-history.js +2 -1
  209. package/dist/services/physical-startup.js +45 -0
  210. package/dist/services/project-registry.js +97 -0
  211. package/dist/services/project-summary.js +1 -1
  212. package/dist/services/projects.js +3 -93
  213. package/dist/services/prompt-delivery.js +71 -0
  214. package/dist/services/provider-capacity.js +112 -46
  215. package/dist/services/recovery-snapshot-database.js +32 -4
  216. package/dist/services/release-cache.js +8 -0
  217. package/dist/services/repair-intents.js +1 -12
  218. package/dist/services/repair-receipt.js +13 -0
  219. package/dist/services/run-control-worker.js +35 -18
  220. package/dist/services/run-control.js +2 -1
  221. package/dist/services/run-controller-adapter.js +2 -1
  222. package/dist/services/run-controller-process.js +4 -7
  223. package/dist/services/run-controller-recovery.js +8 -2
  224. package/dist/services/run-controller.js +215 -94
  225. package/dist/services/run-fork.js +1 -1
  226. package/dist/services/run-recovery-guard.js +30 -0
  227. package/dist/services/run-recovery.js +11 -44
  228. package/dist/services/run-start-custody.js +17 -5
  229. package/dist/services/runs.js +6 -1
  230. package/dist/services/runtime-scope-resume.js +12 -6
  231. package/dist/services/runtime-scope-stop.js +3 -2
  232. package/dist/services/runtime-scope-worker.js +46 -19
  233. package/dist/services/runtime-service.js +2 -1
  234. package/dist/services/sessions.js +3 -24
  235. package/dist/services/setup.js +63 -13
  236. package/dist/services/stage-handlers.js +22 -7
  237. package/dist/services/stage-pause.js +111 -44
  238. package/dist/services/version-status.js +1 -1
  239. package/dist/services/vnext-code.js +2 -2
  240. package/dist/services/vnext-fanout.js +51 -9
  241. package/dist/services/vnext-plan-review.js +3 -5
  242. package/dist/services/work-registry.js +117 -39
  243. package/dist/services/workspace-bootstrap.js +2 -1
  244. package/dist/services/workspace-files.js +1 -1
  245. package/dist/shared/entity-references.js +1 -8
  246. package/dist/storage/data-preservation.js +65 -0
  247. package/dist/storage/database.js +53 -19
  248. package/dist/storage/import-guard.js +84 -27
  249. package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
  250. package/dist/storage/paired-writer-migration.js +424 -0
  251. package/dist/storage/writer-contract.js +11 -4
  252. package/dist/storage/writer-migration.js +313 -52
  253. package/dist/storage/writer-restore.js +217 -0
  254. package/package.json +13 -6
@@ -23,18 +23,21 @@ import { localEnvironmentAllowed, projectServicePathFilter } from "./workspace-f
23
23
  import { cleanupScopeWriters } from "./scope-writer-cleanup.js";
24
24
  import { PreparedInstalledFlowSource } from "./installed-flow-source.js";
25
25
  import { readSetupStatus } from "./setup.js";
26
+ import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
26
27
  import { isObservationOperation, nativeInventoryVersion, retainedDaemonOperations } from "./run-control.js";
27
28
  /** Capture the only flow boundary which exists before `stage start --bootstrap` creates a RUN. */
28
29
  export function prepareEvalBootstrapSnapshot(context, input) {
29
30
  let source;
30
31
  if (input.flow !== undefined && input.flow !== "sdlc") {
31
- if (input.flow !== "mb-init")
32
+ if (input.flow !== "mb-init" && input.flow !== "mb-upgrade" && !isDiagnosticFlow(input.flow))
32
33
  throw new AppError("snapshot_flow_unsupported", "Bootstrap snapshot flow is not registered", 2, { phase: "prepare", effect: "no_effect" });
33
34
  const home = context.env.DD_FLOW_CONFIG_HOME ?? context.ddFlowHome;
34
35
  const selected = readSetupStatus(home);
35
36
  if (!selected.installation)
36
37
  throw new AppError("snapshot_flow_unavailable", "Bootstrap snapshot requires the selected installed flow", 2, { phase: "prepare", effect: "no_effect" });
37
38
  source = PreparedInstalledFlowSource.fromInstalledRelease(home, selected.installation.manifest_sha256, input.flow);
39
+ if (source.resolveRegisteredFlow().descriptor.stages[0]?.id !== "preflight")
40
+ throw new AppError("snapshot_flow_unsupported", "Memory bootstrap requires its preflight entry", 2, { phase: "prepare", effect: "no_effect" });
38
41
  }
39
42
  const projectRoot = resolveProjectRoot(input.projectRoot);
40
43
  const output = path.resolve(input.output);
@@ -68,7 +71,7 @@ export function createEvalBootstrapSnapshot(context, input) {
68
71
  const manifest = {
69
72
  schema_id: source ? "dd-eval/bootstrap-snapshot@2" : "dd-eval/bootstrap-snapshot@1",
70
73
  stage: source?.resolveRegisteredFlow().descriptor.stages[0]?.id ?? "specify",
71
- ...(source ? { flow: { id: "mb-init", source_sha256: source.sha256, descriptor_sha256: source.descriptorReference().sha256 } } : {}),
74
+ ...(source ? { flow: { id: source.resolveRegisteredFlow().descriptor.id, source_sha256: source.sha256, descriptor_sha256: source.descriptorReference().sha256 } } : {}),
72
75
  project_root: projectRoot,
73
76
  project_sha256: treeHash(path.join(temporary, "project")),
74
77
  project_git: snapshotProjectGit(projectRoot, temporary)
@@ -220,7 +223,7 @@ export function prepareEvalRunSnapshotCreation(context, input) {
220
223
  throw new AppError("snapshot_output_inside_project", "Snapshot output must be outside the project and RUN workspace trees", 1, { output });
221
224
  prepareOutputFile(output);
222
225
  prepareSnapshotProjectGit(projectRoot, recovery && status.run.flow_kind === "mb-init");
223
- prepareSnapshotWorkspace(workspaceRoot, projectRoot);
226
+ prepareSnapshotWorkspace(workspaceRoot, projectRoot, isDiagnosticFlow(status.run.flow_kind));
224
227
  return { projectRoot, project, status, recovery, output, workspaceRoot, activeChildren };
225
228
  }
226
229
  function captureEvalRunSnapshot(context, input) {
@@ -238,7 +241,7 @@ function captureEvalRunSnapshot(context, input) {
238
241
  }
239
242
  }
240
243
  else
241
- copyRuntimeTree(context.ddFlowHome, path.join(output, "runtime"), false, [...databaseFiles, ...codexExclusions], grokProviderHomes(context.ddFlowHome), input.observationCuts);
244
+ copyRuntimeTree(context.ddFlowHome, path.join(output, "runtime"), false, [...databaseFiles, ...codexExclusions, ...(isDiagnosticFlow(status.run.flow_kind) ? [workspaceRoot] : [])], grokProviderHomes(context.ddFlowHome), input.observationCuts);
242
245
  for (const [file, cut] of input.journalCuts ?? []) {
243
246
  const target = path.join(output, "runtime", path.relative(context.ddFlowHome, file));
244
247
  fs.mkdirSync(path.dirname(target), { recursive: true });
@@ -278,7 +281,7 @@ function captureEvalRunSnapshot(context, input) {
278
281
  filter: projectPayloadFilter(projectRoot)
279
282
  });
280
283
  const projectGit = snapshotProjectGit(projectRoot, output, recovery && status.run.flow_kind === "mb-init");
281
- const workspace = snapshotWorkspace(workspaceRoot, projectRoot, output);
284
+ const workspace = snapshotWorkspace(workspaceRoot, projectRoot, output, isDiagnosticFlow(status.run.flow_kind));
282
285
  // Accepted Work results are captured in SQLite. Their file representations
283
286
  // are retained evidence, not a second authority or a checkpoint gate.
284
287
  if (recovery) {
@@ -291,7 +294,7 @@ function captureEvalRunSnapshot(context, input) {
291
294
  }
292
295
  }
293
296
  const manifest = {
294
- schema_id: projectGit.state === "unborn" ? "dd-flow/eval-run-snapshot@6" : "dd-flow/eval-run-snapshot@5",
297
+ schema_id: workspace.kind === "git_repository" ? "dd-flow/eval-run-snapshot@7" : projectGit.state === "unborn" ? "dd-flow/eval-run-snapshot@6" : "dd-flow/eval-run-snapshot@5",
295
298
  run_id: input.runId,
296
299
  project_id: project.id,
297
300
  project_root: projectRoot,
@@ -308,7 +311,7 @@ function captureEvalRunSnapshot(context, input) {
308
311
  ...(input.observationCuts ? { observation_cuts: [...input.observationCuts].map(([file, receipt]) => ({ path: path.relative(context.ddFlowHome, file), receipt })) } : {}),
309
312
  project_git: projectGit,
310
313
  workspace,
311
- payload_modes: { project: treeModesHash(path.join(output, "project")), runtime: treeModesHash(path.join(output, "runtime")), ...(workspace.kind === "git_worktree" ? { workspace: treeModesHash(path.join(output, "workspace")) } : {}) },
314
+ payload_modes: { project: treeModesHash(path.join(output, "project")), runtime: treeModesHash(path.join(output, "runtime")), ...(workspace.kind !== "project" ? { workspace: treeModesHash(path.join(output, "workspace")) } : {}) },
312
315
  source_status: status
313
316
  };
314
317
  writeJson(path.join(output, "snapshot.json"), manifest);
@@ -434,7 +437,7 @@ export function prepareSnapshotOperationalDecision(input) {
434
437
  }
435
438
  function assertRunRestoreTargets(context, input, snapshot, projectRoot, manifest) {
436
439
  assertRestoreTargetSeparation(snapshot, [projectRoot, context.ddFlowHome]);
437
- if (manifest.workspace.kind === "git_worktree") {
440
+ if (manifest.workspace.kind !== "project") {
438
441
  const workspaceRoot = restoredWorkspaceRoot(manifest, projectRoot, context.ddFlowHome);
439
442
  assertRestoreTargetSeparation(snapshot, [workspaceRoot]);
440
443
  if (isInside(physicalPath(projectRoot), physicalPath(workspaceRoot)) || isInside(physicalPath(workspaceRoot), physicalPath(projectRoot)) || physicalPath(workspaceRoot) === physicalPath(context.ddFlowHome))
@@ -460,105 +463,114 @@ export function restoreEvalRunSnapshot(context, input, prepared = prepareEvalRun
460
463
  const sourceManifestSha256 = fileHash(path.join(snapshot, "snapshot.json"));
461
464
  const importId = prearmedImportId ?? beginSnapshotImport({ home: context.ddFlowHome, projectRoot,
462
465
  workspaceRoot: restoredWorkspaceRoot(manifest, projectRoot, context.ddFlowHome), sourceManifestSha256 });
463
- clearProjectTree(projectRoot, false);
464
- restoreProjectGit(snapshot, manifest.project_git, projectRoot);
465
- replaceProjectTree(projectRoot, sourceProject);
466
- context.db.close?.();
467
- fs.rmSync(context.ddFlowHome, { recursive: true, force: true });
468
- fs.cpSync(sourceRuntime, context.ddFlowHome, { recursive: true, verbatimSymlinks: true });
469
- const workspaceRoot = restoreWorkspace(snapshot, manifest, projectRoot, context.ddFlowHome);
470
- // The dispatch context was intentionally closed before replacing db.sqlite.
471
- // A fresh process owns all subsequent normal commands.
472
- const restored = createContext({ ...context.env, DD_FLOW_HOME: context.ddFlowHome }, "initialize", importId);
473
- let result, primary, failed = false;
474
466
  try {
475
- const runtimeLineage = recovery ? null : detachSnapshotRuntimeOwners(restored, snapshot, manifest);
476
- const originalRunRoot = restored.db.get("SELECT run_root FROM runs WHERE id = ?", [manifest.run_id])?.run_root;
477
- rebaseRuntime(restored, manifest.project_root, projectRoot, sourceWorkspaceRoot(manifest), workspaceRoot, manifest.dd_flow_home, context.ddFlowHome, recovery);
478
- const publicationRun = restored.db.get("SELECT project_id, run_root FROM runs WHERE id = ?", [manifest.run_id]);
479
- if (!originalRunRoot || !publicationRun?.run_root || !isInside(manifest.dd_flow_home, originalRunRoot) || !isInside(context.ddFlowHome, publicationRun.run_root))
480
- throw new AppError("snapshot_invalid", "Restored publication roots are not bound to their runtime", 1);
481
- const pathMapping = [{ from: manifest.dd_flow_home, to: context.ddFlowHome }, { from: sourceWorkspaceRoot(manifest), to: workspaceRoot }, { from: manifest.project_root, to: projectRoot }];
482
- restored.db.run("INSERT INTO audit_events (project_id, event_type, forced, payload_json, created_at) VALUES (?, 'flow_run.publication_relocation', 0, ?, ?)", [publicationRun.project_id, JSON.stringify({ run_id: manifest.run_id, from: originalRunRoot, to: publicationRun.run_root, source_manifest_sha256: sourceManifestSha256, import_id: importId, path_mapping: pathMapping }), restored.now()]);
483
- registerProject(restored, { root: projectRoot });
484
- requireProjectByRoot(restored, projectRoot);
485
- if (selectedProfile && runtimeLineage) {
486
- const run = restored.db.get("SELECT run_root FROM runs WHERE id = ?", [manifest.run_id]);
487
- if (routingInput) {
488
- const artifact = path.join(run.run_root, selectedProfile.routing_input.artifact);
489
- if (fs.existsSync(artifact)) {
490
- if (!fs.readFileSync(artifact).equals(routingInput.bytes))
491
- throw new AppError("snapshot_checksum_mismatch", "Existing routing input does not match its checksum", 1);
467
+ clearProjectTree(projectRoot, false);
468
+ restoreProjectGit(snapshot, manifest.project_git, projectRoot);
469
+ replaceProjectTree(projectRoot, sourceProject);
470
+ context.db.close?.();
471
+ fs.rmSync(context.ddFlowHome, { recursive: true, force: true });
472
+ fs.cpSync(sourceRuntime, context.ddFlowHome, { recursive: true, verbatimSymlinks: true });
473
+ const workspaceRoot = restoreWorkspace(snapshot, manifest, projectRoot, context.ddFlowHome);
474
+ // The dispatch context was intentionally closed before replacing db.sqlite.
475
+ // A fresh process owns all subsequent normal commands.
476
+ const restored = createContext({ ...context.env, DD_FLOW_HOME: context.ddFlowHome }, "initialize", importId);
477
+ let result, primary, failed = false;
478
+ try {
479
+ const runtimeLineage = recovery ? null : detachSnapshotRuntimeOwners(restored, snapshot, manifest);
480
+ const originalRunRoot = restored.db.get("SELECT run_root FROM runs WHERE id = ?", [manifest.run_id])?.run_root;
481
+ rebaseRuntime(restored, manifest.project_root, projectRoot, sourceWorkspaceRoot(manifest), workspaceRoot, manifest.dd_flow_home, context.ddFlowHome, recovery);
482
+ const publicationRun = restored.db.get("SELECT project_id, run_root FROM runs WHERE id = ?", [manifest.run_id]);
483
+ if (!originalRunRoot || !publicationRun?.run_root || !isInside(manifest.dd_flow_home, originalRunRoot) || !isInside(context.ddFlowHome, publicationRun.run_root))
484
+ throw new AppError("snapshot_invalid", "Restored publication roots are not bound to their runtime", 1);
485
+ const pathMapping = [{ from: manifest.dd_flow_home, to: context.ddFlowHome }, { from: sourceWorkspaceRoot(manifest), to: workspaceRoot }, { from: manifest.project_root, to: projectRoot }];
486
+ restored.db.run("INSERT INTO audit_events (project_id, event_type, forced, payload_json, created_at) VALUES (?, 'flow_run.publication_relocation', 0, ?, ?)", [publicationRun.project_id, JSON.stringify({ run_id: manifest.run_id, from: originalRunRoot, to: publicationRun.run_root, source_manifest_sha256: sourceManifestSha256, import_id: importId, path_mapping: pathMapping }), restored.now()]);
487
+ registerProject(restored, { root: projectRoot });
488
+ requireProjectByRoot(restored, projectRoot);
489
+ if (selectedProfile && runtimeLineage) {
490
+ const run = restored.db.get("SELECT run_root FROM runs WHERE id = ?", [manifest.run_id]);
491
+ if (routingInput) {
492
+ const artifact = path.join(run.run_root, selectedProfile.routing_input.artifact);
493
+ if (fs.existsSync(artifact)) {
494
+ if (!fs.readFileSync(artifact).equals(routingInput.bytes))
495
+ throw new AppError("snapshot_checksum_mismatch", "Existing routing input does not match its checksum", 1);
496
+ }
497
+ else
498
+ fs.writeFileSync(artifact, routingInput.bytes, { flag: "wx", mode: 0o600 });
492
499
  }
493
- else
494
- fs.writeFileSync(artifact, routingInput.bytes, { flag: "wx", mode: 0o600 });
500
+ if (input.operationalDecisionFile) {
501
+ if (!prepared.operationalDecisionBytes || !fs.readFileSync(input.operationalDecisionFile).equals(prepared.operationalDecisionBytes))
502
+ throw new AppError("operational_decision_changed", "Accepted operational decision changed before import publication", 2);
503
+ const binding = { project_id: publicationRun.project_id, run_id: manifest.run_id, ...selectedProfile.operational_decision.scope };
504
+ const policyBytes = fs.readFileSync(path.resolve(projectRoot, selectedProfile.operational_decision.policy.path));
505
+ const policySource = { path: `operational-policy.${selectedProfile.operational_decision.policy.sha256}.source.md`, sha256: crypto.createHash("sha256").update(policyBytes).digest("hex") };
506
+ if (policySource.sha256 !== selectedProfile.operational_decision.policy.sha256)
507
+ throw new AppError("operational_decision_policy_changed", "Imported policy changed before binding", 2);
508
+ if (!fs.existsSync(path.join(run.run_root, policySource.path)))
509
+ fs.writeFileSync(path.join(run.run_root, policySource.path), policyBytes, { flag: "wx" });
510
+ const bytes = `${JSON.stringify({ schema_id: "dd-flow/run-operational-contract@1", binding, settings: { merge_delivery: selectedProfile.settings.merge_delivery, merge_cleanup: selectedProfile.settings.merge_cleanup }, decision: selectedProfile.operational_decision, policy_source: policySource }, null, 2)}\n`;
511
+ const artifact = `operational-contract.${crypto.createHash("sha256").update(bytes).digest("hex")}.json`;
512
+ fs.writeFileSync(path.join(run.run_root, artifact), bytes, { flag: "wx" });
513
+ selectedProfile.operational_contract = { path: artifact, sha256: crypto.createHash("sha256").update(bytes).digest("hex"), policy_source: policySource, binding };
514
+ }
515
+ importFlowRunExecutionProfile(restored, { projectRoot, runId: manifest.run_id, stage: manifest.stage_entry, lineageFile: runtimeLineage, profile: selectedProfile });
495
516
  }
496
- if (input.operationalDecisionFile) {
497
- if (!prepared.operationalDecisionBytes || !fs.readFileSync(input.operationalDecisionFile).equals(prepared.operationalDecisionBytes))
498
- throw new AppError("operational_decision_changed", "Accepted operational decision changed before import publication", 2);
499
- const binding = { project_id: publicationRun.project_id, run_id: manifest.run_id, ...selectedProfile.operational_decision.scope };
500
- const policyBytes = fs.readFileSync(path.resolve(projectRoot, selectedProfile.operational_decision.policy.path));
501
- const policySource = { path: `operational-policy.${selectedProfile.operational_decision.policy.sha256}.source.md`, sha256: crypto.createHash("sha256").update(policyBytes).digest("hex") };
502
- if (policySource.sha256 !== selectedProfile.operational_decision.policy.sha256)
503
- throw new AppError("operational_decision_policy_changed", "Imported policy changed before binding", 2);
504
- if (!fs.existsSync(path.join(run.run_root, policySource.path)))
505
- fs.writeFileSync(path.join(run.run_root, policySource.path), policyBytes, { flag: "wx" });
506
- const bytes = `${JSON.stringify({ schema_id: "dd-flow/run-operational-contract@1", binding, settings: { merge_delivery: selectedProfile.settings.merge_delivery, merge_cleanup: selectedProfile.settings.merge_cleanup }, decision: selectedProfile.operational_decision, policy_source: policySource }, null, 2)}\n`;
507
- const artifact = `operational-contract.${crypto.createHash("sha256").update(bytes).digest("hex")}.json`;
508
- fs.writeFileSync(path.join(run.run_root, artifact), bytes, { flag: "wx" });
509
- selectedProfile.operational_contract = { path: artifact, sha256: crypto.createHash("sha256").update(bytes).digest("hex"), policy_source: policySource, binding };
517
+ const relocation = recovery ? {
518
+ schema_id: "dd-flow/recovery-relocation@1", physical_runtime_id: crypto.randomUUID(), recovery_id: input.recoveryId, run_id: manifest.run_id,
519
+ path_mapping: [{ from: manifest.dd_flow_home, to: context.ddFlowHome }, { from: sourceWorkspaceRoot(manifest), to: workspaceRoot }, { from: manifest.project_root, to: projectRoot }]
520
+ } : null;
521
+ const relocationFile = relocation ? path.join(context.ddFlowHome, "recovery-imports", `${relocation.physical_runtime_id}.json`) : null;
522
+ if (relocationFile) {
523
+ fs.mkdirSync(path.dirname(relocationFile), { recursive: true });
524
+ writeJson(relocationFile, relocation);
510
525
  }
511
- importFlowRunExecutionProfile(restored, { projectRoot, runId: manifest.run_id, stage: manifest.stage_entry, lineageFile: runtimeLineage, profile: selectedProfile });
526
+ const status = getFlowRunStatus(restored, { projectRoot, runId: manifest.run_id });
527
+ if (!recovery)
528
+ assertStageEntry(status.index.stage_runs ?? [], manifest.stage_entry);
529
+ else
530
+ restored.db.run("UPDATE run_recovery_guards SET capture_path = ? WHERE recovery_id = ? AND status = 'sealed'", [snapshot, input.recoveryId]);
531
+ if (!status.run.run_root)
532
+ throw new AppError("runtime_missing", "Restored RUN has no artifact root", 1, { run_id: manifest.run_id });
533
+ const runHome = status.run.run_root;
534
+ const expectedWorkspaceRoot = restoredWorkspaceRoot(manifest, projectRoot, context.ddFlowHome);
535
+ if (physicalPath(workspaceRoot) !== physicalPath(expectedWorkspaceRoot))
536
+ throw new AppError("snapshot_workspace_restore_failed", "Restored workspace differs from the import target", 1, { workspace_root: workspaceRoot, expected_workspace_root: expectedWorkspaceRoot });
537
+ completeSnapshotImport(context.ddFlowHome, importId, { projectRoot, workspaceRoot: expectedWorkspaceRoot, sourceManifestSha256 });
538
+ result = {
539
+ ok: true,
540
+ schema_id: "dd-flow/eval-run-restore@1",
541
+ snapshot,
542
+ run_id: status.run.id,
543
+ project_root: projectRoot,
544
+ workspace_root: workspaceRoot,
545
+ dd_flow_home: context.ddFlowHome,
546
+ run_home: runHome,
547
+ target_stage: manifest.stage_entry,
548
+ ...(runtimeLineage ? { runtime_lineage_file: runtimeLineage } : {}),
549
+ ...(recovery ? { recovery_id: input.recoveryId, native_session_portability: "requires_adapter_verification", relocation_file: relocationFile, relocation } : {}),
550
+ next_command: recovery ? `dd-flow run recovery inspect ${status.run.id} --project-root ${JSON.stringify(projectRoot)} --json` : `dd-flow stage start ${status.run.id} --stage ${manifest.stage_entry} --project-root ${JSON.stringify(projectRoot)} --json`
551
+ };
512
552
  }
513
- const relocation = recovery ? {
514
- schema_id: "dd-flow/recovery-relocation@1", physical_runtime_id: crypto.randomUUID(), recovery_id: input.recoveryId, run_id: manifest.run_id,
515
- path_mapping: [{ from: manifest.dd_flow_home, to: context.ddFlowHome }, { from: sourceWorkspaceRoot(manifest), to: workspaceRoot }, { from: manifest.project_root, to: projectRoot }]
516
- } : null;
517
- const relocationFile = relocation ? path.join(context.ddFlowHome, "recovery-imports", `${relocation.physical_runtime_id}.json`) : null;
518
- if (relocationFile) {
519
- fs.mkdirSync(path.dirname(relocationFile), { recursive: true });
520
- writeJson(relocationFile, relocation);
553
+ catch (error) {
554
+ primary = error;
555
+ failed = true;
521
556
  }
522
- const status = getFlowRunStatus(restored, { projectRoot, runId: manifest.run_id });
523
- if (!recovery)
524
- assertStageEntry(status.index.stage_runs ?? [], manifest.stage_entry);
525
- else
526
- restored.db.run("UPDATE run_recovery_guards SET capture_path = ? WHERE recovery_id = ? AND status = 'sealed'", [snapshot, input.recoveryId]);
527
- if (!status.run.run_root)
528
- throw new AppError("runtime_missing", "Restored RUN has no artifact root", 1, { run_id: manifest.run_id });
529
- const runHome = status.run.run_root;
530
- const expectedWorkspaceRoot = restoredWorkspaceRoot(manifest, projectRoot, context.ddFlowHome);
531
- if (physicalPath(workspaceRoot) !== physicalPath(expectedWorkspaceRoot))
532
- throw new AppError("snapshot_workspace_restore_failed", "Restored workspace differs from the import target", 1, { workspace_root: workspaceRoot, expected_workspace_root: expectedWorkspaceRoot });
533
- completeSnapshotImport(context.ddFlowHome, importId, { projectRoot, workspaceRoot: expectedWorkspaceRoot, sourceManifestSha256 });
534
- result = {
535
- ok: true,
536
- schema_id: "dd-flow/eval-run-restore@1",
537
- snapshot,
538
- run_id: status.run.id,
539
- project_root: projectRoot,
540
- workspace_root: workspaceRoot,
541
- dd_flow_home: context.ddFlowHome,
542
- run_home: runHome,
543
- target_stage: manifest.stage_entry,
544
- ...(runtimeLineage ? { runtime_lineage_file: runtimeLineage } : {}),
545
- ...(recovery ? { recovery_id: input.recoveryId, native_session_portability: "requires_adapter_verification", relocation_file: relocationFile, relocation } : {}),
546
- next_command: recovery ? `dd-flow run recovery inspect ${status.run.id} --project-root ${JSON.stringify(projectRoot)} --json` : `dd-flow stage start ${status.run.id} --stage ${manifest.stage_entry} --project-root ${JSON.stringify(projectRoot)} --json`
547
- };
557
+ const cleanup = cleanupScopeWriters([{ name: restored.ddFlowHome, db: restored.db, rollback: false, close: "always" }]);
558
+ if (failed) {
559
+ if (primary instanceof AppError && cleanup.length)
560
+ primary.details.import_cleanup_errors = cleanup;
561
+ throw primary;
562
+ }
563
+ if (cleanup.length)
564
+ throw new AppError("snapshot_import_cleanup_failed", "Snapshot imported but its connection could not be closed", 1, { effect: "committed", import_complete: true, import_cleanup_errors: cleanup });
565
+ return result;
548
566
  }
549
567
  catch (error) {
550
- primary = error;
551
- failed = true;
552
- }
553
- const cleanup = cleanupScopeWriters([{ name: restored.ddFlowHome, db: restored.db, rollback: false, close: "always" }]);
554
- if (failed) {
555
- if (primary instanceof AppError && cleanup.length)
556
- primary.details.import_cleanup_errors = cleanup;
557
- throw primary;
568
+ // The import fence and target writes have already started. A nested
569
+ // prepare/read validator must not report this operation as no-effect.
570
+ if (error instanceof AppError)
571
+ Object.assign(error.details, { phase: "restore", effect: error.details.effect === "committed" ? "committed" : "partial", import_id: importId, next_action: "reconcile_snapshot_import" });
572
+ throw error;
558
573
  }
559
- if (cleanup.length)
560
- throw new AppError("snapshot_import_cleanup_failed", "Snapshot imported but its connection could not be closed", 1, { effect: "committed", import_complete: true, import_cleanup_errors: cleanup });
561
- return result;
562
574
  }
563
575
  export function prepareVnextSpecifyRun(context, input) {
564
576
  const projectRoot = resolveProjectRoot(input.projectRoot);
@@ -749,27 +761,29 @@ function rebaseStructuredValue(value, oldRoot, newRoot, oldWorkspace, newWorkspa
749
761
  function rebasePathString(value, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome) { for (const [oldPrefix, newPrefix] of [[oldHome, newHome], [oldWorkspace, newWorkspace], [oldRoot, newRoot]])
750
762
  if (value === oldPrefix || value.startsWith(`${oldPrefix}${path.sep}`))
751
763
  return `${newPrefix}${value.slice(oldPrefix.length)}`; return value; }
752
- function prepareSnapshotWorkspace(workspaceRoot, projectRoot) {
764
+ function prepareSnapshotWorkspace(workspaceRoot, projectRoot, allowUnborn = false) {
753
765
  if (samePath(workspaceRoot, projectRoot))
754
766
  return null;
755
767
  const branch = git(workspaceRoot, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
756
768
  if (!branch)
757
769
  throw new AppError("snapshot_workspace_invalid", "Feature workspace must be on a named Git branch", 1, { workspace_root: workspaceRoot });
758
770
  const baseRef = git(workspaceRoot, ["rev-parse", "HEAD"]);
759
- if (!baseRef)
771
+ if (!baseRef && !(allowUnborn && git(workspaceRoot, ['for-each-ref', '--format=%(refname)']) === ''))
760
772
  throw new AppError("snapshot_workspace_invalid", "Feature workspace has no readable HEAD", 1, { workspace_root: workspaceRoot });
761
773
  return { branch, baseRef };
762
774
  }
763
- function snapshotWorkspace(workspaceRoot, projectRoot, output) {
764
- const prepared = prepareSnapshotWorkspace(workspaceRoot, projectRoot);
775
+ function snapshotWorkspace(workspaceRoot, projectRoot, output, independent = false) {
776
+ const prepared = prepareSnapshotWorkspace(workspaceRoot, projectRoot, independent);
765
777
  if (!prepared)
766
778
  return { kind: "project" };
767
779
  const { branch, baseRef } = prepared;
768
780
  const workspace = path.join(output, "workspace");
769
781
  const bundle = path.join(output, "workspace.bundle");
770
782
  fs.cpSync(workspaceRoot, workspace, { recursive: true, verbatimSymlinks: true, filter: projectPayloadFilter(workspaceRoot) });
771
- runGit(projectRoot, ["bundle", "create", bundle, branch]);
772
- return { kind: "git_worktree", source_root: workspaceRoot, branch, base_ref: baseRef, sha256: treeHash(workspace), bundle_sha256: fileHash(bundle), ...snapshotIndex(workspaceRoot, output, "workspace") };
783
+ if (baseRef === null)
784
+ return { kind: 'git_repository', source_root: workspaceRoot, branch, base_ref: null, bundle_sha256: null, state: 'unborn', sha256: treeHash(workspace), ...snapshotIndex(workspaceRoot, output, 'workspace') };
785
+ runGit(independent ? workspaceRoot : projectRoot, ["bundle", "create", bundle, branch]);
786
+ return { kind: independent ? "git_repository" : "git_worktree", source_root: workspaceRoot, branch, base_ref: baseRef, sha256: treeHash(workspace), bundle_sha256: fileHash(bundle), ...snapshotIndex(workspaceRoot, output, "workspace") };
773
787
  }
774
788
  function restoreWorkspace(snapshot, manifest, projectRoot, ddFlowHome) {
775
789
  if (manifest.workspace.kind === "project")
@@ -778,15 +792,28 @@ function restoreWorkspace(snapshot, manifest, projectRoot, ddFlowHome) {
778
792
  const bundle = path.join(snapshot, "workspace.bundle");
779
793
  if (treeHash(source) !== manifest.workspace.sha256)
780
794
  throw new AppError("snapshot_checksum_mismatch", "Snapshot workspace checksum does not match manifest", 1, { snapshot });
781
- if (!fs.existsSync(bundle) || fileHash(bundle) !== manifest.workspace.bundle_sha256)
795
+ if (manifest.workspace.state === 'unborn' ? fs.existsSync(bundle) : !fs.existsSync(bundle) || fileHash(bundle) !== manifest.workspace.bundle_sha256)
782
796
  throw new AppError("snapshot_checksum_mismatch", "Snapshot workspace Git bundle does not match manifest", 1, { snapshot });
783
797
  const workspaceRoot = restoredWorkspaceRoot(manifest, projectRoot, ddFlowHome);
784
798
  if (fs.existsSync(workspaceRoot))
785
799
  throw new AppError("snapshot_workspace_target_exists", "Snapshot feature workspace target already exists", 1, { workspace_root: workspaceRoot });
786
- runGit(projectRoot, ["fetch", bundle, `+refs/heads/${manifest.workspace.branch}:refs/heads/${manifest.workspace.branch}`]);
787
- if (git(projectRoot, ["rev-parse", `refs/heads/${manifest.workspace.branch}`]) !== manifest.workspace.base_ref)
788
- throw new AppError("snapshot_workspace_restore_failed", "Snapshot Git bundle did not restore the frozen feature base", 1, { branch: manifest.workspace.branch, expected_base: manifest.workspace.base_ref });
789
- runGit(projectRoot, ["worktree", "add", workspaceRoot, manifest.workspace.branch]);
800
+ if (manifest.workspace.kind === 'git_repository') {
801
+ fs.mkdirSync(path.dirname(workspaceRoot), { recursive: true });
802
+ if (manifest.workspace.state === 'unborn') {
803
+ fs.mkdirSync(workspaceRoot);
804
+ runGit(workspaceRoot, ['-c', 'init.templateDir=', 'init', '--initial-branch', manifest.workspace.branch]);
805
+ }
806
+ else
807
+ runGit(projectRoot, ['clone', '--no-checkout', '--branch', manifest.workspace.branch, bundle, workspaceRoot]);
808
+ }
809
+ else {
810
+ runGit(projectRoot, ["fetch", bundle, `+refs/heads/${manifest.workspace.branch}:refs/heads/${manifest.workspace.branch}`]);
811
+ if (git(projectRoot, ["rev-parse", `refs/heads/${manifest.workspace.branch}`]) !== manifest.workspace.base_ref)
812
+ throw new AppError("snapshot_workspace_restore_failed", "Snapshot Git bundle did not restore the frozen feature base", 1, { branch: manifest.workspace.branch, expected_base: manifest.workspace.base_ref });
813
+ runGit(projectRoot, ["worktree", "add", workspaceRoot, manifest.workspace.branch]);
814
+ }
815
+ if (git(workspaceRoot, ["rev-parse", "HEAD"]) !== manifest.workspace.base_ref)
816
+ throw new AppError("snapshot_workspace_restore_failed", "Snapshot Git bundle did not restore the frozen workspace base", 1, { branch: manifest.workspace.branch, expected_base: manifest.workspace.base_ref });
790
817
  replaceProjectTree(workspaceRoot, source);
791
818
  restoreIndex(snapshot, "workspace", manifest.workspace, workspaceRoot);
792
819
  return fs.realpathSync(workspaceRoot);
@@ -836,7 +863,7 @@ function restoreProjectGit(snapshot, source, projectRoot) {
836
863
  restoreIndex(snapshot, "project", source, projectRoot);
837
864
  }
838
865
  const mergeStateFiles = ["MERGE_HEAD", "MERGE_MSG", "MERGE_MODE", "ORIG_HEAD", "AUTO_MERGE"];
839
- function indexEntries(root) {
866
+ function indexEntries(root, includeExclude = true) {
840
867
  const entries = Buffer.from(gitBytes(root, ["ls-files", "--stage", "-z"])
841
868
  .toString("utf8").split("\0").filter(entry => entry && localEnvironmentAllowed(entry.slice(entry.indexOf("\t") + 1)))
842
869
  .map(entry => `${entry}\0`).join(""));
@@ -849,7 +876,22 @@ function indexEntries(root) {
849
876
  if (fs.existsSync(resolved))
850
877
  merge[name] = fs.readFileSync(resolved).toString("base64");
851
878
  }
852
- return Buffer.from(JSON.stringify({ entries: entries.toString("base64"), merge }));
879
+ if (!includeExclude)
880
+ return Buffer.from(JSON.stringify({ entries: entries.toString("base64"), merge }));
881
+ const excludePath = git(root, ["rev-parse", "--git-path", "info/exclude"]);
882
+ if (!excludePath)
883
+ throw new AppError("snapshot_index_invalid", "Could not locate Git local excludes", 1);
884
+ const excludeFile = path.resolve(root, excludePath);
885
+ let exclude = null;
886
+ if (fs.existsSync(excludeFile)) {
887
+ const stat = fs.lstatSync(excludeFile);
888
+ if (!stat.isFile() || stat.size > 1024 * 1024)
889
+ throw new AppError("snapshot_index_invalid", "Git local excludes must be a bounded regular file", 1);
890
+ exclude = fs.readFileSync(excludeFile).toString("base64");
891
+ }
892
+ // Local excludes affect review-visible inputs, but are not part of a Git
893
+ // bundle/index. Keep them in the existing hashed, non-executable Git payload.
894
+ return Buffer.from(JSON.stringify({ entries: entries.toString("base64"), merge, exclude }));
853
895
  }
854
896
  function snapshotIndex(root, output, name) {
855
897
  const bytes = indexEntries(root);
@@ -900,6 +942,8 @@ function restoreIndex(snapshot, name, metadata, root) {
900
942
  if (!metadata.index_objects_sha256 || fileHash(pack) !== metadata.index_objects_sha256)
901
943
  throw new AppError("snapshot_checksum_mismatch", "Snapshot Git index object checksum does not match", 1);
902
944
  const value = JSON.parse(fs.readFileSync(file, "utf8"));
945
+ if (value.exclude !== undefined && value.exclude !== null && (typeof value.exclude !== "string" || Buffer.byteLength(value.exclude) > 1400000))
946
+ throw new AppError("snapshot_index_invalid", "Snapshot Git local excludes are invalid", 1);
903
947
  gitBytes(root, ["unpack-objects"], fs.readFileSync(pack));
904
948
  runGit(root, ["read-tree", "--empty"]);
905
949
  gitBytes(root, ["update-index", "-z", "--index-info"], Buffer.from(value.entries, "base64"));
@@ -912,18 +956,36 @@ function restoreIndex(snapshot, name, metadata, root) {
912
956
  else
913
957
  fs.rmSync(path.resolve(root, target), { force: true });
914
958
  }
959
+ if (value.exclude !== undefined) {
960
+ const target = git(root, ["rev-parse", "--git-path", "info/exclude"]);
961
+ if (!target)
962
+ throw new AppError("snapshot_index_invalid", "Could not locate restored Git local excludes", 1);
963
+ const destination = path.resolve(root, target);
964
+ if (fs.existsSync(destination) && !fs.lstatSync(destination).isFile())
965
+ throw new AppError("snapshot_index_invalid", "Restored Git local excludes are not a regular file", 1);
966
+ if (value.exclude === null)
967
+ fs.rmSync(destination, { force: true });
968
+ else {
969
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
970
+ fs.writeFileSync(destination, Buffer.from(value.exclude, "base64"));
971
+ }
972
+ }
915
973
  return;
916
974
  }
917
975
  if (fs.statSync(file).size)
918
976
  runGit(root, ["apply", "--cached", "--binary", file]);
919
977
  }
978
+ function capturedIndexEntries(snapshot, name, root) {
979
+ const captured = JSON.parse(fs.readFileSync(path.join(snapshot, `${name}.index.patch`), "utf8"));
980
+ return indexEntries(root, Object.hasOwn(captured, "exclude"));
981
+ }
920
982
  export function verifyRecoverySnapshot(snapshot, input, checkWorkingState = true) {
921
983
  const manifest = readManifest(snapshot);
922
984
  if (manifest.purpose !== "recovery" || manifest.recovery_id !== input.recoveryId || manifest.run_id !== input.runId)
923
985
  throw new AppError("recovery_capture_invalid", "Snapshot does not identify this recovery", 1);
924
986
  const entries = [
925
987
  { name: "project", root: input.projectRoot, hash: manifest.project_sha256, git: manifest.project_git, head: manifest.project_git.head },
926
- ...(manifest.workspace.kind === "git_worktree" ? [{ name: "workspace", root: input.workspaceRoot, hash: manifest.workspace.sha256, git: manifest.workspace, head: manifest.workspace.base_ref }] : [])
988
+ ...(manifest.workspace.kind !== "project" ? [{ name: "workspace", root: input.workspaceRoot, hash: manifest.workspace.sha256, git: manifest.workspace, head: manifest.workspace.base_ref }] : [])
927
989
  ];
928
990
  verifySnapshotPayload(snapshot, manifest);
929
991
  for (const entry of entries) {
@@ -933,7 +995,7 @@ export function verifyRecoverySnapshot(snapshot, input, checkWorkingState = true
933
995
  throw new AppError("recovery_workspace_drift", "Executable file modes changed after recovery capture", 1, { payload: entry.name });
934
996
  if (checkWorkingState && (treeHash(entry.root, allowed) !== entry.hash || git(entry.root, ["rev-parse", "--verify", "HEAD"]) !== entry.head
935
997
  || entry.head === null && git(entry.root, ["for-each-ref", "--format=%(refname)"]) !== ""
936
- || git(entry.root, ["symbolic-ref", "--quiet", "--short", "HEAD"]) !== entry.git.branch || (entry.git.index_sha256 && crypto.createHash("sha256").update(entry.git.index_format === "entries@1" ? indexEntries(entry.root) : indexPatch(entry.root)).digest("hex") !== entry.git.index_sha256)))
998
+ || git(entry.root, ["symbolic-ref", "--quiet", "--short", "HEAD"]) !== entry.git.branch || (entry.git.index_sha256 && crypto.createHash("sha256").update(entry.git.index_format === "entries@1" ? capturedIndexEntries(snapshot, entry.name, entry.root) : indexPatch(entry.root)).digest("hex") !== entry.git.index_sha256)))
937
999
  throw new AppError("recovery_workspace_drift", "Working files or Git state changed after recovery capture", 1, { payload: entry.name });
938
1000
  }
939
1001
  }
@@ -953,7 +1015,7 @@ function verifySnapshotPayload(snapshot, manifest) {
953
1015
  if (treeHash(path.join(snapshot, "runtime")) !== manifest.runtime_sha256)
954
1016
  throw new AppError("snapshot_checksum_mismatch", "Snapshot runtime checksum does not match", 1);
955
1017
  verifySnapshotModes(snapshot, manifest);
956
- const payloads = [{ name: "project", hash: manifest.project_sha256, git: manifest.project_git }, ...(manifest.workspace.kind === "git_worktree" ? [{ name: "workspace", hash: manifest.workspace.sha256, git: manifest.workspace }] : [])];
1018
+ const payloads = [{ name: "project", hash: manifest.project_sha256, git: manifest.project_git }, ...(manifest.workspace.kind !== "project" ? [{ name: "workspace", hash: manifest.workspace.sha256, git: manifest.workspace }] : [])];
957
1019
  for (const payload of payloads)
958
1020
  verifyGitSnapshotPayload(snapshot, payload);
959
1021
  if (manifest.schema_id === "dd-flow/eval-run-snapshot@6") {
@@ -1422,7 +1484,7 @@ function treeModesHash(root, filtered = false) {
1422
1484
  function verifySnapshotModes(snapshot, manifest) {
1423
1485
  if (!manifest.payload_modes)
1424
1486
  return; // Legacy snapshots did not attest modes.
1425
- for (const name of ["project", "runtime", ...(manifest.workspace.kind === "git_worktree" ? ["workspace"] : [])]) {
1487
+ for (const name of ["project", "runtime", ...(manifest.workspace.kind !== "project" ? ["workspace"] : [])]) {
1426
1488
  if (treeModesHash(path.join(snapshot, name)) !== manifest.payload_modes[name])
1427
1489
  throw new AppError("snapshot_checksum_mismatch", "Snapshot executable modes do not match manifest", 1, { payload: name });
1428
1490
  }
@@ -1454,7 +1516,7 @@ function readManifest(snapshot) {
1454
1516
  if (!fs.existsSync(file))
1455
1517
  throw new AppError("snapshot_missing", "Snapshot manifest is missing", 1, { snapshot });
1456
1518
  const value = JSON.parse(fs.readFileSync(file, "utf8"));
1457
- if (!["dd-flow/eval-run-snapshot@5", "dd-flow/eval-run-snapshot@6"].includes(String(value.schema_id)) || !value.run_id || !value.project_root || !value.dd_flow_home || typeof value.purpose !== "string" || !["stage_entry", "candidate", "incomplete", "recovery"].includes(value.purpose) || (value.purpose === "stage_entry" && !value.stage_entry) || (value.purpose !== "stage_entry" && value.stage_entry !== null) || !value.runtime_sha256 || !value.project_sha256 || !value.project_git || !value.workspace)
1519
+ if (!["dd-flow/eval-run-snapshot@5", "dd-flow/eval-run-snapshot@6", "dd-flow/eval-run-snapshot@7"].includes(String(value.schema_id)) || !value.run_id || !value.project_root || !value.dd_flow_home || typeof value.purpose !== "string" || !["stage_entry", "candidate", "incomplete", "recovery"].includes(value.purpose) || (value.purpose === "stage_entry" && !value.stage_entry) || (value.purpose !== "stage_entry" && value.stage_entry !== null) || !value.runtime_sha256 || !value.project_sha256 || !value.project_git || !value.workspace)
1458
1520
  throw new AppError("snapshot_invalid", "Snapshot manifest is invalid", 1, { snapshot });
1459
1521
  if (value.runtime_payload_policy !== undefined && (value.runtime_payload_policy !== "flow-evidence@1" || value.purpose === "recovery"))
1460
1522
  throw new AppError("snapshot_invalid", "Snapshot runtime payload policy is invalid for its purpose", 1, { snapshot });
@@ -1469,8 +1531,13 @@ function readManifest(snapshot) {
1469
1531
  || spawnSync("git", ["check-ref-format", "--branch", value.project_git.branch], { encoding: "utf8" }).status !== 0
1470
1532
  : value.project_git.state !== undefined || !value.project_git.head || !value.project_git.bundle_sha256))
1471
1533
  throw new AppError("snapshot_invalid", "Snapshot project Git metadata is incomplete", 1, { snapshot });
1472
- if (value.workspace.kind === "git_worktree" && (!value.workspace.branch || !value.workspace.base_ref || !value.workspace.bundle_sha256))
1473
- throw new AppError("snapshot_invalid", "Snapshot worktree manifest is incomplete", 1, { snapshot });
1534
+ if (!['project', 'git_worktree', 'git_repository'].includes(value.workspace.kind)
1535
+ || (value.schema_id === 'dd-flow/eval-run-snapshot@7') !== (value.workspace.kind === 'git_repository')
1536
+ || value.workspace.kind !== 'project' && (!value.workspace.branch || (value.workspace.state === 'unborn'
1537
+ ? value.workspace.kind !== 'git_repository' || value.workspace.base_ref !== null || value.workspace.bundle_sha256 !== null
1538
+ || value.workspace.index_format !== 'entries@1' || !value.workspace.index_sha256 || !value.workspace.index_objects_sha256
1539
+ : value.workspace.state !== undefined || !value.workspace.base_ref || !value.workspace.bundle_sha256)))
1540
+ throw new AppError("snapshot_invalid", "Snapshot workspace manifest is incomplete or incompatible", 1, { snapshot });
1474
1541
  return value;
1475
1542
  }
1476
1543
  function readBootstrapManifest(snapshot) {
@@ -1479,7 +1546,7 @@ function readBootstrapManifest(snapshot) {
1479
1546
  throw new AppError("snapshot_missing", "Bootstrap snapshot manifest is missing", 1, { snapshot });
1480
1547
  const value = JSON.parse(fs.readFileSync(file, "utf8"));
1481
1548
  const validBoundary = value.schema_id === "dd-eval/bootstrap-snapshot@1" ? value.stage === "specify" && value.flow === undefined
1482
- : value.schema_id === "dd-eval/bootstrap-snapshot@2" && value.flow?.id === "mb-init" && value.stage === "preflight"
1549
+ : value.schema_id === "dd-eval/bootstrap-snapshot@2" && (value.flow?.id === "mb-init" || value.flow?.id === "mb-upgrade" || isDiagnosticFlow(value.flow?.id)) && value.stage === "preflight"
1483
1550
  && Object.keys(value.flow).sort().join() === "descriptor_sha256,id,source_sha256"
1484
1551
  && /^[a-f0-9]{64}$/.test(value.flow.source_sha256) && /^[a-f0-9]{64}$/.test(value.flow.descriptor_sha256);
1485
1552
  if (!validBoundary || !value.project_root || !value.project_sha256 || !value.project_git?.branch || !value.project_git.head || !value.project_git.bundle_sha256)
@@ -9,6 +9,7 @@ import { FrozenGenericExecution } from "./generic-execution.js";
9
9
  import { hybridDigest, validateHybridContract, validateHybridShape } from "./hybrid-contracts.js";
10
10
  import { readBoundedReleaseFile } from "./release-source.js";
11
11
  import { acquireInstallationLock } from "./installation-lock.js";
12
+ import { projectPacketLocators } from "./stage-context.js";
12
13
  const hash = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
13
14
  const limit = 1024 * 1024;
14
15
  function refuse(reason, issues) {
@@ -54,7 +55,8 @@ export class RetainedExecutionBundle {
54
55
  #blobs;
55
56
  #admission;
56
57
  #engine;
57
- constructor(bytes, manifest, blobs, admission, descriptor, execution) {
58
+ #engineRelocation;
59
+ constructor(bytes, manifest, blobs, admission, descriptor, execution, engineRelocation) {
58
60
  frozen(manifest);
59
61
  this.manifest = manifest;
60
62
  this.sha256 = hash(bytes);
@@ -63,9 +65,10 @@ export class RetainedExecutionBundle {
63
65
  this.#admission = admission;
64
66
  this.descriptor = descriptor;
65
67
  this.execution = execution;
68
+ this.#engineRelocation = engineRelocation && structuredClone(engineRelocation);
66
69
  Object.freeze(this);
67
70
  }
68
- static admit(input, expectedSha256, sourceBlobs, selected) {
71
+ static admit(input, expectedSha256, sourceBlobs, selected, engineRelocation) {
69
72
  if (input.byteLength > limit || !/^[a-f0-9]{64}$/.test(expectedSha256))
70
73
  refuse("invalid_bundle_identity");
71
74
  const bytes = Buffer.from(input);
@@ -113,12 +116,12 @@ export class RetainedExecutionBundle {
113
116
  const snapshot = { schema_id: "dd-flow/frozen-execution@1", scope: admission.executionScope,
114
117
  settings: json(blobs.get(manifest.execution_settings.path)), profiles: profiles.profiles };
115
118
  const execution = FrozenGenericExecution.restore(snapshot, hybridDigest(snapshot), descriptor, admission.executionScope, admission.receipts);
116
- const admitted = new RetainedExecutionBundle(bytes, manifest, blobs, admission, descriptor, execution);
119
+ const admitted = new RetainedExecutionBundle(bytes, manifest, blobs, admission, descriptor, execution, engineRelocation);
117
120
  admitted.engine();
118
121
  return admitted;
119
122
  }
120
123
  /** Read only the retained inventory. Mutable canon/profile sources are never consulted. */
121
- static read(root, expectedSha256, admission) {
124
+ static read(root, expectedSha256, admission, engineRelocation) {
122
125
  try {
123
126
  directory(root);
124
127
  const bytes = readBoundedReleaseFile(path.join(root, "manifest.json"), limit);
@@ -130,7 +133,7 @@ export class RetainedExecutionBundle {
130
133
  const manifest = value, blobs = new Map();
131
134
  for (const ref of manifest.inventory)
132
135
  blobs.set(ref.path, readBoundedReleaseFile(fileIn(root, ref.path), limit));
133
- return RetainedExecutionBundle.admit(bytes, expectedSha256, blobs, admission);
136
+ return RetainedExecutionBundle.admit(bytes, expectedSha256, blobs, admission, engineRelocation);
134
137
  }
135
138
  catch (error) {
136
139
  if (error instanceof AppError && ["execution_bundle_invalid", "frozen_engine_invalid"].includes(error.code))
@@ -153,8 +156,17 @@ export class RetainedExecutionBundle {
153
156
  }
154
157
  #admitEngine() {
155
158
  const binding = json(this.blob(this.manifest.engine_binding.path));
156
- const engine = AdmittedFrozenEngine.admit(binding, this.manifest.engine_manifest.sha256);
157
- if (!engine.manifestBytes().equals(this.blob(this.manifest.engine_manifest.path)))
159
+ const retainedManifest = this.blob(this.manifest.engine_manifest.path), relocation = this.#engineRelocation;
160
+ if (relocation) {
161
+ const root = JSON.parse(projectPacketLocators(JSON.stringify({ root: binding.engine.snapshot_root }), relocation.relocations)).root;
162
+ if (!relocation.relocations.length || root === binding.engine.snapshot_root || root !== relocation.binding.engine.snapshot_root
163
+ || relocation.binding.run_id !== this.manifest.run_id)
164
+ refuse("foreign_engine_relocation");
165
+ }
166
+ const engine = relocation
167
+ ? AdmittedFrozenEngine.admitRelocation(binding, retainedManifest, this.manifest.engine_manifest.sha256, relocation.binding)
168
+ : AdmittedFrozenEngine.admit(binding, this.manifest.engine_manifest.sha256);
169
+ if (!relocation && !engine.manifestBytes().equals(retainedManifest))
158
170
  refuse("engine_manifest_bytes_changed");
159
171
  const manifest = engine.manifest(), contracts = new Set(Object.values(manifest.supports_contracts).flat());
160
172
  for (const id of ["dd-flow/flow-descriptor@1", "dd-flow/execution-settings@1", "dd-flow/execution-bundle@1"]) {