@deksden-com/dd-flow-cli 0.9.0-beta.16 → 0.9.0-beta.160

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 (670) hide show
  1. package/CHANGELOG.md +959 -0
  2. package/README.md +139 -10
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/argument-values.js +38 -0
  5. package/dist/cli/command-inputs.js +417 -0
  6. package/dist/cli/command-metadata.js +303 -0
  7. package/dist/cli/command-option-descriptions.js +192 -0
  8. package/dist/cli/completion-cli.js +70 -0
  9. package/dist/cli/completion.js +57 -0
  10. package/dist/cli/delivery.js +108 -0
  11. package/dist/cli/diagnostic-invocation.js +62 -0
  12. package/dist/cli/diagnostic.js +131 -0
  13. package/dist/cli/error-output.js +62 -0
  14. package/dist/cli/executor-onboarding.js +76 -0
  15. package/dist/cli/flows.js +29 -0
  16. package/dist/cli/harnesses.js +84 -0
  17. package/dist/cli/help.js +521 -19
  18. package/dist/cli/hook-ingress.js +89 -0
  19. package/dist/cli/input-preparation.js +193 -0
  20. package/dist/cli/interactive-invocation.js +33 -0
  21. package/dist/cli/interactive.js +114 -0
  22. package/dist/cli/memory-fix-invocation.js +52 -0
  23. package/dist/cli/memory-fix.js +100 -0
  24. package/dist/cli/memory-flow-admission.js +54 -0
  25. package/dist/cli/memory-init-invocation.js +85 -0
  26. package/dist/cli/memory-init.js +250 -0
  27. package/dist/cli/memory-storage-admission.js +54 -0
  28. package/dist/cli/memory-storage-qualification.js +105 -0
  29. package/dist/cli/memory-storage-transition.js +123 -0
  30. package/dist/cli/memory-upgrade.js +189 -0
  31. package/dist/cli/models.js +39 -0
  32. package/dist/cli/native-hook-ingress.js +94 -0
  33. package/dist/cli/native-hook-preparation.js +82 -0
  34. package/dist/cli/operator-profiles.js +61 -0
  35. package/dist/cli/output-options.js +35 -0
  36. package/dist/cli/response-file.js +72 -0
  37. package/dist/cli/run-cli.js +2020 -555
  38. package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
  39. package/dist/cli/runtime-maintenance-ingress.js +52 -0
  40. package/dist/cli/runtime-process.js +153 -0
  41. package/dist/cli/setup.js +150 -0
  42. package/dist/cli.js +103 -2
  43. package/dist/domain/delivery-flows.js +8 -0
  44. package/dist/domain/diagnostic-aspects.js +103 -0
  45. package/dist/domain/diagnostic-flows.js +8 -0
  46. package/dist/domain/distill-flow.js +20 -0
  47. package/dist/domain/interactive-flow.js +1 -0
  48. package/dist/domain/memory-fix-flow.js +2 -0
  49. package/dist/domain/review-fix-flow.js +2 -0
  50. package/dist/domain/source-snapshot-flows.js +4 -0
  51. package/dist/domain/stage-catalog.js +1 -1
  52. package/dist/domain/stage-handler-identities.js +28 -0
  53. package/dist/harness-runtime/DD-ZCODE.md +112 -0
  54. package/dist/harness-runtime/OPENCODE-LICENSE.txt +24 -0
  55. package/dist/harness-runtime/bin/dd-agy.mjs +71 -0
  56. package/dist/harness-runtime/bin/dd-codex.mjs +27 -0
  57. package/dist/harness-runtime/bin/dd-droid.mjs +34 -0
  58. package/dist/harness-runtime/bin/dd-grok.mjs +50 -0
  59. package/dist/harness-runtime/bin/dd-opencode.mjs +22 -0
  60. package/dist/harness-runtime/bin/dd-zcode.mjs +95 -0
  61. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  62. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  63. package/dist/harness-runtime/lib/agy-error-boundary.mjs +87 -0
  64. package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
  65. package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
  66. package/dist/harness-runtime/lib/agy-native-inspection.mjs +225 -0
  67. package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
  68. package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
  69. package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +74 -0
  70. package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
  71. package/dist/harness-runtime/lib/daemon-observation.d.mts +3 -0
  72. package/dist/harness-runtime/lib/daemon-observation.mjs +171 -0
  73. package/dist/harness-runtime/lib/daemon-operations.d.mts +2 -0
  74. package/dist/harness-runtime/lib/daemon-operations.mjs +512 -0
  75. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +7 -0
  76. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +1362 -0
  77. package/dist/harness-runtime/lib/dd-agy.mjs +105 -0
  78. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  79. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +271 -0
  80. package/dist/harness-runtime/lib/dd-codex.d.mts +1 -0
  81. package/dist/harness-runtime/lib/dd-codex.mjs +986 -0
  82. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +155 -0
  83. package/dist/harness-runtime/lib/dd-droid.mjs +739 -0
  84. package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
  85. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +667 -0
  86. package/dist/harness-runtime/lib/dd-grok.mjs +182 -0
  87. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +772 -0
  88. package/dist/harness-runtime/lib/dd-opencode.mjs +411 -0
  89. package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
  90. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +845 -0
  91. package/dist/harness-runtime/lib/dd-zcode.mjs +1239 -0
  92. package/dist/harness-runtime/lib/delegation-instructions.d.mts +16 -0
  93. package/dist/harness-runtime/lib/delegation-instructions.mjs +179 -0
  94. package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
  95. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  96. package/dist/harness-runtime/lib/driver-recovery.mjs +121 -0
  97. package/dist/harness-runtime/lib/droid-observation.mjs +70 -0
  98. package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
  99. package/dist/harness-runtime/lib/entity-references.mjs +9 -0
  100. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  101. package/dist/harness-runtime/lib/lease-renewal.mjs +37 -0
  102. package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
  103. package/dist/harness-runtime/lib/lifecycle-command.mjs +273 -0
  104. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +11 -0
  105. package/dist/harness-runtime/lib/lifecycle-contract.mjs +20 -0
  106. package/dist/harness-runtime/lib/managed-daemon.d.mts +16 -0
  107. package/dist/harness-runtime/lib/managed-daemon.mjs +698 -0
  108. package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
  109. package/dist/harness-runtime/lib/model-observations.mjs +113 -0
  110. package/dist/harness-runtime/lib/native-children.d.mts +34 -0
  111. package/dist/harness-runtime/lib/native-children.mjs +265 -0
  112. package/dist/harness-runtime/lib/native-hook-command.d.mts +8 -0
  113. package/dist/harness-runtime/lib/native-hook-command.mjs +306 -0
  114. package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
  115. package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
  116. package/dist/harness-runtime/lib/native-streams.mjs +38 -0
  117. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  118. package/dist/harness-runtime/lib/observation-clock.mjs +95 -0
  119. package/dist/harness-runtime/lib/opencode-plugin.mjs +112 -0
  120. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  121. package/dist/harness-runtime/lib/operation-errors.d.mts +16 -0
  122. package/dist/harness-runtime/lib/operation-errors.mjs +140 -0
  123. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  124. package/dist/harness-runtime/lib/operation-progress.mjs +118 -0
  125. package/dist/harness-runtime/lib/process-json.d.mts +5 -0
  126. package/dist/harness-runtime/lib/process-json.mjs +200 -0
  127. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  128. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +23 -0
  129. package/dist/harness-runtime/lib/provider-overload-policy.mjs +242 -0
  130. package/dist/harness-runtime/lib/runner-events.mjs +202 -0
  131. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  132. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  133. package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
  134. package/dist/harness-runtime/lib/setup-progress.mjs +53 -0
  135. package/dist/harness-runtime/lib/tool-observations.d.mts +21 -0
  136. package/dist/harness-runtime/lib/tool-observations.mjs +238 -0
  137. package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
  138. package/dist/harness-runtime/lib/zcode-usage.mjs +55 -0
  139. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-1y4a697q.js +81 -0
  140. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-2ny4vxct.js +92 -0
  141. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-6mptkzbc.js +0 -0
  142. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-7x67cf1q.js +52 -0
  143. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9rqn6x4v.js +16 -0
  144. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9vdz0zwn.js +1661 -0
  145. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ecs5ftka.js +1466 -0
  146. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-frbwqjmf.js +145 -0
  147. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gne7h7mq.js +50 -0
  148. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gx9wnkzr.js +105 -0
  149. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-m2eywc8w.js +0 -0
  150. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-nwcpakzx.js +12 -0
  151. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-q4g3afnw.js +623 -0
  152. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-qav6mfdz.js +0 -0
  153. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-sxt88q69.js +12 -0
  154. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-t1xa31vm.js +39 -0
  155. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ve4aws36.js +103 -0
  156. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vmytr4q8.js +4 -0
  157. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vt9jbch8.js +36 -0
  158. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vyas3vgt.js +0 -0
  159. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-wjyeq25y.js +30 -0
  160. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-xgnj2w4v.js +20 -0
  161. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-y2etg5bs.js +9 -0
  162. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ydhz1zrh.js +238 -0
  163. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-z6qdwcsf.js +62 -0
  164. package/dist/harness-runtime/vendor/opencode-client/contract.d.ts +1 -0
  165. package/dist/harness-runtime/vendor/opencode-client/contract.js +12 -0
  166. package/dist/harness-runtime/vendor/opencode-client/effect/api/api.d.ts +2806 -0
  167. package/dist/harness-runtime/vendor/opencode-client/effect/api/api.js +0 -0
  168. package/dist/harness-runtime/vendor/opencode-client/effect/api.d.ts +4 -0
  169. package/dist/harness-runtime/vendor/opencode-client/effect/api.js +0 -0
  170. package/dist/harness-runtime/vendor/opencode-client/effect/client.d.ts +2530 -0
  171. package/dist/harness-runtime/vendor/opencode-client/effect/client.js +15 -0
  172. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.d.ts +7 -0
  173. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.js +6 -0
  174. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.d.ts +2529 -0
  175. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.js +8 -0
  176. package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.d.ts +2 -0
  177. package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.js +12 -0
  178. package/dist/harness-runtime/vendor/opencode-client/effect/index.d.ts +35 -0
  179. package/dist/harness-runtime/vendor/opencode-client/effect/index.js +75 -0
  180. package/dist/harness-runtime/vendor/opencode-client/effect/rpc.d.ts +20 -0
  181. package/dist/harness-runtime/vendor/opencode-client/effect/rpc.js +11 -0
  182. package/dist/harness-runtime/vendor/opencode-client/effect/service.d.ts +90 -0
  183. package/dist/harness-runtime/vendor/opencode-client/effect/service.js +187 -0
  184. package/dist/harness-runtime/vendor/opencode-client/package.json +84 -0
  185. package/dist/harness-runtime/vendor/opencode-client/promise/api.d.ts +21 -0
  186. package/dist/harness-runtime/vendor/opencode-client/promise/api.js +0 -0
  187. package/dist/harness-runtime/vendor/opencode-client/promise/client.d.ts +252 -0
  188. package/dist/harness-runtime/vendor/opencode-client/promise/client.js +14 -0
  189. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.d.ts +8 -0
  190. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.js +6 -0
  191. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.d.ts +259 -0
  192. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.js +7 -0
  193. package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.d.ts +3 -0
  194. package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.js +79 -0
  195. package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.d.ts +8932 -0
  196. package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.js +70 -0
  197. package/dist/harness-runtime/vendor/opencode-client/promise/index.d.ts +6 -0
  198. package/dist/harness-runtime/vendor/opencode-client/promise/index.js +83 -0
  199. package/dist/harness-runtime/vendor/opencode-client/promise/rpc.d.ts +32 -0
  200. package/dist/harness-runtime/vendor/opencode-client/promise/rpc.js +7 -0
  201. package/dist/harness-runtime/vendor/opencode-client/promise/service.d.ts +24 -0
  202. package/dist/harness-runtime/vendor/opencode-client/promise/service.js +187 -0
  203. package/dist/harness-runtime/vendor/opencode-client/pty-handoff.d.ts +12 -0
  204. package/dist/harness-runtime/vendor/opencode-client/pty-handoff.js +14 -0
  205. package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.d.ts +20 -0
  206. package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.js +14 -0
  207. package/dist/harness-runtime/vendor/opencode-client/service-contender.d.ts +37 -0
  208. package/dist/harness-runtime/vendor/opencode-client/service-contender.js +10 -0
  209. package/dist/harness-runtime/vendor/opencode-client/service-probe.d.ts +70 -0
  210. package/dist/harness-runtime/vendor/opencode-client/service-probe.js +16 -0
  211. package/dist/harness-runtime/vendor/opencode-client/service-timing.d.ts +12 -0
  212. package/dist/harness-runtime/vendor/opencode-client/service-timing.js +10 -0
  213. package/dist/harness-runtime/vendor/opencode-client/service-version.d.ts +2 -0
  214. package/dist/harness-runtime/vendor/opencode-client/service-version.js +6 -0
  215. package/dist/harness-runtime/vendor/opencode-client/service.d.ts +52 -0
  216. package/dist/harness-runtime/vendor/opencode-client/service.js +1 -0
  217. package/dist/harness-runtime/vendor/opencode-client/shared-events.d.ts +11 -0
  218. package/dist/harness-runtime/vendor/opencode-client/shared-events.js +8 -0
  219. package/dist/harness-runtime/vendor/opencode-client/solid/connection.d.ts +37 -0
  220. package/dist/harness-runtime/vendor/opencode-client/solid/connection.js +10 -0
  221. package/dist/harness-runtime/vendor/opencode-client/solid/data.d.ts +211 -0
  222. package/dist/harness-runtime/vendor/opencode-client/solid/data.js +18 -0
  223. package/dist/harness-runtime/vendor/opencode-client/solid/index.d.ts +3 -0
  224. package/dist/harness-runtime/vendor/opencode-client/solid/index.js +32 -0
  225. package/dist/harness-runtime/vendor/opencode-client/solid/pty.d.ts +22 -0
  226. package/dist/harness-runtime/vendor/opencode-client/solid/pty.js +8 -0
  227. package/dist/harness-runtime/vendor/opencode-plugin.mjs +3 -0
  228. package/dist/protocol/local-files.js +18 -9
  229. package/dist/runtime/context.js +8 -4
  230. package/dist/schemas/agent-profile.schema.json +1 -1
  231. package/dist/schemas/code-review-decision.schema.json +9 -0
  232. package/dist/schemas/code-review-result.schema.json +1 -1
  233. package/dist/schemas/code-verification.schema.json +7 -5
  234. package/dist/schemas/code-work-batch.schema.json +2 -2
  235. package/dist/schemas/code-work-result.schema.json +2 -1
  236. package/dist/schemas/delivery-stage-result.schema.json +286 -0
  237. package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
  238. package/dist/schemas/diagnostic-work-result.schema.json +296 -0
  239. package/dist/schemas/discovery-result.schema.json +184 -0
  240. package/dist/schemas/distill-stage-result.schema.json +667 -0
  241. package/dist/schemas/distill-work-result.schema.json +428 -0
  242. package/dist/schemas/execution-bundle.schema.json +215 -0
  243. package/dist/schemas/execution-settings.schema.json +162 -0
  244. package/dist/schemas/flow-descriptor.schema.json +54 -0
  245. package/dist/schemas/flow-run.schema.json +29 -0
  246. package/dist/schemas/harness-catalog.schema.json +80 -0
  247. package/dist/schemas/interactive-stage-result.schema.json +47 -0
  248. package/dist/schemas/interactive-work-result.schema.json +63 -0
  249. package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
  250. package/dist/schemas/mb-init-compose-result.schema.json +457 -0
  251. package/dist/schemas/mb-init-discover-result.schema.json +500 -0
  252. package/dist/schemas/mb-init-finish-result.schema.json +422 -0
  253. package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
  254. package/dist/schemas/mb-init-review-result.schema.json +396 -0
  255. package/dist/schemas/mb-init-verify-result.schema.json +391 -0
  256. package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
  257. package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
  258. package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
  259. package/dist/schemas/memory-composition-work-result.schema.json +38 -0
  260. package/dist/schemas/memory-repair-work-result.schema.json +56 -0
  261. package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
  262. package/dist/schemas/memory-review-work-result.schema.json +43 -0
  263. package/dist/schemas/model-catalog.schema.json +153 -0
  264. package/dist/schemas/plan-review-decision.schema.json +12 -3
  265. package/dist/schemas/plan-review-result.schema.json +2 -2
  266. package/dist/schemas/profile-settings.schema.json +122 -0
  267. package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
  268. package/dist/schemas/release-impact.schema.json +3 -3
  269. package/dist/schemas/release-manifest.schema.json +291 -0
  270. package/dist/schemas/review-fix-stage-result.schema.json +210 -0
  271. package/dist/schemas/run-control-receipt.schema.json +119 -0
  272. package/dist/schemas/run-control-request.schema.json +36 -0
  273. package/dist/schemas/run-engine-binding.schema.json +1 -1
  274. package/dist/schemas/stage-start-response.schema.json +4 -0
  275. package/dist/schemas/vnext-protocol-plan.schema.json +3 -3
  276. package/dist/services/accepted-intent.js +139 -0
  277. package/dist/services/assigned-lifecycle.js +382 -0
  278. package/dist/services/branch-context.js +3 -3
  279. package/dist/services/canon.js +54 -15
  280. package/dist/services/check-environment.js +21 -0
  281. package/dist/services/check-runner-evidence.js +344 -0
  282. package/dist/services/check-runner-proof.js +373 -0
  283. package/dist/services/cleanup.js +133 -40
  284. package/dist/services/cli-operation-classifier.js +32 -64
  285. package/dist/services/cli-upgrade-allowlist.js +27 -0
  286. package/dist/services/code-checks.js +1426 -121
  287. package/dist/services/codex-capacity.js +367 -0
  288. package/dist/services/codex-hook-delivery.js +41 -0
  289. package/dist/services/command-context.js +80 -0
  290. package/dist/services/compatibility-preflight.js +16 -3
  291. package/dist/services/config.js +11 -8
  292. package/dist/services/continuation-outcome.js +25 -0
  293. package/dist/services/controller-child-capacity.js +71 -0
  294. package/dist/services/controller-child-observer.js +121 -0
  295. package/dist/services/controller-child-repair.js +117 -0
  296. package/dist/services/controller-execution.js +97 -0
  297. package/dist/services/controller-fanout.js +277 -0
  298. package/dist/services/controller-memory-task.js +45 -0
  299. package/dist/services/controller-memory-worker.js +51 -0
  300. package/dist/services/controller-result-authority.js +127 -0
  301. package/dist/services/controller-work-failure.js +198 -0
  302. package/dist/services/dashboard.js +145 -58
  303. package/dist/services/delivery-access.js +123 -0
  304. package/dist/services/delivery-approval.js +68 -0
  305. package/dist/services/delivery-artifact.js +86 -0
  306. package/dist/services/delivery-checks.js +178 -0
  307. package/dist/services/delivery-execution.js +181 -0
  308. package/dist/services/delivery-intents.js +212 -0
  309. package/dist/services/delivery-operation.js +81 -0
  310. package/dist/services/delivery-policy.js +91 -0
  311. package/dist/services/delivery-procedures.js +43 -0
  312. package/dist/services/delivery-request.js +99 -0
  313. package/dist/services/delivery-result.js +144 -0
  314. package/dist/services/delivery-stage-commands.js +26 -0
  315. package/dist/services/delivery-stage-entry.js +56 -0
  316. package/dist/services/delivery-stage-handlers.js +135 -0
  317. package/dist/services/delivery-stage-results.js +94 -0
  318. package/dist/services/descriptor-coordinator-session.js +37 -0
  319. package/dist/services/descriptor-root-work.js +53 -0
  320. package/dist/services/descriptor-run-settlement.js +107 -0
  321. package/dist/services/descriptor-run-start.js +495 -0
  322. package/dist/services/descriptor-stage-coordinator.js +318 -0
  323. package/dist/services/descriptor-stage-settlement.js +268 -0
  324. package/dist/services/descriptor-stage-start.js +175 -0
  325. package/dist/services/diagnostic-evidence.js +73 -0
  326. package/dist/services/diagnostic-repair-intake.js +123 -0
  327. package/dist/services/diagnostic-repair-source-intake.js +102 -0
  328. package/dist/services/diagnostic-report-publication.js +72 -0
  329. package/dist/services/diagnostic-report.js +97 -0
  330. package/dist/services/diagnostic-run-bundle.js +113 -0
  331. package/dist/services/diagnostic-snapshot.js +159 -0
  332. package/dist/services/diagnostic-stage-handlers.js +109 -0
  333. package/dist/services/diagnostic-stage-results.js +158 -0
  334. package/dist/services/diagnostic-start.js +134 -0
  335. package/dist/services/diagnostic-work-batch.js +34 -0
  336. package/dist/services/diagnostic-work-collection.js +97 -0
  337. package/dist/services/diagnostic-work-registration.js +101 -0
  338. package/dist/services/diagnostic-work-results.js +93 -0
  339. package/dist/services/discovery/agy-discovery.js +2 -0
  340. package/dist/services/discovery/codex-discovery.js +2 -0
  341. package/dist/services/discovery/droid-discovery.js +2 -0
  342. package/dist/services/discovery/grok-discovery.js +2 -0
  343. package/dist/services/discovery/opencode-discovery.js +2 -0
  344. package/dist/services/discovery/static.js +31 -0
  345. package/dist/services/discovery/zcode-discovery.js +2 -0
  346. package/dist/services/distill-document-publication.js +280 -0
  347. package/dist/services/distill-promotion.js +197 -0
  348. package/dist/services/distill-publication-custody.js +162 -0
  349. package/dist/services/distill-stage-handlers.js +249 -0
  350. package/dist/services/distill-stage-results.js +105 -0
  351. package/dist/services/distill-user-inputs.js +4 -0
  352. package/dist/services/distill-work-batch.js +42 -0
  353. package/dist/services/distill-work-collection.js +74 -0
  354. package/dist/services/distill-work-registration.js +93 -0
  355. package/dist/services/distill-work-results.js +98 -0
  356. package/dist/services/engines.js +354 -163
  357. package/dist/services/eval-snapshots.js +1326 -150
  358. package/dist/services/execution-bundles.js +275 -0
  359. package/dist/services/execution-policy.js +185 -0
  360. package/dist/services/executor-onboarding.js +56 -0
  361. package/dist/services/external-work-launch.js +316 -0
  362. package/dist/services/flow-catalog.js +54 -0
  363. package/dist/services/flow-descriptors.js +82 -0
  364. package/dist/services/frozen-engine.js +404 -0
  365. package/dist/services/generic-execution.js +165 -0
  366. package/dist/services/harness-adapter.js +342 -35
  367. package/dist/services/harness-config.js +2 -0
  368. package/dist/services/harness-discovery.js +29 -0
  369. package/dist/services/harness-doctor.js +75 -0
  370. package/dist/services/harness-probe-effects.js +51 -0
  371. package/dist/services/harness-version-probe.js +100 -0
  372. package/dist/services/hook-memory-routing.js +188 -0
  373. package/dist/services/hook-recovery-guard.js +14 -0
  374. package/dist/services/hook-session-observation.js +24 -0
  375. package/dist/services/hooks.js +813 -327
  376. package/dist/services/hybrid-contracts.js +479 -0
  377. package/dist/services/hybrid-digest.js +14 -0
  378. package/dist/services/ids.js +29 -1
  379. package/dist/services/installation-lock.js +95 -0
  380. package/dist/services/installed-flow-source.js +261 -0
  381. package/dist/services/interactive-attached-session.js +215 -0
  382. package/dist/services/interactive-baseline-custody.js +54 -0
  383. package/dist/services/interactive-before-versions.js +148 -0
  384. package/dist/services/interactive-bundle-data.js +85 -0
  385. package/dist/services/interactive-checks.js +179 -0
  386. package/dist/services/interactive-execution.js +33 -0
  387. package/dist/services/interactive-finish-integration.js +236 -0
  388. package/dist/services/interactive-git-baseline.js +230 -0
  389. package/dist/services/interactive-git-dispatch.js +278 -0
  390. package/dist/services/interactive-input-delivery.js +82 -0
  391. package/dist/services/interactive-mutation-scope.js +231 -0
  392. package/dist/services/interactive-request.js +287 -0
  393. package/dist/services/interactive-run-bundle.js +109 -0
  394. package/dist/services/interactive-stage-entry.js +41 -0
  395. package/dist/services/interactive-stage-handlers.js +289 -0
  396. package/dist/services/interactive-stage-observation.js +136 -0
  397. package/dist/services/interactive-stage-results.js +111 -0
  398. package/dist/services/interactive-user-inputs.js +142 -0
  399. package/dist/services/interactive-work-batch.js +43 -0
  400. package/dist/services/interactive-work-collection.js +119 -0
  401. package/dist/services/interactive-work-completion.js +180 -0
  402. package/dist/services/interactive-work-context.js +249 -0
  403. package/dist/services/interactive-work-history.js +112 -0
  404. package/dist/services/interactive-work-registration.js +124 -0
  405. package/dist/services/interactive-work-results.js +115 -0
  406. package/dist/services/interactive-work-start.js +155 -0
  407. package/dist/services/lanes.js +78 -65
  408. package/dist/services/lifecycle-command.js +1 -288
  409. package/dist/services/lifecycle-contract.js +1 -0
  410. package/dist/services/lifecycle-invocations.js +2549 -0
  411. package/dist/services/managed-daemon-binding.js +129 -0
  412. package/dist/services/managed-lease-monitor.js +133 -0
  413. package/dist/services/managed-processes.js +449 -69
  414. package/dist/services/memory-accepted-composition.js +78 -0
  415. package/dist/services/memory-baseline-preservation.js +65 -0
  416. package/dist/services/memory-completed-correction-wave.js +328 -0
  417. package/dist/services/memory-compose-handler.js +380 -0
  418. package/dist/services/memory-composition-acceptance.js +375 -0
  419. package/dist/services/memory-composition-graph.js +165 -0
  420. package/dist/services/memory-composition-registration.js +161 -0
  421. package/dist/services/memory-composition-work-custody.js +156 -0
  422. package/dist/services/memory-composition-work-results.js +108 -0
  423. package/dist/services/memory-corrected-review-acceptance.js +129 -0
  424. package/dist/services/memory-corrected-review-batch.js +37 -0
  425. package/dist/services/memory-corrected-review-findings.js +108 -0
  426. package/dist/services/memory-corrected-review-inputs.js +215 -0
  427. package/dist/services/memory-corrected-review-projection.js +78 -0
  428. package/dist/services/memory-corrected-review-receipt.js +178 -0
  429. package/dist/services/memory-corrected-review-registration.js +192 -0
  430. package/dist/services/memory-correction-check-receipt.js +235 -0
  431. package/dist/services/memory-correction-input-owner.js +6 -0
  432. package/dist/services/memory-custody-snapshot.js +36 -0
  433. package/dist/services/memory-discover-handler.js +104 -0
  434. package/dist/services/memory-finish-evidence.js +457 -0
  435. package/dist/services/memory-finish-handler.js +229 -0
  436. package/dist/services/memory-finish-integration.js +491 -0
  437. package/dist/services/memory-finish-readiness.js +169 -0
  438. package/dist/services/memory-fix-finish-integration.js +129 -0
  439. package/dist/services/memory-fix-git-dispatch.js +139 -0
  440. package/dist/services/memory-fix-intake-custody.js +11 -0
  441. package/dist/services/memory-fix-intake-settlement.js +127 -0
  442. package/dist/services/memory-fix-publication.js +196 -0
  443. package/dist/services/memory-fix-repair-admission.js +116 -0
  444. package/dist/services/memory-fix-request.js +141 -0
  445. package/dist/services/memory-fix-run-bundle.js +83 -0
  446. package/dist/services/memory-fix-scope.js +78 -0
  447. package/dist/services/memory-fix-sources.js +85 -0
  448. package/dist/services/memory-fix-stage-entry.js +24 -0
  449. package/dist/services/memory-fix-stage-handlers.js +282 -0
  450. package/dist/services/memory-fix-stage-results.js +106 -0
  451. package/dist/services/memory-fix-workspace.js +127 -0
  452. package/dist/services/memory-flow-user-inputs.js +88 -0
  453. package/dist/services/memory-git-integration.js +220 -0
  454. package/dist/services/memory-historical-corrected-review.js +212 -0
  455. package/dist/services/memory-historical-original-review.js +143 -0
  456. package/dist/services/memory-historical-repair-registration.js +105 -0
  457. package/dist/services/memory-historical-second-review.js +147 -0
  458. package/dist/services/memory-init-bootstrap-readiness.js +210 -0
  459. package/dist/services/memory-init-contexts.js +181 -0
  460. package/dist/services/memory-init-controller-start.js +119 -0
  461. package/dist/services/memory-init-engine-release.js +223 -0
  462. package/dist/services/memory-init-git-policy.js +165 -0
  463. package/dist/services/memory-init-lint-capability.js +203 -0
  464. package/dist/services/memory-init-native-capacity.js +309 -0
  465. package/dist/services/memory-init-preflight.js +334 -0
  466. package/dist/services/memory-init-project-bootstrap.js +599 -0
  467. package/dist/services/memory-init-request-candidate.js +213 -0
  468. package/dist/services/memory-init-request-input.js +82 -0
  469. package/dist/services/memory-init-resource-storage.js +125 -0
  470. package/dist/services/memory-init-run-allocation.js +81 -0
  471. package/dist/services/memory-init-run-bundle.js +135 -0
  472. package/dist/services/memory-init-start.js +181 -0
  473. package/dist/services/memory-init-startup-custody.js +80 -0
  474. package/dist/services/memory-init-startup-input.js +240 -0
  475. package/dist/services/memory-init-storage.js +91 -0
  476. package/dist/services/memory-init-stores.js +359 -0
  477. package/dist/services/memory-init-worktrunk-capability.js +174 -0
  478. package/dist/services/memory-layer-publication.js +611 -0
  479. package/dist/services/memory-layer.js +225 -0
  480. package/dist/services/memory-lint-config.js +194 -0
  481. package/dist/services/memory-native-review-copy.js +127 -0
  482. package/dist/services/memory-normal-flow-entry.js +87 -0
  483. package/dist/services/memory-ordinary-defaults.js +129 -0
  484. package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
  485. package/dist/services/memory-preflight-handler.js +274 -0
  486. package/dist/services/memory-repair-current-view.js +299 -0
  487. package/dist/services/memory-repair-document-delta.js +489 -0
  488. package/dist/services/memory-repair-history.js +496 -0
  489. package/dist/services/memory-repair-physical-binding.js +194 -0
  490. package/dist/services/memory-repair-publication-files.js +152 -0
  491. package/dist/services/memory-repair-publication-intent.js +27 -0
  492. package/dist/services/memory-repair-publication-plan.js +162 -0
  493. package/dist/services/memory-repair-publication-source.js +225 -0
  494. package/dist/services/memory-repair-publication.js +795 -0
  495. package/dist/services/memory-repair-registration.js +240 -0
  496. package/dist/services/memory-repair-work-baseline.js +415 -0
  497. package/dist/services/memory-repair-work-completion.js +117 -0
  498. package/dist/services/memory-repair-work-custody.js +73 -0
  499. package/dist/services/memory-repair-work-results.js +108 -0
  500. package/dist/services/memory-repair-workspace.js +261 -0
  501. package/dist/services/memory-repair-writer-copy.js +736 -0
  502. package/dist/services/memory-result-binding.js +239 -0
  503. package/dist/services/memory-retained-review-authority.js +138 -0
  504. package/dist/services/memory-retained-review-results.js +70 -0
  505. package/dist/services/memory-retained-second-baseline.js +102 -0
  506. package/dist/services/memory-retained-second-repairs.js +162 -0
  507. package/dist/services/memory-review-acceptance.js +214 -0
  508. package/dist/services/memory-review-correction-plan.js +206 -0
  509. package/dist/services/memory-review-handler.js +585 -0
  510. package/dist/services/memory-review-inputs.js +246 -0
  511. package/dist/services/memory-review-limits.js +10 -0
  512. package/dist/services/memory-review-receipt.js +180 -0
  513. package/dist/services/memory-review-registration.js +117 -0
  514. package/dist/services/memory-review-work-custody.js +126 -0
  515. package/dist/services/memory-review-work-input.js +91 -0
  516. package/dist/services/memory-review-work-results.js +95 -0
  517. package/dist/services/memory-second-corrected-review-inputs.js +167 -0
  518. package/dist/services/memory-second-correction-plan.js +77 -0
  519. package/dist/services/memory-second-repair-baseline.js +91 -0
  520. package/dist/services/memory-second-repair-launch.js +614 -0
  521. package/dist/services/memory-second-repair-registration.js +88 -0
  522. package/dist/services/memory-second-repair-source.js +187 -0
  523. package/dist/services/memory-second-review-acceptance.js +115 -0
  524. package/dist/services/memory-source-observations.js +117 -0
  525. package/dist/services/memory-stage-dispatch.js +65 -0
  526. package/dist/services/memory-stage-results.js +290 -0
  527. package/dist/services/memory-tool-probe.js +25 -0
  528. package/dist/services/memory-upgrade-analysis-handler.js +243 -0
  529. package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
  530. package/dist/services/memory-upgrade-evidence.js +100 -0
  531. package/dist/services/memory-upgrade-inventory.js +442 -0
  532. package/dist/services/memory-upgrade-publication-custody.js +73 -0
  533. package/dist/services/memory-upgrade-publication.js +413 -0
  534. package/dist/services/memory-upgrade-selection.js +430 -0
  535. package/dist/services/memory-upgrade-start.js +60 -0
  536. package/dist/services/memory-upgrade-storage-readiness.js +340 -0
  537. package/dist/services/memory-verification-acceptance.js +156 -0
  538. package/dist/services/memory-verification-checks.js +317 -0
  539. package/dist/services/memory-verification-receipt.js +160 -0
  540. package/dist/services/memory-verify-handler.js +137 -0
  541. package/dist/services/memory-work-result-json.js +12 -0
  542. package/dist/services/memory-work-result-preparation.js +64 -0
  543. package/dist/services/merge-queue.js +228 -149
  544. package/dist/services/merge-server.js +163 -35
  545. package/dist/services/migrations.js +164 -114
  546. package/dist/services/model-catalog.js +109 -0
  547. package/dist/services/model-discovery.js +46 -0
  548. package/dist/services/native-daemon-history.js +50 -0
  549. package/dist/services/native-session-control.js +39 -0
  550. package/dist/services/operator-profiles.js +236 -0
  551. package/dist/services/physical-startup.js +45 -0
  552. package/dist/services/plan-runtime.js +26 -11
  553. package/dist/services/plans.js +28 -25
  554. package/dist/services/project-flow-source.js +195 -0
  555. package/dist/services/project-git-integration.js +92 -0
  556. package/dist/services/project-registry.js +97 -0
  557. package/dist/services/project-summary.js +1 -1
  558. package/dist/services/projects.js +17 -87
  559. package/dist/services/prompt-delivery.js +71 -0
  560. package/dist/services/prompts.js +28 -17
  561. package/dist/services/protocols.js +163 -87
  562. package/dist/services/provider-capacity.js +463 -0
  563. package/dist/services/recovery-observation-budget.js +121 -0
  564. package/dist/services/recovery-snapshot-database.js +588 -0
  565. package/dist/services/release-archive-producer.js +52 -0
  566. package/dist/services/release-archive.js +143 -0
  567. package/dist/services/release-cache.js +150 -0
  568. package/dist/services/release-compatibility.js +18 -0
  569. package/dist/services/release-download.js +67 -0
  570. package/dist/services/release-envelope.js +104 -0
  571. package/dist/services/release-manifest.js +45 -0
  572. package/dist/services/release-payload.js +114 -0
  573. package/dist/services/release-producer.js +63 -0
  574. package/dist/services/release-provenance.js +160 -0
  575. package/dist/services/release-registry.js +66 -0
  576. package/dist/services/release-source.js +194 -0
  577. package/dist/services/repair-intents.js +29 -0
  578. package/dist/services/repair-receipt.js +13 -0
  579. package/dist/services/retained-native-hook.js +274 -0
  580. package/dist/services/review-copy.js +165 -0
  581. package/dist/services/review-deferral.js +23 -0
  582. package/dist/services/review-fix-handoff.js +82 -0
  583. package/dist/services/review-fix-intake-custody.js +17 -0
  584. package/dist/services/review-fix-stage-entry.js +43 -0
  585. package/dist/services/review-fix-stage-handlers.js +176 -0
  586. package/dist/services/review-fix-stage-results.js +86 -0
  587. package/dist/services/run-control-receipt.js +56 -0
  588. package/dist/services/run-control-worker.js +469 -0
  589. package/dist/services/run-control.js +1109 -0
  590. package/dist/services/run-controller-adapter.js +434 -0
  591. package/dist/services/run-controller-capture.js +186 -0
  592. package/dist/services/run-controller-process.js +226 -0
  593. package/dist/services/run-controller-recovery.js +335 -0
  594. package/dist/services/run-controller-state.js +106 -0
  595. package/dist/services/run-controller.js +1717 -0
  596. package/dist/services/run-engine-bindings.js +1 -1
  597. package/dist/services/run-fork.js +147 -0
  598. package/dist/services/run-health.js +149 -0
  599. package/dist/services/run-observations.js +152 -0
  600. package/dist/services/run-projection.js +22 -64
  601. package/dist/services/run-recovery-guard.js +107 -0
  602. package/dist/services/run-recovery-runtime.js +112 -0
  603. package/dist/services/run-recovery.js +485 -0
  604. package/dist/services/run-start-custody.js +189 -0
  605. package/dist/services/run-timeline.js +59 -0
  606. package/dist/services/runs.js +996 -271
  607. package/dist/services/runtime-budget.js +446 -0
  608. package/dist/services/runtime-command.js +43 -0
  609. package/dist/services/runtime-scope-capture.js +52 -0
  610. package/dist/services/runtime-scope-control.js +468 -0
  611. package/dist/services/runtime-scope-resume.js +605 -0
  612. package/dist/services/runtime-scope-stop.js +211 -0
  613. package/dist/services/runtime-scope-worker.js +314 -0
  614. package/dist/services/runtime-service.js +19 -14
  615. package/dist/services/schema-validation.js +10 -8
  616. package/dist/services/scope-writer-cleanup.js +27 -0
  617. package/dist/services/sessions.js +37 -91
  618. package/dist/services/setup.js +427 -0
  619. package/dist/services/stage-blocker.js +17 -7
  620. package/dist/services/stage-context.js +240 -18
  621. package/dist/services/stage-handlers.js +141 -0
  622. package/dist/services/stage-lifecycle.js +156 -119
  623. package/dist/services/stage-pause.js +222 -94
  624. package/dist/services/stage-report-renderer.js +230 -6
  625. package/dist/services/stage-work-graph.js +52 -0
  626. package/dist/services/technical-notes.js +31 -0
  627. package/dist/services/tooling.js +111 -44
  628. package/dist/services/usage.js +49 -101
  629. package/dist/services/verification-matrix.js +393 -0
  630. package/dist/services/version-status.js +1 -1
  631. package/dist/services/vnext-code-review.js +302 -91
  632. package/dist/services/vnext-code.js +325 -251
  633. package/dist/services/vnext-contracts.js +10 -1
  634. package/dist/services/vnext-execution-profile.js +80 -5
  635. package/dist/services/vnext-fanout.js +399 -20
  636. package/dist/services/vnext-merge.js +336 -114
  637. package/dist/services/vnext-plan-review.js +288 -133
  638. package/dist/services/vnext-plan.js +230 -106
  639. package/dist/services/vnext-protocolize.js +534 -96
  640. package/dist/services/vnext-specify.js +131 -84
  641. package/dist/services/vnext-workspace-policy.js +7 -2
  642. package/dist/services/work-registry.js +1950 -215
  643. package/dist/services/work-retry-context.js +55 -0
  644. package/dist/services/workspace-bootstrap.js +53 -15
  645. package/dist/services/workspace-files.js +85 -0
  646. package/dist/services/worktrees.js +108 -48
  647. package/dist/shared/entity-references.js +1 -0
  648. package/dist/shared/errors.js +27 -0
  649. package/dist/storage/agent-output.js +26 -0
  650. package/dist/storage/data-preservation.js +65 -0
  651. package/dist/storage/database.js +931 -40
  652. package/dist/storage/foreign-key-transaction.js +39 -0
  653. package/dist/storage/immutable-input.js +45 -0
  654. package/dist/storage/import-guard.js +159 -0
  655. package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
  656. package/dist/storage/native-cleanup.js +52 -0
  657. package/dist/storage/paired-writer-migration.js +424 -0
  658. package/dist/storage/paths.js +17 -4
  659. package/dist/storage/projection-lock.js +73 -0
  660. package/dist/storage/work-references.js +23 -0
  661. package/dist/storage/writer-contract.js +96 -0
  662. package/dist/storage/writer-migration.js +346 -0
  663. package/dist/storage/writer-restore.js +217 -0
  664. package/dist/verification-components.json +384 -0
  665. package/package.json +52 -13
  666. package/tools/create-canon-release.mjs +33 -0
  667. package/tools/probe-codex-history.mjs +37 -0
  668. package/tools/produce-hybrid-release.mjs +41 -0
  669. package/tools/repair-paused-run-status.mjs +84 -0
  670. package/tools/verification-jobs.mjs +342 -0
@@ -2,14 +2,53 @@ import crypto from "node:crypto";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
+ import { DatabaseSync } from "node:sqlite";
6
+ import { registerStoreWriter } from "../storage/writer-contract.js";
7
+ import { beginSnapshotImport, completeSnapshotImport } from "../storage/import-guard.js";
8
+ import { foreignKeyTransaction } from "../storage/foreign-key-transaction.js";
5
9
  import { createContext } from "../runtime/context.js";
6
10
  import { AppError } from "../shared/errors.js";
7
- import { resolveProjectRoot } from "../storage/paths.js";
11
+ import { canonicalPath, resolveProjectRoot, runtimeProtocolDir, runtimeRunDir } from "../storage/paths.js";
8
12
  import { registerProject, requireProjectByRoot } from "./projects.js";
9
- import { getFlowRunStatus, startFlowRun } from "./runs.js";
13
+ import { freezeRunExecutionRouting, getFlowRunStatus, importFlowRunExecutionProfile, startFlowRun } from "./runs.js";
14
+ import { loadExecutionRoutingInput, resolveExecutionPolicy } from "./execution-policy.js";
15
+ import { validateRunOperationalDecision } from "./vnext-execution-profile.js";
10
16
  import { readVnextFlowDefinition } from "./vnext-specify.js";
17
+ import { captureRecoveryDatabase, readRecoveryResourceEvidence } from "./recovery-snapshot-database.js";
18
+ import { resourceHome } from "./managed-processes.js";
19
+ import { getResourceDatabase } from "../storage/database.js";
20
+ import { codexHomeSharedEntries } from "./hooks.js";
21
+ import { prepareOutputFile } from "../cli/input-preparation.js";
22
+ import { localEnvironmentAllowed, projectServicePathFilter } from "./workspace-files.js";
23
+ import { cleanupScopeWriters } from "./scope-writer-cleanup.js";
24
+ import { PreparedInstalledFlowSource } from "./installed-flow-source.js";
25
+ import { PreparedProjectFlowSource } from "./project-flow-source.js";
26
+ import { readSetupStatus } from "./setup.js";
27
+ import { isSourceSnapshotFlow } from "../domain/source-snapshot-flows.js";
28
+ import { isDeliveryFlow } from "../domain/delivery-flows.js";
29
+ import { isObservationOperation, nativeInventoryVersion, retainedDaemonOperations } from "./run-control.js";
11
30
  /** Capture the only flow boundary which exists before `stage start --bootstrap` creates a RUN. */
12
- export function createEvalBootstrapSnapshot(context, input) {
31
+ export function prepareEvalBootstrapSnapshot(context, input) {
32
+ let source;
33
+ if (input.flow === "interactive") {
34
+ const projectRoot = resolveProjectRoot(input.projectRoot);
35
+ source = PreparedProjectFlowSource.fromProject({ projectRoot, workspace: projectRoot, flowId: "interactive" });
36
+ if (source.resolveRegisteredFlow().descriptor.stages[0]?.id !== "preflight")
37
+ throw new AppError("snapshot_flow_unsupported", "Interactive bootstrap requires its preflight entry", 2, { phase: "prepare", effect: "no_effect" });
38
+ }
39
+ else if (input.flow !== undefined && input.flow !== "sdlc") {
40
+ if (input.flow !== "mb-init" && input.flow !== "mb-upgrade" && !isSourceSnapshotFlow(input.flow) && !isDeliveryFlow(input.flow))
41
+ throw new AppError("snapshot_flow_unsupported", "Bootstrap snapshot flow is not registered", 2, { phase: "prepare", effect: "no_effect" });
42
+ if (input.flow === "review-fix")
43
+ throw new AppError("snapshot_flow_unsupported", "Review-fix starts at intake, not the preflight bootstrap boundary", 2, { phase: "prepare", effect: "no_effect" });
44
+ const home = context.env.DD_FLOW_CONFIG_HOME ?? context.ddFlowHome;
45
+ const selected = readSetupStatus(home);
46
+ if (!selected.installation)
47
+ throw new AppError("snapshot_flow_unavailable", "Bootstrap snapshot requires the selected installed flow", 2, { phase: "prepare", effect: "no_effect" });
48
+ source = PreparedInstalledFlowSource.fromInstalledRelease(home, selected.installation.manifest_sha256, input.flow);
49
+ if (source.resolveRegisteredFlow().descriptor.stages[0]?.id !== "preflight")
50
+ throw new AppError("snapshot_flow_unsupported", "Memory bootstrap requires its preflight entry", 2, { phase: "prepare", effect: "no_effect" });
51
+ }
13
52
  const projectRoot = resolveProjectRoot(input.projectRoot);
14
53
  const output = path.resolve(input.output);
15
54
  if (fs.existsSync(output))
@@ -18,44 +57,161 @@ export function createEvalBootstrapSnapshot(context, input) {
18
57
  throw new AppError("snapshot_output_inside_project", "Bootstrap snapshot output must be outside the project tree", 1, { output });
19
58
  if (isInside(physicalPath(context.ddFlowHome), physicalPath(output)))
20
59
  throw new AppError("snapshot_output_inside_runtime", "Bootstrap snapshot output must be outside DD_FLOW_HOME", 1, { output });
21
- fs.mkdirSync(output, { recursive: true });
22
- fs.cpSync(projectRoot, path.join(output, "project"), { recursive: true, verbatimSymlinks: true, filter: (source) => path.basename(source) !== ".git" });
23
- const manifest = {
24
- schema_id: "dd-eval/bootstrap-snapshot@1",
25
- stage: "specify",
26
- project_root: projectRoot,
27
- project_sha256: treeHash(path.join(output, "project")),
28
- project_git: snapshotProjectGit(projectRoot, output)
29
- };
30
- writeJson(path.join(output, "bootstrap.json"), manifest);
31
- return { ok: true, snapshot: output, ...manifest };
60
+ prepareOutputFile(output);
61
+ try {
62
+ prepareSnapshotProjectGit(projectRoot);
63
+ }
64
+ catch (error) {
65
+ if (!(error instanceof AppError) || error.code !== "snapshot_project_git_invalid")
66
+ throw error;
67
+ throw new AppError(error.code, error.message, 2, { ...error.details, phase: "prepare", effect: "no_effect", recoverable: true });
68
+ }
69
+ return { projectRoot, output, source };
70
+ }
71
+ export function createEvalBootstrapSnapshot(context, input) {
72
+ const { projectRoot, output, source } = prepareEvalBootstrapSnapshot(context, input);
73
+ const temporary = `${output}.partial-${crypto.randomUUID()}`;
74
+ const before = bootstrapSourceVersion(projectRoot);
75
+ try {
76
+ fs.mkdirSync(temporary, { recursive: true });
77
+ fs.cpSync(projectRoot, path.join(temporary, "project"), { recursive: true, verbatimSymlinks: true, filter: projectPayloadFilter(projectRoot) });
78
+ const after = bootstrapSourceVersion(projectRoot);
79
+ if (before !== after)
80
+ throw new AppError("snapshot_source_changed", "Bootstrap source changed during snapshot capture", 1, { phase: "bootstrap", recoverable: true });
81
+ const manifest = {
82
+ schema_id: source ? "dd-eval/bootstrap-snapshot@2" : "dd-eval/bootstrap-snapshot@1",
83
+ stage: source?.resolveRegisteredFlow().descriptor.stages[0]?.id ?? "specify",
84
+ ...(source ? { flow: { id: source.resolveRegisteredFlow().descriptor.id, source_sha256: source.sha256, descriptor_sha256: source.descriptorReference().sha256 } } : {}),
85
+ project_root: projectRoot,
86
+ project_sha256: treeHash(path.join(temporary, "project")),
87
+ project_git: snapshotProjectGit(projectRoot, temporary)
88
+ };
89
+ writeJson(path.join(temporary, "bootstrap.json"), manifest);
90
+ if (after !== bootstrapSourceVersion(projectRoot))
91
+ throw new AppError("snapshot_source_changed", "Bootstrap source changed while preserving Git evidence", 1, { phase: "bootstrap", recoverable: true });
92
+ source?.revalidate();
93
+ syncSnapshotTree(temporary);
94
+ fs.renameSync(temporary, output);
95
+ syncPath(path.dirname(output));
96
+ return { ok: true, snapshot: output, ...manifest };
97
+ }
98
+ catch (error) {
99
+ fs.rmSync(temporary, { recursive: true, force: true });
100
+ throw error;
101
+ }
102
+ }
103
+ function bootstrapSourceVersion(projectRoot) {
104
+ return JSON.stringify([
105
+ treeHash(projectRoot, true),
106
+ treeModesHash(projectRoot, true),
107
+ git(projectRoot, ["symbolic-ref", "--quiet", "HEAD"]),
108
+ git(projectRoot, ["rev-parse", "HEAD"]),
109
+ indexEntries(projectRoot).toString("base64")
110
+ ]);
32
111
  }
33
112
  /** Restore a pre-RUN project state; `stage start --bootstrap` creates runtime state afterwards. */
34
- export function restoreEvalBootstrapSnapshot(_context, input) {
113
+ export function prepareEvalBootstrapRestore(input) {
35
114
  const snapshot = path.resolve(input.snapshot);
36
115
  const manifest = readBootstrapManifest(snapshot);
37
116
  const projectRoot = resolveProjectRoot(input.projectRoot);
117
+ assertRestoreTargetSeparation(snapshot, [projectRoot]);
38
118
  const sourceProject = path.join(snapshot, "project");
39
- if (treeHash(sourceProject) !== manifest.project_sha256)
40
- throw new AppError("snapshot_checksum_mismatch", "Bootstrap project checksum does not match manifest", 1, { snapshot });
119
+ verifyGitSnapshotPayload(snapshot, { name: "project", hash: manifest.project_sha256, git: manifest.project_git });
120
+ return { snapshot, manifest, projectRoot, sourceProject };
121
+ }
122
+ export function restoreEvalBootstrapSnapshot(_context, input, prepared = prepareEvalBootstrapRestore(input)) {
123
+ const { snapshot, manifest, projectRoot, sourceProject } = prepared;
124
+ assertRestoreTargetSeparation(snapshot, [projectRoot]);
41
125
  clearProjectTree(projectRoot, false);
42
126
  restoreProjectGit(snapshot, manifest.project_git, projectRoot);
43
127
  replaceProjectTree(projectRoot, sourceProject);
44
- return { ok: true, schema_id: "dd-eval/bootstrap-restore@1", snapshot, project_root: projectRoot, target_stage: "specify" };
128
+ return { ok: true, schema_id: "dd-eval/bootstrap-restore@1", snapshot, project_root: projectRoot, target_stage: manifest.stage, ...(manifest.flow ? { flow: manifest.flow } : {}) };
45
129
  }
46
130
  /**
47
- * Snapshot a deliberately dedicated DD_FLOW_HOME. Eval checkpoints are not a
48
- * general runtime export format: a shared home fails closed instead of trying
49
- * to infer a safe subset of SQLite rows.
131
+ * Eval fixtures require a dedicated home. Sealed recovery exports instead
132
+ * retain only the selected RUN's relational and filesystem evidence.
50
133
  */
51
134
  export function createEvalRunSnapshot(context, input) {
135
+ const { output } = prepareEvalRunSnapshotCreation(context, input);
136
+ const temporary = `${output}.partial-${crypto.randomUUID()}`;
137
+ let sourceVersion = null;
138
+ try {
139
+ input.boundary?.verify();
140
+ sourceVersion = input.boundary ? snapshotSourceVersion(context, input) : null;
141
+ const result = captureEvalRunSnapshot(context, { ...input, ...(sourceVersion ? { journalCuts: sourceVersion.journals, observationCuts: sourceVersion.observations } : {}), output: temporary });
142
+ if (input.boundary && sourceVersion) {
143
+ Object.assign(result, { consistency: "controller_boundary_writer_barrier@1", boundary_capture: { controller_id: input.boundary.controllerId, operation_id: input.boundary.operationId, boundary_key: input.boundary.boundaryKey, source_sha256: sourceVersion.sha256, observation: input.boundary.observation } });
144
+ writeJson(path.join(temporary, "snapshot.json"), Object.fromEntries(Object.entries(result).filter(([key]) => key !== "ok" && key !== "snapshot")));
145
+ }
146
+ if (input.recoveryId) {
147
+ const projectRoot = resolveProjectRoot(input.projectRoot);
148
+ const project = requireProjectByRoot(context, projectRoot);
149
+ const run = context.db.get("SELECT workspace_root FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
150
+ verifyRecoverySnapshot(temporary, { runId: input.runId, recoveryId: input.recoveryId, projectRoot, workspaceRoot: run?.workspace_root ?? projectRoot });
151
+ }
152
+ if (fs.existsSync(output))
153
+ throw new AppError("snapshot_exists", "Snapshot output already exists", 1, { output });
154
+ syncSnapshotTree(temporary);
155
+ if (input.boundary) {
156
+ if (sourceVersion) {
157
+ let after;
158
+ try {
159
+ after = snapshotSourceVersion(context, input, sourceVersion.databases, sourceVersion.journals);
160
+ }
161
+ catch (error) {
162
+ if (error.code !== "ENOENT")
163
+ throw error;
164
+ const missing = error.path;
165
+ throw new AppError("snapshot_source_changed", "Snapshot source disappeared during boundary capture", 1, { components: [missing && path.isAbsolute(missing) && isInside(context.ddFlowHome, missing) ? `runtime/${path.relative(context.ddFlowHome, missing)}` : "source_path_disappeared"] });
166
+ }
167
+ if (sourceVersion.sha256 !== after.sha256)
168
+ throw new AppError("snapshot_source_changed", "Snapshot source files or Git state changed during boundary capture", 1, { components: snapshotSourceChanges(sourceVersion.entries, after.entries), changes: snapshotSourceChangeProofs(sourceVersion.entries, after.entries) });
169
+ }
170
+ input.boundary.verify();
171
+ }
172
+ fs.renameSync(temporary, output);
173
+ syncPath(path.dirname(output));
174
+ return { ...result, snapshot: output };
175
+ }
176
+ catch (error) {
177
+ fs.rmSync(temporary, { recursive: true, force: true });
178
+ // Native journal publication creates its directory before requested.json.
179
+ // Retry a gap during any capture phase, without classifying an incomplete
180
+ // request as read-only. Persistent missing inputs still exhaust the budget.
181
+ if (input.boundary && error.code === "ENOENT") {
182
+ const missing = error.path;
183
+ throw new AppError("snapshot_source_changed", "Snapshot source was not fully published during boundary capture", 1, { components: [missing && path.isAbsolute(missing) && isInside(context.ddFlowHome, missing) ? `runtime/${path.relative(context.ddFlowHome, missing)}` : "source_path_disappeared"] });
184
+ }
185
+ throw error;
186
+ }
187
+ finally {
188
+ for (const db of sourceVersion?.databases.values() ?? [])
189
+ db.close();
190
+ }
191
+ }
192
+ export function prepareEvalRunSnapshotCreation(context, input) {
193
+ if (Number(Boolean(input.candidate)) + Number(Boolean(input.incomplete)) + Number(input.stageEntry !== undefined) + Number(input.recoveryId !== undefined) !== 1)
194
+ throw new AppError("usage", "Use exactly one of candidate, incomplete, recoveryId or stageEntry", 2, { phase: "prepare", effect: "no_effect", recoverable: true });
52
195
  const projectRoot = resolveProjectRoot(input.projectRoot);
53
196
  // Snapshot is read-only at the control-plane boundary. An invalid workspace
54
197
  // root must fail without registering a second project in a dedicated home.
55
198
  const project = requireProjectByRoot(context, projectRoot);
56
199
  const status = getFlowRunStatus(context, { projectRoot, runId: input.runId });
57
- assertDedicatedHome(context, project.id, input.runId);
58
- if (input.candidate)
200
+ const recovery = typeof input.recoveryId === "string";
201
+ if (!recovery && !input.incomplete && context.db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'run_controllers'")) {
202
+ const controller = context.db.get("SELECT controller_id,status,owner_pid,owner_token FROM run_controllers WHERE project_id = ? AND run_id = ? AND status NOT IN ('completed','cancelled','superseded','stop_target_reached')", [project.id, input.runId]);
203
+ if (controller && (controller.status !== "capturing" || controller.controller_id !== input.boundary?.controllerId || controller.owner_pid !== process.pid || controller.owner_token !== context.env.DD_FLOW_CONTROLLER_TOKEN))
204
+ throw new AppError("snapshot_controller_capture_required", "A live managed RUN must be captured by its fenced controller, not an external observer", 1, { controller_id: controller.controller_id });
205
+ }
206
+ if (!recovery)
207
+ assertDedicatedHome(context, project.id, input.runId);
208
+ if (recovery) {
209
+ const guard = context.db.get("SELECT status, recovery_id FROM run_recovery_guards WHERE project_id = ? AND run_id = ? AND recovery_id = ?", [project.id, input.runId, input.recoveryId]);
210
+ if (!guard || guard.status !== "sealed")
211
+ throw new AppError("recovery_invalid_state", "Recovery snapshots require the matching sealed recovery guard", 1, { run_id: input.runId, recovery_id: input.recoveryId ?? null, status: guard?.status ?? null });
212
+ }
213
+ if (input.incomplete || recovery) { /* Recovery captures only after the runner seals all writers. */ }
214
+ else if (input.candidate)
59
215
  assertTerminalCandidate(status.index.stage_runs ?? []);
60
216
  else if (input.stageEntry)
61
217
  assertStageEntry(status.index.stage_runs ?? [], input.stageEntry);
@@ -65,7 +221,7 @@ export function createEvalRunSnapshot(context, input) {
65
221
  const activeChildren = input.candidate
66
222
  ? context.db.get("SELECT COUNT(*) AS count FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NOT NULL AND status IN ('created', 'running', 'paused')", [project.id, input.runId])?.count ?? 0
67
223
  : context.db.get("SELECT COUNT(*) AS count FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NOT NULL AND (status IN ('running', 'paused') OR (status = 'created' AND (? IS NULL OR COALESCE(result_schema, '') NOT LIKE ?)))", [project.id, input.runId, allowedCreatedResultSchema, allowedCreatedResultSchema ? `${allowedCreatedResultSchema}@%` : ""])?.count ?? 0;
68
- if (activeChildren > 0)
224
+ if (!input.incomplete && !recovery && activeChildren > 0)
69
225
  throw new AppError("snapshot_not_quiescent", `${input.candidate ? "Candidate" : "Stage-entry"} snapshot requires no active child Work`, 1, { run_id: input.runId, active_children: activeChildren });
70
226
  const output = path.resolve(input.output);
71
227
  if (fs.existsSync(output))
@@ -75,36 +231,97 @@ export function createEvalRunSnapshot(context, input) {
75
231
  const workspaceRoot = String(status.run.workspace_root ?? projectRoot);
76
232
  if (isInside(physicalPath(projectRoot), physicalPath(output)) || isInside(physicalPath(workspaceRoot), physicalPath(output)))
77
233
  throw new AppError("snapshot_output_inside_project", "Snapshot output must be outside the project and RUN workspace trees", 1, { output });
78
- context.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
79
- fs.mkdirSync(output, { recursive: true });
80
- fs.cpSync(context.ddFlowHome, path.join(output, "runtime"), {
81
- recursive: true,
82
- verbatimSymlinks: true,
83
- // Managed checkouts are restored from their Git bundle and workspace
84
- // payload. Copying their .git indirection into runtime makes the snapshot
85
- // dependent on the source machine's worktree metadata.
86
- filter: (source) => !["engines", "checkouts"].includes(path.basename(source))
87
- });
234
+ prepareOutputFile(output);
235
+ prepareSnapshotProjectGit(projectRoot, recovery && status.run.flow_kind === "mb-init");
236
+ prepareSnapshotWorkspace(workspaceRoot, projectRoot, isSourceSnapshotFlow(status.run.flow_kind));
237
+ return { projectRoot, project, status, recovery, output, workspaceRoot, activeChildren };
238
+ }
239
+ function captureEvalRunSnapshot(context, input) {
240
+ const { projectRoot, project, status, recovery, output, workspaceRoot, activeChildren } = prepareEvalRunSnapshotCreation(context, input);
241
+ fs.mkdirSync(output, { recursive: true, mode: 0o700 });
242
+ const resourceDatabase = path.join(context.ddFlowHome, "runtime.sqlite");
243
+ const databaseFiles = [context.db.path, resourceDatabase].flatMap(file => [file, `${file}-wal`, `${file}-shm`]);
244
+ const codexExclusions = [...codexHomeSnapshotExclusions(context, project.id, input.runId), ...input.journalCuts?.keys() ?? [], ...input.observationCuts?.keys() ?? []];
245
+ if (recovery) {
246
+ if (typeof status.run.run_root !== "string" || !isInside(physicalPath(context.ddFlowHome), physicalPath(status.run.run_root)) || samePath(physicalPath(context.ddFlowHome), physicalPath(status.run.run_root)))
247
+ throw new AppError("snapshot_run_scope_invalid", "Recovery requires a RUN artifact root within its runtime", 1);
248
+ for (const source of new Set([status.run.run_root, runtimeRunDir(context.ddFlowHome, project.id, input.runId)])) {
249
+ if (fs.existsSync(source))
250
+ copyRuntimeTree(source, path.join(output, "runtime", path.relative(context.ddFlowHome, source)), true, codexExclusions, [], input.observationCuts);
251
+ }
252
+ }
253
+ else
254
+ copyRuntimeTree(context.ddFlowHome, path.join(output, "runtime"), false, [...databaseFiles, ...codexExclusions, ...(isSourceSnapshotFlow(status.run.flow_kind) ? [workspaceRoot] : [])], grokProviderHomes(context.ddFlowHome), input.observationCuts);
255
+ for (const [file, cut] of input.journalCuts ?? []) {
256
+ const target = path.join(output, "runtime", path.relative(context.ddFlowHome, file));
257
+ fs.mkdirSync(path.dirname(target), { recursive: true });
258
+ fs.writeFileSync(target, cut.payload, { mode: cut.mode });
259
+ }
260
+ // VACUUM INTO is a consistent SQLite read snapshot, including committed WAL
261
+ // pages. Copying the live database and its WAL separately is not atomic.
262
+ const databaseCopy = path.join(output, "runtime", path.relative(context.ddFlowHome, context.db.path));
263
+ fs.mkdirSync(path.dirname(databaseCopy), { recursive: true });
264
+ if (recovery) {
265
+ const report = typeof status.run.run_root === "string" ? path.join(output, "runtime", path.relative(context.ddFlowHome, status.run.run_root), "02-protocolize", "stage-report.json") : null;
266
+ if (report && !isInside(physicalPath(path.join(output, "runtime")), physicalPath(report)))
267
+ throw new AppError("snapshot_protocol_scope_invalid", "Recovery protocol evidence is outside the captured runtime", 1);
268
+ const protocols = report && fs.existsSync(report) ? JSON.parse(fs.readFileSync(report, "utf8")).semantic?.acceptance : [];
269
+ if (!Array.isArray(protocols) || !protocols.every(id => typeof id === "string" && id.startsWith("PRT-")))
270
+ throw new AppError("snapshot_protocol_scope_invalid", "Recovery export requires the accepted PROTOCOLIZE protocol IDs", 1);
271
+ captureRecoveryDatabase(context.db, databaseCopy, project.id, input.runId, protocols);
272
+ }
273
+ else
274
+ context.db.exec(`VACUUM INTO '${databaseCopy.replace(/'/g, "''")}'`);
275
+ // Resource heartbeats can continue after productive RUN writers are sealed.
276
+ // Keep their committed evidence consistent too; never copy a live WAL pair.
277
+ if (!recovery && fs.existsSync(resourceDatabase)) {
278
+ const resources = new DatabaseSync(resourceDatabase, { readOnly: true });
279
+ try {
280
+ const resourceCopy = path.join(output, "runtime", "runtime.sqlite");
281
+ resources.exec("PRAGMA busy_timeout = 4000");
282
+ resources.exec(`VACUUM INTO '${resourceCopy.replace(/'/g, "''")}'`);
283
+ }
284
+ finally {
285
+ resources.close();
286
+ }
287
+ }
88
288
  fs.cpSync(projectRoot, path.join(output, "project"), {
89
289
  recursive: true,
90
290
  verbatimSymlinks: true,
91
- filter: (source) => path.basename(source) !== ".git"
291
+ filter: projectPayloadFilter(projectRoot)
92
292
  });
93
- const projectGit = snapshotProjectGit(projectRoot, output);
94
- const workspace = snapshotWorkspace(workspaceRoot, projectRoot, output);
293
+ const projectGit = snapshotProjectGit(projectRoot, output, recovery && status.run.flow_kind === "mb-init");
294
+ const workspace = snapshotWorkspace(workspaceRoot, projectRoot, output, isSourceSnapshotFlow(status.run.flow_kind));
295
+ // Accepted Work results are captured in SQLite. Their file representations
296
+ // are retained evidence, not a second authority or a checkpoint gate.
297
+ if (recovery) {
298
+ const copiedContext = createContext({ ...context.env, DD_FLOW_HOME: path.join(output, "runtime") }, "read_existing");
299
+ try {
300
+ captureRecoveryAuxiliary(context, copiedContext, { projectId: project.id, runId: input.runId, output });
301
+ }
302
+ finally {
303
+ copiedContext.db.close?.();
304
+ }
305
+ }
95
306
  const manifest = {
96
- schema_id: "dd-flow/eval-run-snapshot@5",
307
+ 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",
97
308
  run_id: input.runId,
98
309
  project_id: project.id,
99
310
  project_root: projectRoot,
100
311
  dd_flow_home: context.ddFlowHome,
101
- purpose: input.candidate ? "candidate" : "stage_entry",
102
- stage_entry: input.candidate ? null : input.stageEntry,
312
+ purpose: input.incomplete ? "incomplete" : recovery ? "recovery" : input.candidate ? "candidate" : "stage_entry",
313
+ stage_entry: input.incomplete || recovery || input.candidate ? null : input.stageEntry,
314
+ ...(input.incomplete ? { consistency: "sqlite_read_snapshot_files_copied_nonatomically", active_child_work_count: activeChildren, restorable: false } : recovery ? { recovery_id: input.recoveryId, consistency: "sealed_writer_barrier_required", active_child_work_count: activeChildren, restorable: "same-runtime-or-explicit-restore" } : {}),
103
315
  created_at: context.now(),
104
316
  runtime_sha256: treeHash(path.join(output, "runtime")),
105
317
  project_sha256: treeHash(path.join(output, "project")),
318
+ project_payload_policy: "service-files@1",
319
+ ...(!recovery ? { runtime_payload_policy: "flow-evidence@1" } : {}),
320
+ ...(input.journalCuts ? { journal_cuts: [...input.journalCuts].map(([file, cut]) => ({ path: path.relative(context.ddFlowHome, file), bytes: cut.payload.length, sha256: cut.sha256 })) } : {}),
321
+ ...(input.observationCuts ? { observation_cuts: [...input.observationCuts].map(([file, receipt]) => ({ path: path.relative(context.ddFlowHome, file), receipt })) } : {}),
106
322
  project_git: projectGit,
107
323
  workspace,
324
+ payload_modes: { project: treeModesHash(path.join(output, "project")), runtime: treeModesHash(path.join(output, "runtime")), ...(workspace.kind !== "project" ? { workspace: treeModesHash(path.join(output, "workspace")) } : {}) },
108
325
  source_status: status
109
326
  };
110
327
  writeJson(path.join(output, "snapshot.json"), manifest);
@@ -118,57 +335,251 @@ function createdWorkSchemaForStage(stage) {
118
335
  return "dd-flow/merge-result";
119
336
  return null;
120
337
  }
121
- export function restoreEvalRunSnapshot(context, input) {
122
- const snapshot = path.resolve(input.snapshot);
338
+ /** Validate sealed source data before any target cleanup or copy. */
339
+ export function prepareEvalRunSnapshot(snapshot) {
123
340
  const manifest = readManifest(snapshot);
341
+ if (manifest.purpose === "incomplete")
342
+ throw new AppError("snapshot_incomplete_not_restorable", "Incomplete snapshot is forensic evidence, not a stage-entry fixture", 1, { snapshot });
343
+ if (manifest.purpose === "candidate")
344
+ throw new AppError("snapshot_candidate_not_restorable", "A terminal candidate snapshot is evidence, not a stage-entry fixture", 1, { snapshot });
345
+ verifySnapshotPayload(snapshot, manifest);
346
+ if (manifest.purpose === "stage_entry") {
347
+ const db = new DatabaseSync(path.join(snapshot, "runtime", "db.sqlite"), { readOnly: true });
348
+ try {
349
+ assertSnapshotRuntimeDetachAllowed(sql => db.prepare(sql).get());
350
+ const row = db.prepare("SELECT index_json FROM runs WHERE id = ?").get(manifest.run_id);
351
+ if (!row)
352
+ throw new AppError("snapshot_invalid", "Snapshot RUN is not registered", 1);
353
+ const index = JSON.parse(row.index_json);
354
+ if (!Array.isArray(index?.stage_runs))
355
+ throw new AppError("snapshot_invalid", "Snapshot RUN index is invalid", 1);
356
+ assertStageEntry(index.stage_runs, manifest.stage_entry);
357
+ }
358
+ finally {
359
+ db.close();
360
+ }
361
+ }
362
+ return manifest;
363
+ }
364
+ export function prepareEvalRunRestore(context, input, preparedManifest) {
365
+ const snapshot = path.resolve(input.snapshot);
366
+ const manifest = preparedManifest ?? prepareEvalRunSnapshot(snapshot);
367
+ if (manifest.purpose === "incomplete")
368
+ throw new AppError("snapshot_incomplete_not_restorable", "Incomplete snapshot is forensic evidence, not a stage-entry fixture", 1, { snapshot });
369
+ const recovery = manifest.purpose === "recovery";
370
+ if (recovery && input.executionRoutingFile !== undefined)
371
+ throw new AppError("snapshot_recovery_policy_frozen", "Recovery must retain its frozen execution policy", 2);
372
+ if (recovery && input.operationalDecisionFile !== undefined)
373
+ throw new AppError("snapshot_recovery_policy_frozen", "Recovery cannot replace its accepted operational decision", 2);
374
+ if (recovery && input.recoveryId !== manifest.recovery_id)
375
+ throw new AppError("snapshot_recovery_requires_explicit_restore", "Recovery snapshots require their explicit --recovery-id", 1, { snapshot });
376
+ if (!recovery && input.recoveryId)
377
+ throw new AppError("snapshot_invalid", "Recovery ID cannot restore a non-recovery snapshot", 1, { snapshot });
124
378
  if (manifest.purpose === "candidate")
125
379
  throw new AppError("snapshot_candidate_not_restorable", "A terminal candidate snapshot is evidence, not a stage-entry fixture", 1, { snapshot });
126
- if (!manifest.stage_entry)
380
+ if (!recovery && !manifest.stage_entry)
127
381
  throw new AppError("snapshot_invalid", "Stage-entry snapshot has no target stage", 1, { snapshot });
128
382
  const projectRoot = resolveProjectRoot(input.projectRoot);
129
- const existing = fs.readdirSync(context.ddFlowHome).filter((name) => !["db.sqlite", "db.sqlite-wal", "db.sqlite-shm"].includes(name));
130
- if (existing.length > 0)
131
- throw new AppError("restore_home_not_empty", "Snapshot restore requires an empty DD_FLOW_HOME", 1, { dd_flow_home: context.ddFlowHome, existing });
383
+ assertRunRestoreTargets(context, input, snapshot, projectRoot, manifest);
384
+ if (recovery)
385
+ verifyRecoverySnapshot(snapshot, { runId: manifest.run_id, recoveryId: input.recoveryId, projectRoot, workspaceRoot: projectRoot }, false);
132
386
  const sourceRuntime = path.join(snapshot, "runtime");
133
387
  const sourceProject = path.join(snapshot, "project");
134
- if (treeHash(sourceRuntime) !== manifest.runtime_sha256)
135
- throw new AppError("snapshot_checksum_mismatch", "Snapshot runtime checksum does not match manifest", 1, { snapshot });
136
- if (treeHash(sourceProject) !== manifest.project_sha256)
137
- throw new AppError("snapshot_checksum_mismatch", "Snapshot project checksum does not match manifest", 1, { snapshot });
138
- clearProjectTree(projectRoot, false);
139
- restoreProjectGit(snapshot, manifest.project_git, projectRoot);
140
- replaceProjectTree(projectRoot, sourceProject);
141
- context.db.close?.();
142
- fs.rmSync(context.ddFlowHome, { recursive: true, force: true });
143
- fs.cpSync(sourceRuntime, context.ddFlowHome, { recursive: true, verbatimSymlinks: true });
144
- const workspaceRoot = restoreWorkspace(snapshot, manifest, projectRoot, context.ddFlowHome);
145
- // The dispatch context was intentionally closed before replacing db.sqlite.
146
- // A fresh process owns all subsequent normal commands.
147
- const restored = createContext({ ...context.env, DD_FLOW_HOME: context.ddFlowHome });
388
+ const routingInput = input.executionRoutingFile !== undefined ? loadExecutionRoutingInput(input.executionRoutingFile) : undefined;
389
+ const acceptedOperational = recovery ? undefined : prepareSnapshotOperationalDecision({ snapshot, runId: manifest.run_id, projectRoot, now: context.now(), ...(input.operationalDecisionFile ? { file: input.operationalDecisionFile } : {}) });
390
+ const operationalDecisionBytes = acceptedOperational?.bytes;
391
+ let selectedProfile = acceptedOperational?.profile;
392
+ if (routingInput) {
393
+ // Resolve selected profiles before replacing the caller's fresh runtime.
394
+ const sourceDb = new DatabaseSync(path.join(sourceRuntime, "db.sqlite"), { readOnly: true });
395
+ try {
396
+ const row = sourceDb.prepare("SELECT index_json FROM runs WHERE id = ?").get(manifest.run_id);
397
+ const index = row ? JSON.parse(row.index_json) : null;
398
+ if (!index?.execution_profile || !["vnext_specify", "vnext_protocolize"].includes(index.flow_kind))
399
+ throw new AppError("execution_profile_not_frozen", "Stage-entry routing requires a vNext execution snapshot", 1);
400
+ assertStageEntry(index.stage_runs, manifest.stage_entry);
401
+ selectedProfile = freezeRunExecutionRouting(context.ddFlowHome, selectedProfile ?? index.execution_profile, routingInput);
402
+ for (const row of sourceDb.prepare("SELECT payload_json FROM works WHERE run_id = ? AND status = 'created'").all(manifest.run_id)) {
403
+ const payload = row.payload_json ? JSON.parse(row.payload_json) : null;
404
+ if (payload?.execution_override !== undefined)
405
+ resolveExecutionPolicy({ ddFlowHome: context.ddFlowHome, policy: routingInput.execution, profiles: selectedProfile.agent_profiles, stage: manifest.stage_entry, work: payload.execution_override });
406
+ }
407
+ selectedProfile.routing_input.artifact = `execution-routing.${routingInput.source_checksum}.input.json`;
408
+ const sourceRun = sourceDb.prepare("SELECT run_root FROM runs WHERE id = ?").get(manifest.run_id);
409
+ if (typeof sourceRun?.run_root !== "string" || !path.isAbsolute(sourceRun.run_root) || !isInside(manifest.dd_flow_home, sourceRun.run_root))
410
+ throw new AppError("snapshot_invalid", "Snapshot RUN root must be inside its runtime home", 1);
411
+ const existingRouting = path.join(sourceRuntime, path.relative(manifest.dd_flow_home, sourceRun.run_root), selectedProfile.routing_input.artifact);
412
+ if (fs.existsSync(existingRouting) && !fs.readFileSync(existingRouting).equals(routingInput.bytes))
413
+ throw new AppError("snapshot_checksum_mismatch", "Existing routing input does not match its checksum", 1);
414
+ }
415
+ finally {
416
+ sourceDb.close();
417
+ }
418
+ }
419
+ return { snapshot, manifest, recovery, projectRoot, sourceRuntime, sourceProject, routingInput, selectedProfile, operationalDecisionBytes };
420
+ }
421
+ /** Shared no-effect scope preparation for direct restore and run fork. */
422
+ export function prepareSnapshotOperationalDecision(input) {
423
+ const sourceDb = new DatabaseSync(path.join(input.snapshot, "runtime", "db.sqlite"), { readOnly: true });
148
424
  try {
149
- rebaseRuntime(restored, manifest.project_root, projectRoot, sourceWorkspaceRoot(manifest), workspaceRoot, manifest.dd_flow_home, context.ddFlowHome);
150
- registerProject(restored, { root: projectRoot });
151
- requireProjectByRoot(restored, projectRoot);
152
- const status = getFlowRunStatus(restored, { projectRoot, runId: manifest.run_id });
153
- assertStageEntry(status.index.stage_runs ?? [], manifest.stage_entry);
154
- if (!status.run.run_root)
155
- throw new AppError("runtime_missing", "Restored RUN has no artifact root", 1, { run_id: manifest.run_id });
156
- const runHome = status.run.run_root;
157
- return {
158
- ok: true,
159
- schema_id: "dd-flow/eval-run-restore@1",
160
- snapshot,
161
- run_id: status.run.id,
162
- project_root: projectRoot,
163
- workspace_root: workspaceRoot,
164
- dd_flow_home: context.ddFlowHome,
165
- run_home: runHome,
166
- target_stage: manifest.stage_entry,
167
- next_command: `dd-flow stage start ${status.run.id} --stage ${manifest.stage_entry} --project-root ${JSON.stringify(projectRoot)} --json`
168
- };
425
+ const row = sourceDb.prepare("SELECT index_json FROM runs WHERE id = ?").get(input.runId);
426
+ const profile = row ? JSON.parse(row.index_json).execution_profile : undefined;
427
+ if (profile?.operational_decision && !input.file)
428
+ throw new AppError("snapshot_operational_acceptance_required", "A new stage-entry execution needs its own accepted operational decision", 2);
429
+ if (!input.file)
430
+ return undefined;
431
+ if (!profile)
432
+ throw new AppError("execution_profile_not_frozen", "Operational stage-entry decision needs a frozen vNext profile", 2);
433
+ const bytes = fs.readFileSync(input.file);
434
+ const decision = validateRunOperationalDecision(JSON.parse(bytes.toString("utf8")), profile.settings);
435
+ if (canonicalPath(decision.scope.project_root, false) !== canonicalPath(input.projectRoot, false))
436
+ throw new AppError("operational_decision_scope_mismatch", "Imported decision must name the new project root", 2);
437
+ const sourceProject = path.join(input.snapshot, "project"), policyFile = path.resolve(sourceProject, decision.policy.path);
438
+ if (!isInside(sourceProject, policyFile) || !fs.existsSync(policyFile) || !isInside(fs.realpathSync(sourceProject), fs.realpathSync(policyFile)) || fileHash(policyFile) !== decision.policy.sha256)
439
+ throw new AppError("operational_decision_policy_changed", "Imported decision policy must match the owned frozen project source", 2);
440
+ if (decision.exception?.expires_at && Date.parse(decision.exception.expires_at) <= Date.parse(input.now))
441
+ throw new AppError("operational_decision_expired", "Imported operational decision has explicitly expired", 2);
442
+ return { profile: { ...profile, operational_decision: decision }, bytes };
169
443
  }
170
444
  finally {
171
- restored.db.close?.();
445
+ sourceDb.close();
446
+ }
447
+ }
448
+ function assertRunRestoreTargets(context, input, snapshot, projectRoot, manifest) {
449
+ assertRestoreTargetSeparation(snapshot, [projectRoot, context.ddFlowHome]);
450
+ if (manifest.workspace.kind !== "project") {
451
+ const workspaceRoot = restoredWorkspaceRoot(manifest, projectRoot, context.ddFlowHome);
452
+ assertRestoreTargetSeparation(snapshot, [workspaceRoot]);
453
+ if (isInside(physicalPath(projectRoot), physicalPath(workspaceRoot)) || isInside(physicalPath(workspaceRoot), physicalPath(projectRoot)) || physicalPath(workspaceRoot) === physicalPath(context.ddFlowHome))
454
+ throw new AppError("snapshot_path_conflict", "Feature workspace overlaps a restore target", 2);
455
+ if (fs.existsSync(workspaceRoot))
456
+ throw new AppError("snapshot_workspace_target_exists", "Snapshot feature workspace target already exists", 1, { workspace_root: workspaceRoot });
457
+ }
458
+ if (manifest.purpose === "recovery") {
459
+ if (fs.readdirSync(projectRoot).length || (context.db.get("SELECT COUNT(*) AS count FROM projects")?.count ?? 0) > 0)
460
+ throw new AppError("recovery_restore_target_not_empty", "Recovery restore requires fresh project and runtime targets", 1);
461
+ }
462
+ const existing = (fs.existsSync(context.ddFlowHome) ? fs.readdirSync(context.ddFlowHome) : []).filter((name) => !["db.sqlite", "db.sqlite-wal", "db.sqlite-shm", ...(input.executionRoutingFile !== undefined ? ["agent-profiles"] : [])].includes(name));
463
+ if (existing.length > 0)
464
+ throw new AppError("restore_home_not_empty", "Snapshot restore requires an empty DD_FLOW_HOME", 1, { dd_flow_home: context.ddFlowHome, existing });
465
+ if (context.db.get("SELECT 1 FROM projects LIMIT 1") || context.db.get("SELECT 1 FROM runs LIMIT 1"))
466
+ throw new AppError("restore_home_not_empty", "Snapshot restore cannot replace an initialized runtime", 1);
467
+ }
468
+ export function restoreEvalRunSnapshot(context, input, prepared = prepareEvalRunRestore(context, input), prearmedImportId) {
469
+ const { snapshot, manifest, recovery, projectRoot, sourceRuntime, sourceProject, routingInput, selectedProfile } = prepared;
470
+ if (input.operationalDecisionFile && (!prepared.operationalDecisionBytes || !fs.readFileSync(input.operationalDecisionFile).equals(prepared.operationalDecisionBytes)))
471
+ throw new AppError("operational_decision_changed", "Accepted operational decision changed after import preparation", 2);
472
+ assertRunRestoreTargets(context, input, snapshot, projectRoot, manifest);
473
+ const sourceManifestSha256 = fileHash(path.join(snapshot, "snapshot.json"));
474
+ const importId = prearmedImportId ?? beginSnapshotImport({ home: context.ddFlowHome, projectRoot,
475
+ workspaceRoot: restoredWorkspaceRoot(manifest, projectRoot, context.ddFlowHome), sourceManifestSha256 });
476
+ try {
477
+ clearProjectTree(projectRoot, false);
478
+ restoreProjectGit(snapshot, manifest.project_git, projectRoot);
479
+ replaceProjectTree(projectRoot, sourceProject);
480
+ context.db.close?.();
481
+ fs.rmSync(context.ddFlowHome, { recursive: true, force: true });
482
+ fs.cpSync(sourceRuntime, context.ddFlowHome, { recursive: true, verbatimSymlinks: true });
483
+ const workspaceRoot = restoreWorkspace(snapshot, manifest, projectRoot, context.ddFlowHome);
484
+ // The dispatch context was intentionally closed before replacing db.sqlite.
485
+ // A fresh process owns all subsequent normal commands.
486
+ const restored = createContext({ ...context.env, DD_FLOW_HOME: context.ddFlowHome }, "initialize", importId);
487
+ let result, primary, failed = false;
488
+ try {
489
+ const runtimeLineage = recovery ? null : detachSnapshotRuntimeOwners(restored, snapshot, manifest);
490
+ const originalRunRoot = restored.db.get("SELECT run_root FROM runs WHERE id = ?", [manifest.run_id])?.run_root;
491
+ rebaseRuntime(restored, manifest.project_root, projectRoot, sourceWorkspaceRoot(manifest), workspaceRoot, manifest.dd_flow_home, context.ddFlowHome, recovery);
492
+ const publicationRun = restored.db.get("SELECT project_id, run_root FROM runs WHERE id = ?", [manifest.run_id]);
493
+ if (!originalRunRoot || !publicationRun?.run_root || !isInside(manifest.dd_flow_home, originalRunRoot) || !isInside(context.ddFlowHome, publicationRun.run_root))
494
+ throw new AppError("snapshot_invalid", "Restored publication roots are not bound to their runtime", 1);
495
+ const pathMapping = [{ from: manifest.dd_flow_home, to: context.ddFlowHome }, { from: sourceWorkspaceRoot(manifest), to: workspaceRoot }, { from: manifest.project_root, to: projectRoot }];
496
+ 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()]);
497
+ registerProject(restored, { root: projectRoot });
498
+ requireProjectByRoot(restored, projectRoot);
499
+ if (selectedProfile && runtimeLineage) {
500
+ const run = restored.db.get("SELECT run_root FROM runs WHERE id = ?", [manifest.run_id]);
501
+ if (routingInput) {
502
+ const artifact = path.join(run.run_root, selectedProfile.routing_input.artifact);
503
+ if (fs.existsSync(artifact)) {
504
+ if (!fs.readFileSync(artifact).equals(routingInput.bytes))
505
+ throw new AppError("snapshot_checksum_mismatch", "Existing routing input does not match its checksum", 1);
506
+ }
507
+ else
508
+ fs.writeFileSync(artifact, routingInput.bytes, { flag: "wx", mode: 0o600 });
509
+ }
510
+ if (input.operationalDecisionFile) {
511
+ if (!prepared.operationalDecisionBytes || !fs.readFileSync(input.operationalDecisionFile).equals(prepared.operationalDecisionBytes))
512
+ throw new AppError("operational_decision_changed", "Accepted operational decision changed before import publication", 2);
513
+ const binding = { project_id: publicationRun.project_id, run_id: manifest.run_id, ...selectedProfile.operational_decision.scope };
514
+ const policyBytes = fs.readFileSync(path.resolve(projectRoot, selectedProfile.operational_decision.policy.path));
515
+ const policySource = { path: `operational-policy.${selectedProfile.operational_decision.policy.sha256}.source.md`, sha256: crypto.createHash("sha256").update(policyBytes).digest("hex") };
516
+ if (policySource.sha256 !== selectedProfile.operational_decision.policy.sha256)
517
+ throw new AppError("operational_decision_policy_changed", "Imported policy changed before binding", 2);
518
+ if (!fs.existsSync(path.join(run.run_root, policySource.path)))
519
+ fs.writeFileSync(path.join(run.run_root, policySource.path), policyBytes, { flag: "wx" });
520
+ 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`;
521
+ const artifact = `operational-contract.${crypto.createHash("sha256").update(bytes).digest("hex")}.json`;
522
+ fs.writeFileSync(path.join(run.run_root, artifact), bytes, { flag: "wx" });
523
+ selectedProfile.operational_contract = { path: artifact, sha256: crypto.createHash("sha256").update(bytes).digest("hex"), policy_source: policySource, binding };
524
+ }
525
+ importFlowRunExecutionProfile(restored, { projectRoot, runId: manifest.run_id, stage: manifest.stage_entry, lineageFile: runtimeLineage, profile: selectedProfile });
526
+ }
527
+ const relocation = recovery ? {
528
+ schema_id: "dd-flow/recovery-relocation@1", physical_runtime_id: crypto.randomUUID(), recovery_id: input.recoveryId, run_id: manifest.run_id,
529
+ path_mapping: [{ from: manifest.dd_flow_home, to: context.ddFlowHome }, { from: sourceWorkspaceRoot(manifest), to: workspaceRoot }, { from: manifest.project_root, to: projectRoot }]
530
+ } : null;
531
+ const relocationFile = relocation ? path.join(context.ddFlowHome, "recovery-imports", `${relocation.physical_runtime_id}.json`) : null;
532
+ if (relocationFile) {
533
+ fs.mkdirSync(path.dirname(relocationFile), { recursive: true });
534
+ writeJson(relocationFile, relocation);
535
+ }
536
+ const status = getFlowRunStatus(restored, { projectRoot, runId: manifest.run_id });
537
+ if (!recovery)
538
+ assertStageEntry(status.index.stage_runs ?? [], manifest.stage_entry);
539
+ else
540
+ restored.db.run("UPDATE run_recovery_guards SET capture_path = ? WHERE recovery_id = ? AND status = 'sealed'", [snapshot, input.recoveryId]);
541
+ if (!status.run.run_root)
542
+ throw new AppError("runtime_missing", "Restored RUN has no artifact root", 1, { run_id: manifest.run_id });
543
+ const runHome = status.run.run_root;
544
+ const expectedWorkspaceRoot = restoredWorkspaceRoot(manifest, projectRoot, context.ddFlowHome);
545
+ if (physicalPath(workspaceRoot) !== physicalPath(expectedWorkspaceRoot))
546
+ throw new AppError("snapshot_workspace_restore_failed", "Restored workspace differs from the import target", 1, { workspace_root: workspaceRoot, expected_workspace_root: expectedWorkspaceRoot });
547
+ completeSnapshotImport(context.ddFlowHome, importId, { projectRoot, workspaceRoot: expectedWorkspaceRoot, sourceManifestSha256 });
548
+ result = {
549
+ ok: true,
550
+ schema_id: "dd-flow/eval-run-restore@1",
551
+ snapshot,
552
+ run_id: status.run.id,
553
+ project_root: projectRoot,
554
+ workspace_root: workspaceRoot,
555
+ dd_flow_home: context.ddFlowHome,
556
+ run_home: runHome,
557
+ target_stage: manifest.stage_entry,
558
+ ...(runtimeLineage ? { runtime_lineage_file: runtimeLineage } : {}),
559
+ ...(recovery ? { recovery_id: input.recoveryId, native_session_portability: "requires_adapter_verification", relocation_file: relocationFile, relocation } : {}),
560
+ 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`
561
+ };
562
+ }
563
+ catch (error) {
564
+ primary = error;
565
+ failed = true;
566
+ }
567
+ const cleanup = cleanupScopeWriters([{ name: restored.ddFlowHome, db: restored.db, rollback: false, close: "always" }]);
568
+ if (failed) {
569
+ if (primary instanceof AppError && cleanup.length)
570
+ primary.details.import_cleanup_errors = cleanup;
571
+ throw primary;
572
+ }
573
+ if (cleanup.length)
574
+ 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 });
575
+ return result;
576
+ }
577
+ catch (error) {
578
+ // The import fence and target writes have already started. A nested
579
+ // prepare/read validator must not report this operation as no-effect.
580
+ if (error instanceof AppError)
581
+ Object.assign(error.details, { phase: "restore", effect: error.details.effect === "committed" ? "committed" : "partial", import_id: importId, next_action: "reconcile_snapshot_import" });
582
+ throw error;
172
583
  }
173
584
  }
174
585
  export function prepareVnextSpecifyRun(context, input) {
@@ -182,6 +593,7 @@ export function prepareVnextSpecifyRun(context, input) {
182
593
  subjectType: "discussion",
183
594
  subjectId: "SPECIFY",
184
595
  slug: input.slug,
596
+ executionRoutingFile: input.executionRoutingFile,
185
597
  nextAction: "start_specify"
186
598
  });
187
599
  return { ok: true, run_id: started.run.id, run_root: started.run.run_root, target_stage: "specify" };
@@ -196,19 +608,91 @@ function assertDedicatedHome(context, projectId, runId) {
196
608
  function assertStageEntry(stages, target) {
197
609
  if (stages.some((entry) => entry.stage === target))
198
610
  throw new AppError("snapshot_target_started", "Stage-entry snapshot requires its target stage to be unstarted", 1, { target });
199
- const unfinished = stages.filter((entry) => entry.status !== "done");
611
+ const unfinished = stages.filter((entry) => !["done", "skipped"].includes(entry.status ?? ""));
200
612
  if (unfinished.length > 0)
201
613
  throw new AppError("snapshot_predecessor_incomplete", "Stage-entry snapshot requires completed predecessor stages", 1, { target, unfinished });
202
614
  }
203
615
  function assertTerminalCandidate(stages) {
204
- const unfinished = stages.filter((entry) => entry.status !== "done");
616
+ const unfinished = stages.filter((entry) => !["done", "skipped"].includes(entry.status ?? ""));
205
617
  if (unfinished.length > 0)
206
618
  throw new AppError("snapshot_candidate_incomplete", "Candidate snapshot requires every started stage to be done", 1, { unfinished });
207
619
  }
208
- function rebaseRuntime(context, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome) {
209
- context.db.exec("PRAGMA foreign_keys = OFF");
210
- context.db.exec("BEGIN IMMEDIATE");
620
+ /** A stage-entry import is a new experiment, not a lease takeover or recovery.
621
+ * Keep source authority as evidence, never as executable target inventory. */
622
+ function assertSnapshotRuntimeDetachAllowed(get) {
623
+ if (get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_recovery_guards'") && get("SELECT 1 FROM run_recovery_guards WHERE status IN ('draining','sealed','resuming') LIMIT 1"))
624
+ throw new AppError("snapshot_control_requires_recovery", "A fenced RUN requires explicit recovery, not a stage-entry fork", 1);
625
+ if (get("SELECT 1 FROM merge_requests WHERE status IN ('dispatching','active','waiting_user','action_required','recovery_required') LIMIT 1"))
626
+ throw new AppError("snapshot_execution_active", "A stage-entry fork cannot discard an unresolved MERGE execution", 1);
627
+ }
628
+ function detachSnapshotRuntimeOwners(context, snapshot, manifest) {
629
+ const hasTable = (table) => Boolean(context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = ?", [table]));
630
+ assertSnapshotRuntimeDetachAllowed(sql => context.db.get(sql));
631
+ const detachedTables = ["lifecycle_packet_assignments", "lifecycle_invocations", "run_control_admissions", "run_control_stage_admissions", "run_control_operations", "run_control_workers", "run_controls", "run_controller_events", "run_controller_operations", "run_controllers", "run_recovery_bindings", "work_recovery_segments", "run_recovery_guards", "work_launches", "merge_servers", "merge_sessions", "lane_locks", "lane_waiters"].filter(hasTable);
632
+ const runTables = Object.fromEntries([...detachedTables, "runs", "merge_requests", "sessions", "work_sessions"].map(table => [table, context.db.all(`SELECT * FROM ${table}`)]));
633
+ const resourceFile = path.join(context.ddFlowHome, "runtime.sqlite");
634
+ const resources = fs.existsSync(resourceFile) ? new DatabaseSync(resourceFile) : null;
635
+ let resourceHeld = false, resourceCommitted = false, result, primary, failed = false;
211
636
  try {
637
+ if (resources)
638
+ registerStoreWriter(resources);
639
+ const resourceTables = Object.fromEntries(["managed_resources", "managed_processes"].filter(table => resources?.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = ?").get(table)).map(table => [table, resources.prepare(`SELECT * FROM ${table}`).all()]));
640
+ const directory = path.join(context.ddFlowHome, "snapshot-imports");
641
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
642
+ const file = path.join(directory, `${crypto.randomUUID()}.json`);
643
+ fs.writeFileSync(file, `${JSON.stringify({ schema_id: "dd-flow/snapshot-runtime-lineage@1", source_snapshot: snapshot, source_manifest_sha256: fileHash(path.join(snapshot, "snapshot.json")), source_runtime: manifest.dd_flow_home, restored_runtime: context.ddFlowHome, run_id: manifest.run_id, run_tables: runTables, resource_tables: resourceTables }, null, 2)}\n`, { flag: "wx", mode: 0o600 });
644
+ syncPath(file);
645
+ syncPath(directory);
646
+ const lineage = JSON.stringify({ lineage_file: file, lineage_sha256: fileHash(file) });
647
+ context.db.writeTransaction(() => {
648
+ assertSnapshotRuntimeDetachAllowed(sql => context.db.get(sql));
649
+ for (const [table, rows] of Object.entries(runTables))
650
+ if (JSON.stringify(context.db.all(`SELECT * FROM ${table}`)) !== JSON.stringify(rows))
651
+ throw new AppError("snapshot_runtime_changed", "Imported runtime changed during lineage preparation", 1, { table });
652
+ context.db.run("UPDATE sessions SET metadata_json = json_set(metadata_json, '$.snapshot_import', json(?))", [lineage]);
653
+ for (const table of detachedTables) {
654
+ try {
655
+ context.db.exec(`DELETE FROM ${table}`);
656
+ }
657
+ catch (error) {
658
+ throw new AppError("snapshot_runtime_detach_failed", `Could not detach imported ${table} authority`, 1, { table, cause: String(error) });
659
+ }
660
+ }
661
+ context.db.run("UPDATE merge_requests SET dispatch_owner = NULL, dispatch_lease_token = NULL, dispatch_lease_expires_at = NULL, lock_acquired_at = NULL, adapter_receipt_json = NULL");
662
+ // Only the copied dedicated registry is touched. Source/external homes
663
+ // are historical references in the archive and are never opened here.
664
+ if (resources) {
665
+ resources.exec("BEGIN IMMEDIATE");
666
+ resourceHeld = true;
667
+ for (const [table, rows] of Object.entries(resourceTables)) {
668
+ if (JSON.stringify(resources.prepare(`SELECT * FROM ${table}`).all()) !== JSON.stringify(rows))
669
+ throw new AppError("snapshot_runtime_changed", "Imported resource evidence changed during lineage preparation", 1, { table });
670
+ resources.exec(`DELETE FROM ${table}`);
671
+ }
672
+ resources.exec("COMMIT");
673
+ resourceHeld = false;
674
+ resourceCommitted = true;
675
+ }
676
+ });
677
+ result = file;
678
+ }
679
+ catch (error) {
680
+ primary = error;
681
+ failed = true;
682
+ }
683
+ const cleanup = resources ? cleanupScopeWriters([{ name: resourceFile, db: resources, rollback: resourceHeld, close: "always" }]) : [];
684
+ if (failed || cleanup.length) {
685
+ const details = { effect: resourceCommitted ? "partial" : "unknown", ...(cleanup.length ? { import_cleanup_errors: cleanup } : {}) };
686
+ if (primary instanceof AppError) {
687
+ Object.assign(primary.details, details);
688
+ throw primary;
689
+ }
690
+ throw new AppError("snapshot_runtime_detach_failed", primary instanceof Error ? primary.message : "Imported runtime authority could not be detached", 1, { ...details, ...(failed ? { cause: String(primary) } : {}) });
691
+ }
692
+ return result;
693
+ }
694
+ function rebaseRuntime(context, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome, recovery = false) {
695
+ foreignKeyTransaction(context.db, () => {
212
696
  context.db.run("UPDATE projects SET root = ?", [newRoot]);
213
697
  context.db.run("UPDATE runs SET project_root = ?, workspace_root = ?", [newRoot, newWorkspace]);
214
698
  for (const column of ["runtime_path", "run_dir", "run_index_path", "run_home_path", "run_root", "index_json"])
@@ -216,7 +700,12 @@ function rebaseRuntime(context, oldRoot, newRoot, oldWorkspace, newWorkspace, ol
216
700
  context.db.run("UPDATE protocols SET project_root = ?", [newRoot]);
217
701
  for (const column of ["state_path", "plan_path"])
218
702
  rewriteDatabaseColumn(context, "protocols", column, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome);
219
- context.db.run("UPDATE sessions SET project_root = ?, status = CASE WHEN status = 'active' THEN 'stopped' ELSE status END, stop_reason = CASE WHEN status = 'active' THEN 'snapshot_restored' ELSE stop_reason END", [newRoot]);
703
+ for (const column of ["source_workspace", "target_workspace"])
704
+ rewriteDatabaseColumn(context, "merge_requests", column, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome);
705
+ if (recovery)
706
+ context.db.run("UPDATE sessions SET project_root = ?, status = CASE WHEN status = 'active' THEN 'stopped' ELSE status END, stop_reason = CASE WHEN status = 'active' THEN 'snapshot_restored' ELSE stop_reason END", [newRoot]);
707
+ else
708
+ context.db.run("UPDATE sessions SET project_root = ?, status = 'stopped', stop_reason = 'snapshot_restored', stopped_at = ?", [newRoot, context.now()]);
220
709
  for (const column of ["workspace_path", "cwd", "transcript_path"])
221
710
  rewriteDatabaseColumn(context, "sessions", column, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome);
222
711
  for (const column of ["prompt_path", "result_path"])
@@ -226,47 +715,55 @@ function rebaseRuntime(context, oldRoot, newRoot, oldWorkspace, newWorkspace, ol
226
715
  rewriteDatabaseColumn(context, "usage", "source_locator", oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome);
227
716
  for (const column of ["worktree_path", "worktrunk_metadata_json", "last_command_result_json"])
228
717
  rewriteDatabaseColumn(context, "worktree_records", column, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome);
229
- context.db.run("UPDATE work_sessions SET status = CASE WHEN status = 'running' THEN 'completed' ELSE status END, completed_at = COALESCE(completed_at, ?), updated_at = ?", [context.now(), context.now()]);
718
+ // Recovery imports remain sealed. Their old Work sessions are historical
719
+ // bindings, not completed work; explicit recovery closes them interrupted.
720
+ if (!recovery)
721
+ context.db.run("UPDATE work_sessions SET status = CASE WHEN status = 'running' THEN 'completed' ELSE status END, completed_at = COALESCE(completed_at, ?), updated_at = ?", [context.now(), context.now()]);
230
722
  context.db.run("DELETE FROM codex_session_bindings");
231
- context.db.run("COMMIT");
232
- }
233
- catch (error) {
234
- context.db.exec("ROLLBACK");
235
- throw error;
236
- }
237
- finally {
238
- context.db.exec("PRAGMA foreign_keys = ON");
239
- }
240
- rewriteStructuredFiles(context.ddFlowHome, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome);
723
+ });
724
+ // Only operational database paths move. Imported packets, results and
725
+ // journals retain their historical bytes, including interrupted JSONL tails.
726
+ // The frozen workspace route is an operational receipt consumed by later
727
+ // stages, so its relocated path is the sole structured-file exception.
728
+ rewriteOperationalRouteReceipts(context.ddFlowHome, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome);
241
729
  }
242
- function rewriteStructuredFiles(root, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome) {
243
- const visit = (dir) => {
244
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
245
- const file = path.join(dir, entry.name);
246
- // DD_FLOW_HOME also contains the restored checkout and installed tools.
247
- // They are project/tool bytes, not flow records; parsing every *.json
248
- // there breaks on valid JSON-with-comments such as tsconfig files.
249
- if (entry.isDirectory() && ![".git", "checkouts", "engines", "node_modules", "tools"].includes(entry.name))
730
+ function rewriteOperationalRouteReceipts(root, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome) {
731
+ const visit = (directory) => {
732
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
733
+ const file = path.join(directory, entry.name);
734
+ if (entry.isDirectory())
250
735
  visit(file);
251
- else if (entry.isFile() && entry.name.endsWith(".json")) {
736
+ else if (entry.isFile() && entry.name === "workspace-route.json") {
252
737
  const value = JSON.parse(fs.readFileSync(file, "utf8"));
253
- writeJson(file, rebaseStructuredValue(value, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome));
254
- }
255
- else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
256
- const lines = fs.readFileSync(file, "utf8").split("\n").filter(Boolean).map((line) => JSON.stringify(rebaseStructuredValue(JSON.parse(line), oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome)));
257
- fs.writeFileSync(file, `${lines.join("\n")}\n`);
738
+ fs.writeFileSync(file, `${JSON.stringify(rebaseStructuredValue(value, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome), null, 2)}\n`);
258
739
  }
259
740
  }
260
741
  };
261
742
  visit(root);
262
743
  }
263
- function rewriteDatabaseColumn(context, table, column, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome) { for (const row of context.db.all(`SELECT rowid AS rid, ${column} AS value FROM ${table} WHERE ${column} IS NOT NULL`)) {
264
- if (!row.value)
265
- continue;
266
- const value = column.endsWith("_json") || column === "index_json" ? JSON.stringify(rebaseStructuredValue(JSON.parse(row.value), oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome)) : rebasePathString(row.value, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome);
267
- if (value !== row.value)
268
- context.db.run(`UPDATE ${table} SET ${column} = ? WHERE rowid = ?`, [value, row.rid]);
269
- } }
744
+ function rewriteDatabaseColumn(context, table, column, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome) {
745
+ for (const row of context.db.all(`SELECT rowid AS rid, ${column} AS value FROM ${table} WHERE ${column} IS NOT NULL`)) {
746
+ if (!row.value)
747
+ continue;
748
+ let value;
749
+ if (column.endsWith("_json") || column === "index_json") {
750
+ const original = JSON.parse(row.value);
751
+ const mapped = rebaseStructuredValue(original, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome);
752
+ if (table === "runs" && column === "index_json" && original.execution_profile) {
753
+ // Accepted source/binding are logical provenance, not mutable physical locators.
754
+ if (original.execution_profile.operational_decision)
755
+ mapped.execution_profile.operational_decision = original.execution_profile.operational_decision;
756
+ if (original.execution_profile.operational_contract)
757
+ mapped.execution_profile.operational_contract = original.execution_profile.operational_contract;
758
+ }
759
+ value = JSON.stringify(mapped);
760
+ }
761
+ else
762
+ value = rebasePathString(row.value, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome);
763
+ if (value !== row.value)
764
+ context.db.run(`UPDATE ${table} SET ${column} = ? WHERE rowid = ?`, [value, row.rid]);
765
+ }
766
+ }
270
767
  function rebaseStructuredValue(value, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome) { if (typeof value === "string")
271
768
  return rebasePathString(value, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome); if (Array.isArray(value))
272
769
  return value.map((item) => rebaseStructuredValue(item, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome)); if (value && typeof value === "object")
@@ -274,20 +771,29 @@ function rebaseStructuredValue(value, oldRoot, newRoot, oldWorkspace, newWorkspa
274
771
  function rebasePathString(value, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome) { for (const [oldPrefix, newPrefix] of [[oldHome, newHome], [oldWorkspace, newWorkspace], [oldRoot, newRoot]])
275
772
  if (value === oldPrefix || value.startsWith(`${oldPrefix}${path.sep}`))
276
773
  return `${newPrefix}${value.slice(oldPrefix.length)}`; return value; }
277
- function snapshotWorkspace(workspaceRoot, projectRoot, output) {
774
+ function prepareSnapshotWorkspace(workspaceRoot, projectRoot, allowUnborn = false) {
278
775
  if (samePath(workspaceRoot, projectRoot))
279
- return { kind: "project" };
776
+ return null;
280
777
  const branch = git(workspaceRoot, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
281
778
  if (!branch)
282
779
  throw new AppError("snapshot_workspace_invalid", "Feature workspace must be on a named Git branch", 1, { workspace_root: workspaceRoot });
283
780
  const baseRef = git(workspaceRoot, ["rev-parse", "HEAD"]);
284
- if (!baseRef)
781
+ if (!baseRef && !(allowUnborn && git(workspaceRoot, ['for-each-ref', '--format=%(refname)']) === ''))
285
782
  throw new AppError("snapshot_workspace_invalid", "Feature workspace has no readable HEAD", 1, { workspace_root: workspaceRoot });
783
+ return { branch, baseRef };
784
+ }
785
+ function snapshotWorkspace(workspaceRoot, projectRoot, output, independent = false) {
786
+ const prepared = prepareSnapshotWorkspace(workspaceRoot, projectRoot, independent);
787
+ if (!prepared)
788
+ return { kind: "project" };
789
+ const { branch, baseRef } = prepared;
286
790
  const workspace = path.join(output, "workspace");
287
791
  const bundle = path.join(output, "workspace.bundle");
288
- fs.cpSync(workspaceRoot, workspace, { recursive: true, verbatimSymlinks: true, filter: (source) => path.basename(source) !== ".git" });
289
- runGit(projectRoot, ["bundle", "create", bundle, branch]);
290
- return { kind: "git_worktree", source_root: workspaceRoot, branch, base_ref: baseRef, sha256: treeHash(workspace), bundle_sha256: fileHash(bundle) };
792
+ fs.cpSync(workspaceRoot, workspace, { recursive: true, verbatimSymlinks: true, filter: projectPayloadFilter(workspaceRoot) });
793
+ if (baseRef === null)
794
+ return { kind: 'git_repository', source_root: workspaceRoot, branch, base_ref: null, bundle_sha256: null, state: 'unborn', sha256: treeHash(workspace), ...snapshotIndex(workspaceRoot, output, 'workspace') };
795
+ runGit(independent ? workspaceRoot : projectRoot, ["bundle", "create", bundle, branch]);
796
+ 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") };
291
797
  }
292
798
  function restoreWorkspace(snapshot, manifest, projectRoot, ddFlowHome) {
293
799
  if (manifest.workspace.kind === "project")
@@ -296,42 +802,250 @@ function restoreWorkspace(snapshot, manifest, projectRoot, ddFlowHome) {
296
802
  const bundle = path.join(snapshot, "workspace.bundle");
297
803
  if (treeHash(source) !== manifest.workspace.sha256)
298
804
  throw new AppError("snapshot_checksum_mismatch", "Snapshot workspace checksum does not match manifest", 1, { snapshot });
299
- if (!fs.existsSync(bundle) || fileHash(bundle) !== manifest.workspace.bundle_sha256)
805
+ if (manifest.workspace.state === 'unborn' ? fs.existsSync(bundle) : !fs.existsSync(bundle) || fileHash(bundle) !== manifest.workspace.bundle_sha256)
300
806
  throw new AppError("snapshot_checksum_mismatch", "Snapshot workspace Git bundle does not match manifest", 1, { snapshot });
301
- const managedRoot = physicalPath(manifest.dd_flow_home);
302
- const sourceRoot = physicalPath(manifest.workspace.source_root);
303
- const workspaceRoot = isInside(managedRoot, sourceRoot)
304
- ? path.join(ddFlowHome, path.relative(managedRoot, sourceRoot))
305
- : path.join(path.dirname(projectRoot), "workspace");
807
+ const workspaceRoot = restoredWorkspaceRoot(manifest, projectRoot, ddFlowHome);
306
808
  if (fs.existsSync(workspaceRoot))
307
809
  throw new AppError("snapshot_workspace_target_exists", "Snapshot feature workspace target already exists", 1, { workspace_root: workspaceRoot });
308
- runGit(projectRoot, ["fetch", bundle, `+refs/heads/${manifest.workspace.branch}:refs/heads/${manifest.workspace.branch}`]);
309
- if (git(projectRoot, ["rev-parse", `refs/heads/${manifest.workspace.branch}`]) !== manifest.workspace.base_ref)
310
- 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 });
311
- runGit(projectRoot, ["worktree", "add", workspaceRoot, manifest.workspace.branch]);
810
+ if (manifest.workspace.kind === 'git_repository') {
811
+ fs.mkdirSync(path.dirname(workspaceRoot), { recursive: true });
812
+ if (manifest.workspace.state === 'unborn') {
813
+ fs.mkdirSync(workspaceRoot);
814
+ runGit(workspaceRoot, ['-c', 'init.templateDir=', 'init', '--initial-branch', manifest.workspace.branch]);
815
+ }
816
+ else
817
+ runGit(projectRoot, ['clone', '--no-checkout', '--branch', manifest.workspace.branch, bundle, workspaceRoot]);
818
+ }
819
+ else {
820
+ runGit(projectRoot, ["fetch", bundle, `+refs/heads/${manifest.workspace.branch}:refs/heads/${manifest.workspace.branch}`]);
821
+ if (git(projectRoot, ["rev-parse", `refs/heads/${manifest.workspace.branch}`]) !== manifest.workspace.base_ref)
822
+ 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 });
823
+ runGit(projectRoot, ["worktree", "add", workspaceRoot, manifest.workspace.branch]);
824
+ }
825
+ if (git(workspaceRoot, ["rev-parse", "HEAD"]) !== manifest.workspace.base_ref)
826
+ 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 });
312
827
  replaceProjectTree(workspaceRoot, source);
828
+ restoreIndex(snapshot, "workspace", manifest.workspace, workspaceRoot);
313
829
  return fs.realpathSync(workspaceRoot);
314
830
  }
831
+ export function restoredWorkspaceRoot(manifest, projectRoot, ddFlowHome) {
832
+ if (manifest.workspace.kind === "project")
833
+ return projectRoot;
834
+ const managedRoot = physicalPath(manifest.dd_flow_home), sourceRoot = physicalPath(manifest.workspace.source_root);
835
+ return isInside(managedRoot, sourceRoot) ? path.join(ddFlowHome, path.relative(managedRoot, sourceRoot)) : path.join(path.dirname(projectRoot), "workspace");
836
+ }
837
+ function assertRestoreTargetSeparation(snapshot, targets) {
838
+ const roots = [snapshot, ...targets].map(physicalPath);
839
+ for (let i = 0; i < roots.length; i++)
840
+ for (let j = i + 1; j < roots.length; j++) {
841
+ if (isInside(roots[i], roots[j]) || isInside(roots[j], roots[i]))
842
+ throw new AppError("snapshot_path_conflict", "Snapshot source and restore targets must not overlap", 2, { source: roots[i], target: roots[j] });
843
+ }
844
+ }
315
845
  function sourceWorkspaceRoot(manifest) {
316
846
  return manifest.workspace.kind === "project" ? manifest.project_root : manifest.workspace.source_root;
317
847
  }
318
- function snapshotProjectGit(projectRoot, output) {
848
+ function prepareSnapshotProjectGit(projectRoot, allowUnborn = false) {
319
849
  const branch = git(projectRoot, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
320
- const head = git(projectRoot, ["rev-parse", "HEAD"]);
321
- if (!branch || !head)
322
- throw new AppError("snapshot_project_git_invalid", "Snapshot requires a project on a named Git branch with a readable HEAD", 1, { project_root: projectRoot });
850
+ const head = git(projectRoot, ["rev-parse", "--verify", "HEAD"]);
851
+ if (!branch || !head && !(allowUnborn && git(projectRoot, ["for-each-ref", "--format=%(refname)"]) === ""))
852
+ throw new AppError("snapshot_project_git_invalid", "Snapshot requires a named Git branch and readable HEAD, or an admitted empty-ref Memory recovery", 1, { project_root: projectRoot });
853
+ return { branch, head };
854
+ }
855
+ function snapshotProjectGit(projectRoot, output, allowUnborn = false) {
856
+ const { branch, head } = prepareSnapshotProjectGit(projectRoot, allowUnborn);
857
+ if (head === null)
858
+ return { branch, head: null, bundle_sha256: null, state: "unborn", ...snapshotIndex(projectRoot, output, "project") };
323
859
  const bundle = path.join(output, "project.bundle");
324
- runGit(projectRoot, ["bundle", "create", bundle, "--all"]);
325
- return { branch, head, bundle_sha256: fileHash(bundle) };
860
+ runGit(projectRoot, ["bundle", "create", bundle, `refs/heads/${branch}`]);
861
+ return { branch, head, bundle_sha256: fileHash(bundle), ...snapshotIndex(projectRoot, output, "project") };
326
862
  }
327
863
  function restoreProjectGit(snapshot, source, projectRoot) {
328
864
  const bundle = path.join(snapshot, "project.bundle");
329
- if (!fs.existsSync(bundle) || fileHash(bundle) !== source.bundle_sha256)
865
+ if (source.state === "unborn" ? fs.existsSync(bundle) : !fs.existsSync(bundle) || fileHash(bundle) !== source.bundle_sha256)
330
866
  throw new AppError("snapshot_checksum_mismatch", "Snapshot project Git bundle checksum does not match manifest", 1, { snapshot });
331
867
  fs.rmSync(path.join(projectRoot, ".git"), { recursive: true, force: true });
332
868
  runGit(projectRoot, ["init", "-b", source.branch]);
333
- runGit(projectRoot, ["fetch", bundle, `+refs/heads/${source.branch}:refs/remotes/snapshot/${source.branch}`]);
334
- runGit(projectRoot, ["checkout", "-B", source.branch, source.head]);
869
+ if (source.state !== "unborn") {
870
+ runGit(projectRoot, ["fetch", bundle, `+refs/heads/${source.branch}:refs/remotes/snapshot/${source.branch}`]);
871
+ runGit(projectRoot, ["checkout", "-B", source.branch, source.head]);
872
+ }
873
+ restoreIndex(snapshot, "project", source, projectRoot);
874
+ }
875
+ const mergeStateFiles = ["MERGE_HEAD", "MERGE_MSG", "MERGE_MODE", "ORIG_HEAD", "AUTO_MERGE"];
876
+ function indexEntries(root, includeExclude = true) {
877
+ const entries = Buffer.from(gitBytes(root, ["ls-files", "--stage", "-z"])
878
+ .toString("utf8").split("\0").filter(entry => entry && localEnvironmentAllowed(entry.slice(entry.indexOf("\t") + 1)))
879
+ .map(entry => `${entry}\0`).join(""));
880
+ const merge = {};
881
+ for (const name of mergeStateFiles) {
882
+ const file = git(root, ["rev-parse", "--git-path", name]);
883
+ if (!file)
884
+ throw new AppError("snapshot_index_invalid", "Could not locate Git merge state", 1);
885
+ const resolved = path.resolve(root, file);
886
+ if (fs.existsSync(resolved))
887
+ merge[name] = fs.readFileSync(resolved).toString("base64");
888
+ }
889
+ if (!includeExclude)
890
+ return Buffer.from(JSON.stringify({ entries: entries.toString("base64"), merge }));
891
+ const excludePath = git(root, ["rev-parse", "--git-path", "info/exclude"]);
892
+ if (!excludePath)
893
+ throw new AppError("snapshot_index_invalid", "Could not locate Git local excludes", 1);
894
+ const excludeFile = path.resolve(root, excludePath);
895
+ let exclude = null;
896
+ if (fs.existsSync(excludeFile)) {
897
+ const stat = fs.lstatSync(excludeFile);
898
+ if (!stat.isFile() || stat.size > 1024 * 1024)
899
+ throw new AppError("snapshot_index_invalid", "Git local excludes must be a bounded regular file", 1);
900
+ exclude = fs.readFileSync(excludeFile).toString("base64");
901
+ }
902
+ // Local excludes affect review-visible inputs, but are not part of a Git
903
+ // bundle/index. Keep them in the existing hashed, non-executable Git payload.
904
+ return Buffer.from(JSON.stringify({ entries: entries.toString("base64"), merge, exclude }));
905
+ }
906
+ function snapshotIndex(root, output, name) {
907
+ const bytes = indexEntries(root);
908
+ const value = JSON.parse(bytes.toString());
909
+ const objects = new Set();
910
+ for (const entry of Buffer.from(value.entries, "base64").toString("utf8").split("\0").filter(Boolean)) {
911
+ const match = /^\d{6} ([a-f0-9]{40,64}) [0-3]\t/.exec(entry);
912
+ if (!match)
913
+ throw new AppError("snapshot_index_invalid", "Malformed Git index entry", 1);
914
+ // Gitlinks belong to another repository and are preserved as index IDs.
915
+ if (!entry.startsWith("160000 "))
916
+ objects.add(match[1]);
917
+ }
918
+ for (const name of ["MERGE_HEAD", "ORIG_HEAD", "AUTO_MERGE"]) {
919
+ if (value.merge[name])
920
+ for (const oid of Buffer.from(value.merge[name], "base64").toString().trim().split("\n")) {
921
+ if (!/^[a-f0-9]{40,64}$/.test(oid))
922
+ throw new AppError("snapshot_index_invalid", "Malformed Git merge reference", 1);
923
+ objects.add(oid);
924
+ }
925
+ }
926
+ const payload = path.join(output, `${name}.index.patch`);
927
+ const pack = path.join(output, `${name}.index.pack`);
928
+ fs.writeFileSync(payload, bytes);
929
+ fs.writeFileSync(pack, gitBytes(root, ["pack-objects", "--stdout", "--revs"], Buffer.from([...objects].sort().join("\n") + "\n")));
930
+ return { index_format: "entries@1", index_sha256: fileHash(payload), index_objects_sha256: fileHash(pack) };
931
+ }
932
+ function gitBytes(root, args, input) {
933
+ const result = spawnSync("git", ["-C", root, ...args], { input, maxBuffer: 64 * 1024 * 1024 });
934
+ if (result.status !== 0)
935
+ throw new AppError("snapshot_index_invalid", "Could not preserve Git index objects", 1, { args, stderr: result.stderr?.toString().trim() });
936
+ return result.stdout;
937
+ }
938
+ function indexPatch(root) {
939
+ const result = spawnSync("git", ["-C", root, "diff", "--cached", "--binary", "--full-index", "--no-ext-diff", "HEAD", "--", ".", ":(exclude)**/.env", ":(exclude)**/.env.*"], { maxBuffer: 64 * 1024 * 1024 });
940
+ if (result.status !== 0)
941
+ throw new AppError("snapshot_index_invalid", "Could not capture Git index", 1);
942
+ return result.stdout;
943
+ }
944
+ function restoreIndex(snapshot, name, metadata, root) {
945
+ if (!metadata.index_sha256)
946
+ return;
947
+ const file = path.join(snapshot, `${name}.index.patch`);
948
+ if (fileHash(file) !== metadata.index_sha256)
949
+ throw new AppError("snapshot_checksum_mismatch", "Snapshot Git index checksum does not match", 1);
950
+ if (metadata.index_format === "entries@1") {
951
+ const pack = path.join(snapshot, `${name}.index.pack`);
952
+ if (!metadata.index_objects_sha256 || fileHash(pack) !== metadata.index_objects_sha256)
953
+ throw new AppError("snapshot_checksum_mismatch", "Snapshot Git index object checksum does not match", 1);
954
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
955
+ if (value.exclude !== undefined && value.exclude !== null && (typeof value.exclude !== "string" || Buffer.byteLength(value.exclude) > 1400000))
956
+ throw new AppError("snapshot_index_invalid", "Snapshot Git local excludes are invalid", 1);
957
+ gitBytes(root, ["unpack-objects"], fs.readFileSync(pack));
958
+ runGit(root, ["read-tree", "--empty"]);
959
+ gitBytes(root, ["update-index", "-z", "--index-info"], Buffer.from(value.entries, "base64"));
960
+ for (const name of mergeStateFiles) {
961
+ const target = git(root, ["rev-parse", "--git-path", name]);
962
+ if (!target)
963
+ throw new AppError("snapshot_index_invalid", "Could not locate restored Git merge state", 1);
964
+ if (value.merge[name] !== undefined)
965
+ fs.writeFileSync(path.resolve(root, target), Buffer.from(value.merge[name], "base64"));
966
+ else
967
+ fs.rmSync(path.resolve(root, target), { force: true });
968
+ }
969
+ if (value.exclude !== undefined) {
970
+ const target = git(root, ["rev-parse", "--git-path", "info/exclude"]);
971
+ if (!target)
972
+ throw new AppError("snapshot_index_invalid", "Could not locate restored Git local excludes", 1);
973
+ const destination = path.resolve(root, target);
974
+ if (fs.existsSync(destination) && !fs.lstatSync(destination).isFile())
975
+ throw new AppError("snapshot_index_invalid", "Restored Git local excludes are not a regular file", 1);
976
+ if (value.exclude === null)
977
+ fs.rmSync(destination, { force: true });
978
+ else {
979
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
980
+ fs.writeFileSync(destination, Buffer.from(value.exclude, "base64"));
981
+ }
982
+ }
983
+ return;
984
+ }
985
+ if (fs.statSync(file).size)
986
+ runGit(root, ["apply", "--cached", "--binary", file]);
987
+ }
988
+ function capturedIndexEntries(snapshot, name, root) {
989
+ const captured = JSON.parse(fs.readFileSync(path.join(snapshot, `${name}.index.patch`), "utf8"));
990
+ return indexEntries(root, Object.hasOwn(captured, "exclude"));
991
+ }
992
+ export function verifyRecoverySnapshot(snapshot, input, checkWorkingState = true) {
993
+ const manifest = readManifest(snapshot);
994
+ if (manifest.purpose !== "recovery" || manifest.recovery_id !== input.recoveryId || manifest.run_id !== input.runId)
995
+ throw new AppError("recovery_capture_invalid", "Snapshot does not identify this recovery", 1);
996
+ const entries = [
997
+ { name: "project", root: input.projectRoot, hash: manifest.project_sha256, git: manifest.project_git, head: manifest.project_git.head },
998
+ ...(manifest.workspace.kind !== "project" ? [{ name: "workspace", root: input.workspaceRoot, hash: manifest.workspace.sha256, git: manifest.workspace, head: manifest.workspace.base_ref }] : [])
999
+ ];
1000
+ verifySnapshotPayload(snapshot, manifest);
1001
+ for (const entry of entries) {
1002
+ const allowed = manifest.project_payload_policy === "service-files@1" ? projectPayloadFilter(entry.root) : snapshotPayloadAllowed;
1003
+ const expectedModes = manifest.payload_modes?.[entry.name];
1004
+ if (checkWorkingState && expectedModes && treeModesHash(entry.root, allowed) !== expectedModes)
1005
+ throw new AppError("recovery_workspace_drift", "Executable file modes changed after recovery capture", 1, { payload: entry.name });
1006
+ if (checkWorkingState && (treeHash(entry.root, allowed) !== entry.hash || git(entry.root, ["rev-parse", "--verify", "HEAD"]) !== entry.head
1007
+ || entry.head === null && git(entry.root, ["for-each-ref", "--format=%(refname)"]) !== ""
1008
+ || 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)))
1009
+ throw new AppError("recovery_workspace_drift", "Working files or Git state changed after recovery capture", 1, { payload: entry.name });
1010
+ }
1011
+ }
1012
+ /** Reuse an already published boundary only after checking its identity and
1013
+ * every payload, including its Git index objects. Never overwrite evidence. */
1014
+ export function verifyControllerBoundarySnapshot(snapshot, input) {
1015
+ const manifest = readManifest(snapshot), boundary = manifest.boundary_capture;
1016
+ if (!boundary || manifest.consistency !== "controller_boundary_writer_barrier@1" || manifest.run_id !== input.runId || boundary.controller_id !== input.controllerId || boundary.operation_id !== input.operationId || boundary.boundary_key !== input.boundaryKey || manifest.stage_entry !== input.stageEntry || manifest.purpose !== (input.stageEntry ? "stage_entry" : "candidate") || !/^[a-f0-9]{64}$/.test(boundary.source_sha256) || boundary.observation?.settled !== true || boundary.observation.operation_id !== input.operationId)
1017
+ throw new AppError("controller_boundary_snapshot_invalid", "Published snapshot does not prove this controller boundary", 1, { snapshot });
1018
+ verifySnapshotPayload(snapshot, manifest);
1019
+ const file = path.join(snapshot, "snapshot.json");
1020
+ if (input.manifestSha256 !== undefined && fileHash(file) !== input.manifestSha256)
1021
+ throw new AppError("snapshot_checksum_mismatch", "Boundary manifest no longer matches its retained publication receipt", 1, { snapshot });
1022
+ return { snapshot, manifest: file, manifest_sha256: fileHash(file), purpose: manifest.purpose, stage_entry: manifest.stage_entry };
1023
+ }
1024
+ function verifySnapshotPayload(snapshot, manifest) {
1025
+ if (treeHash(path.join(snapshot, "runtime")) !== manifest.runtime_sha256)
1026
+ throw new AppError("snapshot_checksum_mismatch", "Snapshot runtime checksum does not match", 1);
1027
+ verifySnapshotModes(snapshot, manifest);
1028
+ 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 }] : [])];
1029
+ for (const payload of payloads)
1030
+ verifyGitSnapshotPayload(snapshot, payload);
1031
+ if (manifest.schema_id === "dd-flow/eval-run-snapshot@6") {
1032
+ const db = new DatabaseSync(path.join(snapshot, "runtime", "db.sqlite"), { readOnly: true });
1033
+ try {
1034
+ const row = db.prepare("SELECT flow_kind, project_root, workspace_root FROM runs WHERE id=?").get(manifest.run_id);
1035
+ if (!row || row.flow_kind !== "mb-init" || row.project_root !== manifest.project_root || row.workspace_root !== manifest.project_root)
1036
+ throw new AppError("snapshot_invalid", "Unborn recovery must belong to its captured Memory RUN", 1, { snapshot });
1037
+ }
1038
+ finally {
1039
+ db.close();
1040
+ }
1041
+ }
1042
+ }
1043
+ function verifyGitSnapshotPayload(snapshot, payload) {
1044
+ const bundle = path.join(snapshot, `${payload.name}.bundle`);
1045
+ const invalidBundle = payload.git.state === "unborn" ? payload.git.bundle_sha256 !== null || fs.existsSync(bundle)
1046
+ : !fs.existsSync(bundle) || fileHash(bundle) !== payload.git.bundle_sha256;
1047
+ if (treeHash(path.join(snapshot, payload.name)) !== payload.hash || invalidBundle || payload.git.index_sha256 && fileHash(path.join(snapshot, `${payload.name}.index.patch`)) !== payload.git.index_sha256 || payload.git.index_format === "entries@1" && (!payload.git.index_objects_sha256 || fileHash(path.join(snapshot, `${payload.name}.index.pack`)) !== payload.git.index_objects_sha256))
1048
+ throw new AppError("snapshot_checksum_mismatch", "Snapshot payload or Git index checksum does not match", 1, { payload: payload.name });
335
1049
  }
336
1050
  function git(root, args) {
337
1051
  const result = spawnSync("git", ["-C", root, ...args], { encoding: "utf8" });
@@ -343,12 +1057,408 @@ function runGit(root, args) {
343
1057
  throw new AppError("snapshot_workspace_restore_failed", "Could not restore feature worktree", 1, { stderr: result.stderr.trim(), args });
344
1058
  }
345
1059
  function samePath(left, right) { return path.resolve(left) === path.resolve(right); }
346
- function treeHash(root) {
1060
+ function snapshotPayloadAllowed(source) {
1061
+ const name = path.basename(source);
1062
+ return ![".git", "node_modules", ".cache", ".npm", ".pnpm-store"].includes(name)
1063
+ && localEnvironmentAllowed(source);
1064
+ }
1065
+ function projectPayloadFilter(root) {
1066
+ const serviceAllowed = projectServicePathFilter(root);
1067
+ return file => snapshotPayloadAllowed(file) && serviceAllowed(file);
1068
+ }
1069
+ function runtimePayloadAllowed(source) {
1070
+ return snapshotPayloadAllowed(source) && !["auth.json", "auth.bk", "auth.encrypted", "auth.v2.key", "auth.v2.file", "credentials.json", ".credentials.json", "credentials", ".credentials", "id_rsa", "id_ed25519", "engines", "checkouts"].includes(path.basename(source));
1071
+ }
1072
+ /** Only a Grok daemon's verified provider root is excluded from flow evidence.
1073
+ * An unrelated directory named grok-home remains ordinary project data. */
1074
+ function grokProviderHomes(runtimeRoot) {
1075
+ const homes = [];
1076
+ const visit = (directory) => {
1077
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
1078
+ const file = path.join(directory, entry.name);
1079
+ if (entry.isDirectory()) {
1080
+ if (![".git", "node_modules", "grok-home"].includes(entry.name))
1081
+ visit(file);
1082
+ }
1083
+ else if (entry.isFile() && entry.name === "daemon.json") {
1084
+ let state;
1085
+ try {
1086
+ state = JSON.parse(fs.readFileSync(file, "utf8"));
1087
+ }
1088
+ catch {
1089
+ continue;
1090
+ }
1091
+ if (state.schema_id !== "dd-grok/daemon-state@1")
1092
+ continue;
1093
+ const expected = path.join(directory, "grok-home");
1094
+ if (state.config?.grokHome !== expected || state.config?.ddFlowHome !== runtimeRoot)
1095
+ throw new AppError("snapshot_native_scope_invalid", "Grok provider home differs from its retained daemon binding", 1, { state_dir: directory });
1096
+ homes.push(expected);
1097
+ }
1098
+ }
1099
+ };
1100
+ visit(runtimeRoot);
1101
+ return homes;
1102
+ }
1103
+ function nativeDaemonFile(file) {
1104
+ if (!fs.lstatSync(file, { throwIfNoEntry: false })?.isFile())
1105
+ return false;
1106
+ try {
1107
+ const state = JSON.parse(fs.readFileSync(file, "utf8"));
1108
+ return typeof state.daemon_id === "string" && (/^dd-(codex|zcode|grok|agy|opencode|droid)\/daemon-state@1$/.test(String(state.schema_id)) || state.schema_id === "dd-opencode/daemon-state@2");
1109
+ }
1110
+ catch {
1111
+ return false;
1112
+ } // Unrecognized files retain full-byte protection.
1113
+ }
1114
+ function sqliteFile(file) {
1115
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile())
1116
+ return false;
1117
+ const descriptor = fs.openSync(file, "r");
1118
+ try {
1119
+ const header = Buffer.alloc(16);
1120
+ return fs.readSync(descriptor, header, 0, 16, 0) === 16 && header.toString() === "SQLite format 3\0";
1121
+ }
1122
+ finally {
1123
+ fs.closeSync(descriptor);
1124
+ }
1125
+ }
1126
+ function sqliteIdentifier(value) { return `"${value.replaceAll('"', '""')}"`; }
1127
+ function sqliteValue(value) {
1128
+ if (value === null || typeof value === "string" || typeof value === "boolean")
1129
+ return value;
1130
+ if (typeof value === "number")
1131
+ return Number.isFinite(value) ? value : String(value);
1132
+ if (typeof value === "bigint")
1133
+ return `${value}n`;
1134
+ if (Buffer.isBuffer(value))
1135
+ return `blob:${value.toString("base64")}`;
1136
+ if (value instanceof Uint8Array)
1137
+ return `blob:${Buffer.from(value).toString("base64")}`;
1138
+ return String(value);
1139
+ }
1140
+ /**
1141
+ * SQLite page layout is not semantic identity: VACUUM, checkpoint and a
1142
+ * delete/insert rewrite can change serialized bytes without changing the
1143
+ * logical database. Snapshot only the schema and ordered logical row values.
1144
+ * This intentionally applies to the small set of managed SQLite files, not
1145
+ * to arbitrary provider databases.
1146
+ */
1147
+ function sqliteSemanticFingerprint(db) {
1148
+ db.exec("PRAGMA busy_timeout = 4000");
1149
+ db.exec("BEGIN");
1150
+ try {
1151
+ const schema = db.prepare("SELECT type, name, tbl_name, sql FROM sqlite_schema WHERE name NOT LIKE 'sqlite_%' ORDER BY type, name, tbl_name, sql").all();
1152
+ const tables = schema.filter(row => row.type === "table").map(row => {
1153
+ const columns = db.prepare(`PRAGMA table_info(${sqliteIdentifier(row.name)})`).all()
1154
+ .sort((left, right) => Number(left.cid ?? 0) - Number(right.cid ?? 0))
1155
+ .map(column => ({ name: String(column.name ?? ""), type: String(column.type ?? ""), notnull: Number(column.notnull ?? 0), default: sqliteValue(column.dflt_value ?? null), pk: Number(column.pk ?? 0) }));
1156
+ const names = columns.map(column => column.name);
1157
+ const rows = db.prepare(`SELECT * FROM ${sqliteIdentifier(row.name)}`).all()
1158
+ .map(record => names.map(name => [name, sqliteValue(record[name])]))
1159
+ .sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
1160
+ return { name: row.name, columns, rows };
1161
+ });
1162
+ return crypto.createHash("sha256").update(JSON.stringify({ schema, tables })).digest("hex");
1163
+ }
1164
+ finally {
1165
+ try {
1166
+ db.exec("ROLLBACK");
1167
+ }
1168
+ catch { /* The read transaction may already have closed on provider error. */ }
1169
+ }
1170
+ }
1171
+ /** A WAL is copied through its SQLite read snapshot, not as raw bytes. */
1172
+ function runtimeSnapshotKind(file, excluded, providerHomes = []) {
1173
+ if (!runtimePayloadAllowed(file) || providerHomes.some(home => file === home || isInside(home, file)) || excluded.includes(file))
1174
+ return "excluded";
1175
+ if (fs.lstatSync(file).isFile()) {
1176
+ if (/-(?:wal|shm|journal)$/.test(file) && sqliteFile(file.replace(/-(?:wal|shm|journal)$/, "")))
1177
+ return "sidecar";
1178
+ if (sqliteFile(file))
1179
+ return "sqlite";
1180
+ }
1181
+ return "ordinary";
1182
+ }
1183
+ /** Productive writers are fenced separately. Compare the copied source set
1184
+ * without idle diagnostic appends; SQLite includes committed WAL pages without
1185
+ * mistaking a physical checkpoint for new logical content. */
1186
+ function snapshotSourceVersion(context, input, retainedDatabases, retainedJournals) {
1187
+ const databases = retainedDatabases ?? new Map();
1188
+ const journals = retainedJournals ?? new Map();
1189
+ const projectRoot = resolveProjectRoot(input.projectRoot);
1190
+ const project = requireProjectByRoot(context, projectRoot);
1191
+ const run = context.db.get("SELECT workspace_root FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
1192
+ if (!run)
1193
+ throw new AppError("not_found", "Snapshot RUN is not registered", 1);
1194
+ const entries = new Map();
1195
+ const diagnosticLogs = new Set(getResourceDatabase(resourceHome(context), "read_existing").all("SELECT stdout_path,stderr_path,metadata_json FROM managed_processes WHERE project_id=? AND run_id=? AND kind IN ('codex-daemon','zcode-daemon','grok-daemon','agy-daemon','opencode-daemon','droid-daemon')", [project.id, input.runId]).flatMap(row => {
1196
+ const directory = JSON.parse(row.metadata_json).state_dir;
1197
+ const log = typeof directory === "string" && path.isAbsolute(directory) ? path.join(directory, "daemon.log") : null;
1198
+ return log && isInside(context.ddFlowHome, log) && row.stdout_path === log && row.stderr_path === log ? [log] : [];
1199
+ }));
1200
+ const excluded = codexHomeSnapshotExclusions(context, project.id, input.runId);
1201
+ const providerHomes = input.recoveryId === undefined ? grokProviderHomes(context.ddFlowHome) : [];
1202
+ const observations = new Map();
1203
+ const operationRoots = new Set();
1204
+ const visit = (directory) => {
1205
+ const daemon = path.join(directory, "daemon.json");
1206
+ const native = nativeDaemonFile(daemon);
1207
+ if (native) {
1208
+ operationRoots.add(path.join(directory, "operations"));
1209
+ for (const operation of retainedDaemonOperations(directory))
1210
+ if (isObservationOperation(operation.operation))
1211
+ observations.set(path.join(directory, "operations", crypto.createHash("sha256").update(String(operation.operation_id)).digest("hex")), operation);
1212
+ }
1213
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
1214
+ const file = path.join(directory, entry.name);
1215
+ // These diagnostic cuts are retained, but their creation cannot
1216
+ // invalidate a productive boundary. Unknown operations remain protected.
1217
+ if (observations.has(file))
1218
+ continue;
1219
+ const kind = runtimeSnapshotKind(file, excluded, providerHomes);
1220
+ if (kind === "excluded" || kind === "sidecar")
1221
+ continue;
1222
+ const key = `runtime/${path.relative(context.ddFlowHome, file)}`;
1223
+ const stat = fs.lstatSync(file);
1224
+ if (stat.isDirectory()) {
1225
+ if (!operationRoots.has(file))
1226
+ entries.set(key, "directory");
1227
+ visit(file);
1228
+ }
1229
+ else if (stat.isSymbolicLink())
1230
+ entries.set(key, `link:${fs.readlinkSync(file)}`);
1231
+ else if (native && file === daemon)
1232
+ entries.set(key, `daemon:${nativeInventoryVersion(directory)}:${stat.mode & 0o111}`);
1233
+ else if (kind === "sqlite") {
1234
+ // Their productive identities are checked by boundary.verify(). A
1235
+ // consistent backup is evidence, not a demand for global DB silence.
1236
+ if (file === context.db.path || file === path.join(resourceHome(context), "runtime.sqlite")) {
1237
+ entries.set(key, `managed-sqlite:${stat.mode & 0o111}`);
1238
+ continue;
1239
+ }
1240
+ let db = databases.get(file);
1241
+ if (!db) {
1242
+ db = new DatabaseSync(file, { readOnly: true });
1243
+ databases.set(file, db);
1244
+ }
1245
+ entries.set(key, `sqlite:${sqliteSemanticFingerprint(db)}:${stat.mode & 0o111}`);
1246
+ }
1247
+ else if (stat.isFile() && diagnosticLogs.has(file)) {
1248
+ // Daemon stdout/stderr can append while an otherwise settled Session is idle.
1249
+ // Keep its identity/mode fenced; the copied bytes remain sealed in the snapshot.
1250
+ entries.set(key, `diagnostic-log:${stat.dev}:${stat.ino}:${stat.mode & 0o111}`);
1251
+ }
1252
+ else if (stat.isFile() && (["adapter.events.jsonl", "events.jsonl", "model-observations.jsonl", "timeline.jsonl", "controller.log"].includes(path.basename(file)) || native && file === path.join(directory, "daemon.log"))) {
1253
+ const bytes = fs.readFileSync(file);
1254
+ let cut = journals.get(file);
1255
+ if (!cut && !retainedJournals) {
1256
+ const payload = bytes.subarray(0, bytes.lastIndexOf(10) + 1);
1257
+ cut = { payload, dev: stat.dev, ino: stat.ino, mode: stat.mode, sha256: crypto.createHash("sha256").update(payload).digest("hex") };
1258
+ journals.set(file, cut);
1259
+ }
1260
+ const valid = cut && cut.dev === stat.dev && cut.ino === stat.ino && bytes.length >= cut.payload.length && bytes.subarray(0, cut.payload.length).equals(cut.payload);
1261
+ entries.set(key, valid && cut ? `journal:${cut.sha256}:${cut.payload.length}:${stat.mode & 0o111}` : "journal:changed");
1262
+ }
1263
+ else if (stat.isFile())
1264
+ entries.set(key, `file:${crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex")}:${stat.mode & 0o111}`);
1265
+ }
1266
+ };
1267
+ try {
1268
+ visit(context.ddFlowHome);
1269
+ for (const root of new Set([projectRoot, run.workspace_root]))
1270
+ entries.set(`source/${root}`, JSON.stringify([
1271
+ treeHash(root, true), treeModesHash(root, true), git(root, ["symbolic-ref", "--quiet", "HEAD"]), git(root, ["rev-parse", "HEAD"]), indexEntries(root).toString("base64")
1272
+ ]));
1273
+ return { entries, databases, journals, observations, sha256: crypto.createHash("sha256").update(JSON.stringify([...entries].sort(([a], [b]) => a.localeCompare(b)))).digest("hex") };
1274
+ }
1275
+ catch (error) {
1276
+ if (!retainedDatabases)
1277
+ for (const db of databases.values())
1278
+ try {
1279
+ db.close();
1280
+ }
1281
+ catch { /* Preserve the original source error. */ }
1282
+ throw error;
1283
+ }
1284
+ }
1285
+ function snapshotSourceChanges(before, after) {
1286
+ return [...new Set([...before.keys(), ...after.keys()])].filter(key => before.get(key) !== after.get(key)).sort().slice(0, 8);
1287
+ }
1288
+ function snapshotSourceChangeProofs(before, after) {
1289
+ return snapshotSourceChanges(before, after).map(component => ({
1290
+ component,
1291
+ before: before.has(component) ? crypto.createHash("sha256").update(before.get(component)).digest("hex") : null,
1292
+ after: after.has(component) ? crypto.createHash("sha256").update(after.get(component)).digest("hex") : null
1293
+ }));
1294
+ }
1295
+ /** Managed Codex homes share host configuration, not owned snapshot payload. */
1296
+ function codexHomeSnapshotExclusions(context, projectId, runId) {
1297
+ const homes = new Set(context.db.all("SELECT target_home FROM codex_home_profiles WHERE project_id = ?", [projectId]).map(row => row.target_home));
1298
+ if (context.db.get("SELECT 1 FROM sqlite_schema WHERE name = 'run_controllers'")) {
1299
+ for (const { home } of context.db.all("SELECT json_extract(value, '$.codex_home') AS home FROM run_controllers, json_each(state_json, '$.sessions') WHERE project_id = ? AND run_id = ?", [projectId, runId]))
1300
+ if (home)
1301
+ homes.add(home);
1302
+ }
1303
+ const excluded = [];
1304
+ for (const home of homes) {
1305
+ if (!path.isAbsolute(home) || !fs.existsSync(home) || fs.lstatSync(home).isSymbolicLink())
1306
+ continue;
1307
+ const marker = path.join(home, ".dd-flow-home.json");
1308
+ if (!fs.existsSync(marker) || fs.lstatSync(marker).isSymbolicLink())
1309
+ continue;
1310
+ const identity = JSON.parse(fs.readFileSync(marker, "utf8"));
1311
+ if (identity.managed_by !== "dd-flow-cli" || identity.project_id !== projectId || identity.target_home !== home || !identity.source_home || !path.isAbsolute(identity.source_home))
1312
+ continue;
1313
+ for (const entry of codexHomeSharedEntries) {
1314
+ const file = path.join(home, entry);
1315
+ // Skip only the exact links installed by initCodexHome, never arbitrary
1316
+ // external links or owned local files with the same convenient name.
1317
+ if (fs.lstatSync(file, { throwIfNoEntry: false })?.isSymbolicLink() && fs.readlinkSync(file) === path.join(identity.source_home, entry))
1318
+ excluded.push(file);
1319
+ }
1320
+ excluded.push(path.join(home, "tmp"), path.join(home, ".tmp"));
1321
+ }
1322
+ return excluded;
1323
+ }
1324
+ /** Copy a dedicated home or scoped recovery tree, preserving SQLite WAL commits. */
1325
+ function copyRuntimeTree(source, destination, scoped = true, excluded = [], providerHomes = [], observationCuts) {
1326
+ if (fs.lstatSync(source).isSymbolicLink())
1327
+ throw new AppError("snapshot_runtime_symlink_outside_scope", "Recovery artifact roots must not be symbolic links", 1, { path: source });
1328
+ const root = physicalPath(source);
1329
+ if (isInside(root, physicalPath(destination)))
1330
+ throw new AppError("snapshot_output_inside_runtime", "Recovery output must be outside its source artifact tree", 1);
1331
+ const databases = [];
1332
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
1333
+ fs.cpSync(source, destination, {
1334
+ recursive: true, verbatimSymlinks: true,
1335
+ filter: file => {
1336
+ if (observationCuts && path.basename(path.dirname(file)) === "operations" && fs.lstatSync(file).isDirectory() && nativeDaemonFile(path.join(path.dirname(path.dirname(file)), "daemon.json"))) {
1337
+ let operations;
1338
+ try {
1339
+ operations = retainedDaemonOperations(path.dirname(path.dirname(file)));
1340
+ }
1341
+ catch (error) {
1342
+ if (error.code !== "ENOENT")
1343
+ throw error;
1344
+ throw new AppError("snapshot_source_changed", "Native operation was not fully published during capture", 1, { components: ["native_operation_publication"] });
1345
+ }
1346
+ const operation = operations.find(row => crypto.createHash("sha256").update(String(row.operation_id)).digest("hex") === path.basename(file));
1347
+ if (operation && isObservationOperation(operation.operation)) {
1348
+ if (!observationCuts.has(file))
1349
+ observationCuts.set(file, operation);
1350
+ return false;
1351
+ }
1352
+ }
1353
+ const kind = runtimeSnapshotKind(file, excluded, providerHomes);
1354
+ if (kind === "excluded" || kind === "sidecar")
1355
+ return false;
1356
+ const stat = fs.lstatSync(file);
1357
+ if (stat.isSymbolicLink()) {
1358
+ if (scoped && !isInside(root, physicalPath(file)))
1359
+ throw new AppError("snapshot_runtime_symlink_outside_scope", "Recovery artifact link leaves its owned tree", 1, { path: file });
1360
+ return true;
1361
+ }
1362
+ if (!stat.isDirectory() && !stat.isFile())
1363
+ return false;
1364
+ if (stat.isFile()) {
1365
+ if (kind === "sqlite") {
1366
+ databases.push(file);
1367
+ return false;
1368
+ }
1369
+ }
1370
+ return true;
1371
+ }
1372
+ });
1373
+ for (const file of databases) {
1374
+ const db = new DatabaseSync(file, { readOnly: true });
1375
+ try {
1376
+ const target = source === file ? destination : path.join(destination, path.relative(source, file));
1377
+ fs.mkdirSync(path.dirname(target), { recursive: true });
1378
+ db.exec("PRAGMA busy_timeout = 4000");
1379
+ db.exec(`VACUUM INTO '${target.replace(/'/g, "''")}'`);
1380
+ }
1381
+ finally {
1382
+ db.close();
1383
+ }
1384
+ }
1385
+ }
1386
+ function captureRecoveryAuxiliary(context, captured, input) {
1387
+ const runtime = path.join(input.output, "runtime");
1388
+ const codexExclusions = codexHomeSnapshotExclusions(context, input.projectId, input.runId);
1389
+ const run = captured.db.get("SELECT run_root, project_root, workspace_root FROM runs WHERE project_id = ? AND id = ?", [input.projectId, input.runId]);
1390
+ const roots = [run.run_root, runtimeRunDir(context.ddFlowHome, input.projectId, input.runId)];
1391
+ for (const protocol of captured.db.all("SELECT id, state_path, plan_path FROM protocols")) {
1392
+ const directory = runtimeProtocolDir(context.ddFlowHome, input.projectId, protocol.id);
1393
+ if (!isInside(physicalPath(context.ddFlowHome), physicalPath(directory)))
1394
+ throw new AppError("snapshot_protocol_scope_invalid", "Protocol runtime leaves DD_FLOW_HOME", 1);
1395
+ if (fs.existsSync(directory)) {
1396
+ copyRuntimeTree(directory, path.join(runtime, path.relative(context.ddFlowHome, directory)), true, codexExclusions);
1397
+ roots.push(directory);
1398
+ }
1399
+ for (const file of [protocol.state_path, protocol.plan_path]) {
1400
+ if (!fs.existsSync(file) || roots.some(root => isInside(physicalPath(root), physicalPath(file))))
1401
+ continue;
1402
+ if (isInside(physicalPath(context.ddFlowHome), physicalPath(file)) && fs.statSync(file).isFile())
1403
+ copyRuntimeTree(file, path.join(runtime, path.relative(context.ddFlowHome, file)));
1404
+ else if (![run.project_root, run.workspace_root].some(root => isInside(physicalPath(root), physicalPath(file))))
1405
+ throw new AppError("snapshot_protocol_scope_invalid", "Protocol evidence is outside captured payloads", 1, { path: file });
1406
+ }
1407
+ }
1408
+ const homes = new Set([context.ddFlowHome, resourceHome(context)]);
1409
+ const queries = {
1410
+ run_controllers: "SELECT json_extract(manifest_json, '$.resource_home') AS home FROM run_controllers UNION SELECT json_extract(value, '$.owner.resource_home') FROM run_controllers, json_each(state_json, '$.sessions')",
1411
+ run_control_workers: "SELECT resource_home AS home FROM run_control_workers",
1412
+ work_launches: "SELECT json_extract(profile_json, '$.runtime_adapter.owner.resource_home') AS home FROM work_launches",
1413
+ merge_requests: "SELECT json_extract(adapter_receipt_json, '$.owner.resource_home') AS home FROM merge_requests",
1414
+ run_recovery_guards: "SELECT value AS home FROM run_recovery_guards, json_each(settlement_json, '$.resource_homes')"
1415
+ };
1416
+ const references = Object.entries(queries).flatMap(([table, query]) => captured.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = ?", [table]) ? captured.db.all(query) : []);
1417
+ for (const { home } of references) {
1418
+ if (home === null)
1419
+ continue;
1420
+ if (typeof home !== "string" || !path.isAbsolute(home))
1421
+ throw new AppError("snapshot_resource_scope_invalid", "Retained resource home must be an absolute path", 1);
1422
+ homes.add(home);
1423
+ }
1424
+ const nativeCaptures = new Map();
1425
+ const registries = [];
1426
+ for (const home of [...homes].sort()) {
1427
+ if (!path.isAbsolute(home))
1428
+ throw new AppError("snapshot_resource_scope_invalid", "Resource home must be an absolute path", 1);
1429
+ const evidence = readRecoveryResourceEvidence(home, input.projectId, input.runId, { db: captured.db, ddFlowHome: context.ddFlowHome });
1430
+ for (const process of evidence.processes) {
1431
+ const directory = JSON.parse(process.metadata_json).state_dir;
1432
+ if (directory === undefined)
1433
+ continue;
1434
+ if (typeof directory !== "string" || !path.isAbsolute(directory))
1435
+ throw new AppError("snapshot_native_scope_invalid", "Native artifact root must be absolute", 1);
1436
+ if (roots.some(root => isInside(physicalPath(root), physicalPath(directory))) || nativeCaptures.has(directory))
1437
+ continue;
1438
+ const state = JSON.parse(fs.readFileSync(path.join(directory, "daemon.json"), "utf8"));
1439
+ const owner = state.config?.runtime_owner;
1440
+ if (!owner || owner.project_id !== input.projectId || owner.run_id !== input.runId || owner.owner_id !== process.owner_id || owner.dd_flow_home !== context.ddFlowHome || owner.resource_home !== home || owner.state_dir !== directory)
1441
+ throw new AppError("snapshot_native_scope_invalid", "External native artifact root has no matching RUN owner", 1, { state_dir: directory });
1442
+ const relative = path.join("native-evidence", crypto.createHash("sha256").update(directory).digest("hex"));
1443
+ copyRuntimeTree(directory, path.join(runtime, relative), true, codexExclusions);
1444
+ nativeCaptures.set(directory, relative);
1445
+ }
1446
+ const relative = path.join("resource-evidence", `${crypto.createHash("sha256").update(home).digest("hex")}.json`);
1447
+ fs.mkdirSync(path.dirname(path.join(runtime, relative)), { recursive: true });
1448
+ writeJson(path.join(runtime, relative), evidence);
1449
+ registries.push(relative);
1450
+ }
1451
+ writeJson(path.join(runtime, "recovery-evidence.json"), { schema_id: "dd-flow/recovery-runtime-evidence@1", project_id: input.projectId, run_id: input.runId, registries, native_captures: [...nativeCaptures].map(([source_root, payload]) => ({ source_root, payload })) });
1452
+ }
1453
+ function treeHash(root, filtered = false) {
347
1454
  const hash = crypto.createHash("sha256");
1455
+ const allowed = typeof filtered === "function" ? filtered : filtered ? projectPayloadFilter(root) : () => true;
348
1456
  const visit = (dir, relative) => {
349
1457
  for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
350
1458
  const file = path.join(dir, entry.name);
351
1459
  const rel = path.join(relative, entry.name);
1460
+ if (!allowed(file))
1461
+ continue;
352
1462
  if (entry.isDirectory())
353
1463
  visit(file, rel);
354
1464
  else if (entry.isFile()) {
@@ -364,18 +1474,80 @@ function treeHash(root) {
364
1474
  visit(root, "");
365
1475
  return hash.digest("hex");
366
1476
  }
1477
+ function treeModesHash(root, filtered = false) {
1478
+ const entries = [];
1479
+ const allowed = typeof filtered === "function" ? filtered : filtered ? projectPayloadFilter(root) : () => true;
1480
+ const visit = (directory) => {
1481
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
1482
+ const file = path.join(directory, entry.name);
1483
+ if (!allowed(file))
1484
+ continue;
1485
+ if (entry.isDirectory())
1486
+ visit(file);
1487
+ else if (entry.isFile())
1488
+ entries.push([path.relative(root, file), fs.statSync(file).mode & 0o111]);
1489
+ }
1490
+ };
1491
+ visit(root);
1492
+ return crypto.createHash("sha256").update(JSON.stringify(entries)).digest("hex");
1493
+ }
1494
+ function verifySnapshotModes(snapshot, manifest) {
1495
+ if (!manifest.payload_modes)
1496
+ return; // Legacy snapshots did not attest modes.
1497
+ for (const name of ["project", "runtime", ...(manifest.workspace.kind !== "project" ? ["workspace"] : [])]) {
1498
+ if (treeModesHash(path.join(snapshot, name)) !== manifest.payload_modes[name])
1499
+ throw new AppError("snapshot_checksum_mismatch", "Snapshot executable modes do not match manifest", 1, { payload: name });
1500
+ }
1501
+ }
367
1502
  function fileHash(file) { return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); }
1503
+ // Flush children before their directory entries, then publish and flush the
1504
+ // parent. Never follow snapshot symlinks into the original project tree.
1505
+ function syncSnapshotTree(root) {
1506
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
1507
+ const file = path.join(root, entry.name);
1508
+ if (entry.isDirectory())
1509
+ syncSnapshotTree(file);
1510
+ else if (entry.isFile())
1511
+ syncPath(file);
1512
+ }
1513
+ syncPath(root);
1514
+ }
1515
+ function syncPath(file) {
1516
+ const descriptor = fs.openSync(file, "r");
1517
+ try {
1518
+ fs.fsyncSync(descriptor);
1519
+ }
1520
+ finally {
1521
+ fs.closeSync(descriptor);
1522
+ }
1523
+ }
368
1524
  function readManifest(snapshot) {
369
1525
  const file = path.join(snapshot, "snapshot.json");
370
1526
  if (!fs.existsSync(file))
371
1527
  throw new AppError("snapshot_missing", "Snapshot manifest is missing", 1, { snapshot });
372
1528
  const value = JSON.parse(fs.readFileSync(file, "utf8"));
373
- if (value.schema_id !== "dd-flow/eval-run-snapshot@5" || !value.run_id || !value.project_root || !value.dd_flow_home || typeof value.purpose !== "string" || !["stage_entry", "candidate"].includes(value.purpose) || (value.purpose === "stage_entry" && !value.stage_entry) || (value.purpose === "candidate" && value.stage_entry !== null) || !value.runtime_sha256 || !value.project_sha256 || !value.project_git || !value.workspace)
1529
+ 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)
374
1530
  throw new AppError("snapshot_invalid", "Snapshot manifest is invalid", 1, { snapshot });
375
- if (!value.project_git.branch || !value.project_git.head || !value.project_git.bundle_sha256)
1531
+ if (value.runtime_payload_policy !== undefined && (value.runtime_payload_policy !== "flow-evidence@1" || value.purpose === "recovery"))
1532
+ throw new AppError("snapshot_invalid", "Snapshot runtime payload policy is invalid for its purpose", 1, { snapshot });
1533
+ const unborn = value.schema_id === "dd-flow/eval-run-snapshot@6";
1534
+ if (!value.project_git.branch || (unborn
1535
+ ? value.purpose !== "recovery" || value.workspace.kind !== "project" || value.project_git.state !== "unborn"
1536
+ || value.project_git.head !== null || value.project_git.bundle_sha256 !== null
1537
+ || value.project_git.index_format !== "entries@1" || !/^[a-f0-9]{64}$/.test(value.project_git.index_sha256 ?? "")
1538
+ || !/^[a-f0-9]{64}$/.test(value.project_git.index_objects_sha256 ?? "")
1539
+ || Object.keys(value.project_git).sort().join() !== "branch,bundle_sha256,head,index_format,index_objects_sha256,index_sha256,state"
1540
+ || typeof value.project_git.branch !== "string"
1541
+ || spawnSync("git", ["check-ref-format", "--branch", value.project_git.branch], { encoding: "utf8" }).status !== 0
1542
+ : value.project_git.state !== undefined || !value.project_git.head || !value.project_git.bundle_sha256))
376
1543
  throw new AppError("snapshot_invalid", "Snapshot project Git metadata is incomplete", 1, { snapshot });
377
- if (value.workspace.kind === "git_worktree" && (!value.workspace.branch || !value.workspace.base_ref || !value.workspace.bundle_sha256))
378
- throw new AppError("snapshot_invalid", "Snapshot worktree manifest is incomplete", 1, { snapshot });
1544
+ if (!['project', 'git_worktree', 'git_repository'].includes(value.workspace.kind)
1545
+ || (value.schema_id === 'dd-flow/eval-run-snapshot@7') !== (value.workspace.kind === 'git_repository')
1546
+ || value.workspace.kind !== 'project' && (!value.workspace.branch || (value.workspace.state === 'unborn'
1547
+ ? value.workspace.kind !== 'git_repository' || value.workspace.base_ref !== null || value.workspace.bundle_sha256 !== null
1548
+ || value.workspace.index_format !== 'entries@1' || !value.workspace.index_sha256 || !value.workspace.index_objects_sha256
1549
+ : value.workspace.state !== undefined || !value.workspace.base_ref || !value.workspace.bundle_sha256)))
1550
+ throw new AppError("snapshot_invalid", "Snapshot workspace manifest is incomplete or incompatible", 1, { snapshot });
379
1551
  return value;
380
1552
  }
381
1553
  function readBootstrapManifest(snapshot) {
@@ -383,7 +1555,11 @@ function readBootstrapManifest(snapshot) {
383
1555
  if (!fs.existsSync(file))
384
1556
  throw new AppError("snapshot_missing", "Bootstrap snapshot manifest is missing", 1, { snapshot });
385
1557
  const value = JSON.parse(fs.readFileSync(file, "utf8"));
386
- if (value.schema_id !== "dd-eval/bootstrap-snapshot@1" || value.stage !== "specify" || !value.project_root || !value.project_sha256 || !value.project_git?.branch || !value.project_git.head || !value.project_git.bundle_sha256)
1558
+ const validBoundary = value.schema_id === "dd-eval/bootstrap-snapshot@1" ? value.stage === "specify" && value.flow === undefined
1559
+ : value.schema_id === "dd-eval/bootstrap-snapshot@2" && (value.flow?.id === "mb-init" || value.flow?.id === "mb-upgrade" || value.flow?.id === "interactive" || isSourceSnapshotFlow(value.flow?.id) || isDeliveryFlow(value.flow?.id)) && value.stage === "preflight"
1560
+ && Object.keys(value.flow).sort().join() === "descriptor_sha256,id,source_sha256"
1561
+ && /^[a-f0-9]{64}$/.test(value.flow.source_sha256) && /^[a-f0-9]{64}$/.test(value.flow.descriptor_sha256);
1562
+ if (!validBoundary || !value.project_root || !value.project_sha256 || !value.project_git?.branch || !value.project_git.head || !value.project_git.bundle_sha256)
387
1563
  throw new AppError("snapshot_invalid", "Bootstrap snapshot manifest is invalid", 1, { snapshot });
388
1564
  return value;
389
1565
  }