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

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 +965 -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 +257 -110
  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,10 +2,16 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
- import { formatFullId, isFullEntityId, isShortEntityId, parseFullEntityId } from "../domain/entity-ids.js";
5
+ import { withProjectionLock } from "../storage/projection-lock.js";
6
+ import { readTechnicalNotes } from "./technical-notes.js";
7
+ import { appendTimelineAudit, writeTimelineProjection } from "./run-timeline.js";
8
+ import { assertAssignedSettlementCurrent } from "./assigned-lifecycle.js";
9
+ import { reconcileRunStageReports } from "./stage-report-renderer.js";
10
+ import { isFullEntityId, isShortEntityId, parseFullEntityId } from "../domain/entity-ids.js";
6
11
  import { checksumForFlowFlagValues, isFlowFlagDowngrade, loadCanonicalFlowContract, loadProjectFlowContract, resolveFlowFlags } from "../domain/flow-contract.js";
7
12
  import { AppError } from "../shared/errors.js";
8
- import { ensureDir, projectRunHome, projectRunJsonPath, resolveProjectRoot } from "../storage/paths.js";
13
+ import { nextRunId } from "./ids.js";
14
+ import { ensureDir, canonicalPath, projectRunHome, projectRunJsonPath, resolveProjectRoot } from "../storage/paths.js";
9
15
  import { appendAudit } from "./audit.js";
10
16
  import { registerProject, requireProjectByRoot } from "./projects.js";
11
17
  import { buildRunFlowGuidance } from "./flow-guidance.js";
@@ -14,12 +20,19 @@ import { recalculateRunUsage } from "./usage.js";
14
20
  import { refreshRunSessionProjection } from "./run-projection.js";
15
21
  import { resolveCanonRoot } from "./canon.js";
16
22
  import { bindCurrentEngineToRun } from "./engines.js";
17
- import { executionProfilePath, loadVnextExecutionProfile } from "./vnext-execution-profile.js";
23
+ import { assertOperationalDecisionSource, executionProfilePath, loadVnextExecutionProfileSnapshot } from "./vnext-execution-profile.js";
24
+ import { loadVnextWorkspacePolicy } from "./vnext-workspace-policy.js";
25
+ import { assertSameSessionMerge, loadExecutionRoutingInput, resolveStageTransition, snapshotAgentProfiles } from "./execution-policy.js";
18
26
  import { isLegalVnextTransition, vnextStageDirectory, vnextStages } from "../domain/stage-catalog.js";
19
27
  import { publicSessionIdentity } from "./session-identity.js";
28
+ import { flowCommand } from "./stage-pause.js";
29
+ import { RetainedExecutionBundle } from "./execution-bundles.js";
30
+ import { admitBundledFlow, bundledHandlerContext } from "./stage-handlers.js";
31
+ import { hybridDigest } from "./hybrid-contracts.js";
32
+ import { projectPacketLocators, runContextRelocations } from "./stage-context.js";
33
+ import { readRunEngineBinding } from "./run-engine-bindings.js";
20
34
  const runSchemaId = "dd-flow/flow-run@3";
21
35
  const runtimeSchemaId = "dd-flow/flow-run@3";
22
- const runIdType = "RUN";
23
36
  const allowedRunFlowKinds = [
24
37
  "mb_sdlc",
25
38
  "coding",
@@ -27,7 +40,11 @@ const allowedRunFlowKinds = [
27
40
  "mb-init",
28
41
  "mb-upgrade",
29
42
  "mb-audit",
43
+ "project-review",
30
44
  "mb-distill",
45
+ "mb-fix",
46
+ "review-fix",
47
+ "interactive",
31
48
  "mb-upgrade-review",
32
49
  "mb-sdlc-review",
33
50
  "vnext_specify",
@@ -35,22 +52,28 @@ const allowedRunFlowKinds = [
35
52
  "review",
36
53
  "custom"
37
54
  ];
38
- export function startFlowRun(context, input) {
55
+ export function prepareFlowRunStart(context, input) {
56
+ const subjectId = requiredPlain(input.subjectId, "subject-id");
57
+ const subjectType = requiredPlain(input.subjectType, "subject-type");
58
+ const slug = normalizeSlug(input.slug);
59
+ for (const [name, value] of [["task profile", input.taskProfile], ["protocol overrides", input.protocolOverrides], ["RUN overrides", input.runOverrides]]) {
60
+ if (value !== undefined && (!value || typeof value !== "object" || Array.isArray(value)))
61
+ throw new AppError("validation", `${name} must be a JSON object`, 2);
62
+ }
39
63
  const projectRoot = resolveProjectRoot(input.projectRoot);
64
+ const flowKind = parseRunFlowKind(input.flowKind);
65
+ if (flowKind === "interactive")
66
+ throw new AppError("descriptor_run_start_required", "Interactive requires its retained request and registered descriptor RUN start", 2, { effect: "no_effect", next_action: "use_supported_interactive_flow_launcher" });
67
+ const vnext = flowKind === "vnext_protocolize" || flowKind === "vnext_specify";
68
+ if (input.executionRoutingFile !== undefined && !vnext)
69
+ throw new AppError("usage", "Execution routing input is supported only for vNext RUNs", 2);
70
+ const routingInput = input.executionRoutingFile !== undefined ? loadExecutionRoutingInput(input.executionRoutingFile) : undefined;
71
+ const executionProfile = vnext ? snapshotVnextExecutionProfile(projectRoot, context.ddFlowHome, routingInput, context.now()) : undefined;
40
72
  const flowContract = input.flowKind === "mb-upgrade"
41
73
  ? loadUpgradeFlowContract(context, projectRoot)
42
74
  : loadProjectFlowContract(projectRoot);
43
- registerProject(context, { root: projectRoot });
44
- const project = requireProjectByRoot(context, projectRoot);
45
75
  const workspaceRoot = resolveWorkspaceRoot(input.workspaceRoot ?? projectRoot);
46
- const flowKind = parseRunFlowKind(input.flowKind);
47
- const slug = normalizeSlug(input.slug);
48
- const runId = nextRunId(context, project.id, slug);
49
- const { shortId } = parseFullEntityId(runId);
50
76
  const now = context.now();
51
- const executionProfile = (flowKind === "vnext_protocolize" || flowKind === "vnext_specify")
52
- ? snapshotVnextExecutionProfile(projectRoot)
53
- : undefined;
54
77
  const flowFlags = resolveFlowFlags(flowContract, {
55
78
  flowKind,
56
79
  ...(input.preset ? { preset: input.preset } : {}),
@@ -60,17 +83,42 @@ export function startFlowRun(context, input) {
60
83
  snapshotRevision: 1,
61
84
  now
62
85
  });
86
+ return { projectRoot, flowKind, routingInput, executionProfile, workspaceRoot, slug, subjectId, subjectType, now, flowFlags };
87
+ }
88
+ export function startFlowRun(context, input, prepared = prepareFlowRunStart(context, input)) {
89
+ const { projectRoot, flowKind, routingInput, executionProfile, workspaceRoot, slug, subjectId, subjectType, now, flowFlags } = prepared;
90
+ // Ordinary settings are already frozen by preparation; only scoped authority
91
+ // requires its live acceptance source to survive final binding unchanged.
92
+ if (executionProfile?.operational_decision) {
93
+ if (crypto.createHash("sha256").update(fs.readFileSync(executionProfile.source_path)).digest("hex") !== executionProfile.source_checksum)
94
+ throw new AppError("execution_profile_changed", "Execution profile changed after RUN preparation", 2);
95
+ if (executionProfile.operational_decision)
96
+ assertOperationalDecisionSource(executionProfile.operational_decision, projectRoot, context.now());
97
+ }
98
+ registerProject(context, { root: projectRoot });
99
+ const project = requireProjectByRoot(context, projectRoot);
100
+ const runId = nextRunId(context, project.id, slug);
101
+ const { shortId } = parseFullEntityId(runId);
63
102
  const runHome = projectRunHome(context.ddFlowHome, project.id, runId);
64
103
  const runDirRelative = path.posix.join("runs", runId);
65
104
  const runtimePath = projectRunJsonPath(context.ddFlowHome, project.id, runId);
105
+ const policyBytes = executionProfile?.operational_decision ? fs.readFileSync(path.resolve(projectRoot, executionProfile.operational_decision.policy.path)) : undefined;
106
+ const policySource = policyBytes ? { path: "operational-policy.source.md", sha256: crypto.createHash("sha256").update(policyBytes).digest("hex") } : undefined;
107
+ if (policySource && policySource.sha256 !== executionProfile?.operational_decision?.policy.sha256)
108
+ throw new AppError("operational_decision_policy_changed", "Policy changed during final RUN binding", 2);
109
+ const binding = { project_id: project.id, run_id: runId, project_root: projectRoot, ...(executionProfile?.operational_decision?.scope.eval_id ? { eval_id: executionProfile.operational_decision.scope.eval_id } : {}), ...(executionProfile?.operational_decision?.scope.execution_id ? { execution_id: executionProfile.operational_decision.scope.execution_id } : {}) };
110
+ const operationalBytes = executionProfile ? `${JSON.stringify({ schema_id: "dd-flow/run-operational-contract@1", binding, settings: { merge_delivery: executionProfile.settings.merge_delivery, merge_cleanup: executionProfile.settings.merge_cleanup }, ...(executionProfile.operational_decision ? { decision: executionProfile.operational_decision, policy_source: policySource } : {}) }, null, 2)}\n` : undefined;
111
+ if (executionProfile && operationalBytes)
112
+ executionProfile.operational_contract = { path: "operational-contract.json", sha256: crypto.createHash("sha256").update(operationalBytes).digest("hex"), ...(policySource ? { policy_source: policySource } : {}), binding };
66
113
  const index = {
67
114
  schema_id: runSchemaId,
68
115
  run_id: runId,
69
116
  short_id: shortId,
70
117
  flow_kind: flowKind,
118
+ ...(["vnext_specify", "vnext_protocolize"].includes(flowKind) ? { verification_matrix_contract: "dd-flow/verification-matrix@1", final_check_coverage_contract: "dd-flow/final-check-coverage@1" } : {}),
71
119
  subject: {
72
- id: requiredPlain(input.subjectId, "subject-id"),
73
- title: requiredPlain(input.subjectId, "subject-id")
120
+ id: subjectId,
121
+ title: subjectId
74
122
  },
75
123
  project: {
76
124
  id: project.id,
@@ -126,9 +174,14 @@ export function startFlowRun(context, input) {
126
174
  };
127
175
  ensureDir(path.dirname(runtimePath));
128
176
  ensureDir(runHome);
177
+ if (policyBytes && policySource)
178
+ fs.writeFileSync(path.join(runHome, policySource.path), policyBytes, { flag: "wx" });
179
+ if (operationalBytes)
180
+ fs.writeFileSync(path.join(runHome, "operational-contract.json"), operationalBytes, { flag: "wx" });
181
+ if (routingInput)
182
+ fs.writeFileSync(path.join(runHome, "execution-routing.input.json"), routingInput.bytes, { flag: "wx", mode: 0o600 });
129
183
  bindCurrentEngineToRun(context, { projectRoot, runId, runRoot: runHome });
130
184
  const persistedIndex = persistedVnextIndex(index);
131
- writeJsonFile(runtimePath, runtimeSnapshotForIndex(persistedIndex, 1));
132
185
  context.db.run(`INSERT INTO runs
133
186
  (id, short_id, slug, project_id, project_root, workspace_root, flow_kind, subject_type, subject_id,
134
187
  status, verdict, next_action, runtime_path, run_dir, run_index_path, run_home_path, run_root, layout_version, artifact_root_kind,
@@ -141,7 +194,7 @@ export function startFlowRun(context, input) {
141
194
  project.root,
142
195
  workspaceRoot,
143
196
  flowKind,
144
- requiredPlain(input.subjectType, "subject-type"),
197
+ subjectType,
145
198
  index.subject.id,
146
199
  index.status,
147
200
  index.verdict,
@@ -162,8 +215,9 @@ export function startFlowRun(context, input) {
162
215
  eventType: "flow_run.started",
163
216
  payload: { run_id: runId, flow_kind: flowKind, subject: index.subject, workspace_root: workspaceRoot, run_home: runHome }
164
217
  });
165
- appendRunTimeline(runHome, { at: now, type: "run_started", run_id: runId });
166
- appendRunTimeline(runHome, {
218
+ context.db.afterCommit(() => publishRunProjections(context, project.id, runId));
219
+ appendRunTimeline(context, project.id, runId, { at: now, type: "run_started", run_id: runId });
220
+ appendRunTimeline(context, project.id, runId, {
167
221
  at: now,
168
222
  type: "flow_flags_resolved",
169
223
  run_id: runId,
@@ -173,13 +227,16 @@ export function startFlowRun(context, input) {
173
227
  });
174
228
  return { ok: true, run: flowRunSummary(requireRunById(context, project.id, runId)), index };
175
229
  }
176
- function snapshotVnextExecutionProfile(projectRoot) {
177
- const profile = loadVnextExecutionProfile(projectRoot);
230
+ function snapshotVnextExecutionProfile(projectRoot, ddFlowHome, routingInput, now = new Date().toISOString()) {
231
+ const { profile, bytes } = loadVnextExecutionProfileSnapshot(projectRoot);
232
+ if (profile.operational_decision)
233
+ assertOperationalDecisionSource(profile.operational_decision, projectRoot, now);
178
234
  const sourcePath = executionProfilePath(projectRoot);
179
- return {
235
+ const frozen = freezeRunExecutionRouting(ddFlowHome, {
180
236
  schema_id: "dd-flow/run-execution-profile@1",
181
237
  source_path: sourcePath,
182
- source_checksum: crypto.createHash("sha256").update(fs.readFileSync(sourcePath)).digest("hex"),
238
+ source_checksum: crypto.createHash("sha256").update(bytes).digest("hex"),
239
+ ...(profile.operational_decision ? { operational_decision: profile.operational_decision } : {}),
183
240
  settings: {
184
241
  stage_session_mode: profile.stage_session_mode,
185
242
  plan_review_mode: profile.plan_review_mode,
@@ -188,9 +245,47 @@ function snapshotVnextExecutionProfile(projectRoot) {
188
245
  merge_delivery: profile.merge_delivery,
189
246
  merge_cleanup: profile.merge_cleanup,
190
247
  stop_target: profile.stop_target,
191
- code_bootstrap: profile.code_bootstrap
248
+ ...(profile.code_bootstrap ? { code_bootstrap: profile.code_bootstrap } : {}),
249
+ ...(profile.execution ? { execution: profile.execution } : {})
192
250
  }
193
- };
251
+ }, routingInput);
252
+ if (frozen.settings.merge_mode === "same_session" && frozen.settings.stop_target === "merge_completed"
253
+ && fs.existsSync(path.join(projectRoot, ".memory-bank", "dd-flow", "project-workspace.json"))
254
+ && loadVnextWorkspacePolicy(projectRoot).workspace.route === "feature_worktree") {
255
+ throw new AppError("merge_session_workspace_conflict", "Feature-worktree MERGE targets the integration checkout; use server MERGE or a qualified new Session", 2, { project_root: projectRoot });
256
+ }
257
+ return frozen;
258
+ }
259
+ /** Fresh RUN and fresh stage-entry experiments freeze through the same resolver. */
260
+ export function freezeRunExecutionRouting(ddFlowHome, base, input) {
261
+ const execution = input?.execution ?? base.settings.execution;
262
+ const profiles = execution ? snapshotAgentProfiles(ddFlowHome, execution) : undefined;
263
+ if (execution && profiles && base.settings.merge_mode === "same_session" && base.settings.stop_target === "merge_completed") {
264
+ const mode = base.settings.code_review_mode;
265
+ assertSameSessionMerge({ ddFlowHome, policy: execution, profiles, previousStages: mode === "off" ? ["code"] : mode === "auto" ? ["code", "code-review"] : ["code-review"] });
266
+ }
267
+ return { ...base, ...(profiles ? { agent_profiles: profiles } : {}),
268
+ ...(input ? { routing_input: { source_path: input.source_path, source_checksum: input.source_checksum, artifact: "execution-routing.input.json" } } : {}),
269
+ settings: { ...base.settings, ...(execution ? { execution } : {}) } };
270
+ }
271
+ /** Called only after a stage-entry import has archived and detached source owners. */
272
+ export function importFlowRunExecutionProfile(context, input) {
273
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
274
+ const run = resolveRun(context, project.id, input.runId);
275
+ assertRunMutationAllowed(context, project.id, run.id);
276
+ const index = authoritativeIndex(run);
277
+ const lineage = JSON.parse(fs.readFileSync(input.lineageFile, "utf8"));
278
+ if (lineage.schema_id !== "dd-flow/snapshot-runtime-lineage@1" || lineage.run_id !== run.id || path.resolve(lineage.restored_runtime ?? "") !== path.resolve(context.ddFlowHome)
279
+ || !vnextStages.some(stage => stage.id === input.stage)
280
+ || (context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_controllers'") && context.db.get("SELECT 1 FROM run_controllers WHERE project_id = ? AND run_id = ? LIMIT 1", [project.id, run.id]))
281
+ || index.stage_runs.some(stage => stage.stage === input.stage || !["done", "skipped"].includes(stage.status))
282
+ || context.db.get("SELECT 1 FROM sessions WHERE project_id = ? AND run_id = ? AND status IN ('active','idle') LIMIT 1", [project.id, run.id])) {
283
+ throw new AppError("snapshot_execution_active", "Execution policy can be selected only at a detached stage-entry import", 1);
284
+ }
285
+ index.execution_profile = { ...input.profile, stage_entry_import: { stage: input.stage, lineage_file: input.lineageFile } };
286
+ index.updated_at = context.now();
287
+ persistRunState(context, project, run, index);
288
+ appendRunTimeline(context, project.id, run.id, { at: index.updated_at, type: "execution_policy_imported", stage: input.stage, lineage_file: input.lineageFile, source_checksum: input.profile.routing_input?.source_checksum });
194
289
  }
195
290
  /**
196
291
  * A vNext route may materialize a feature checkout after its semantic
@@ -200,19 +295,82 @@ function snapshotVnextExecutionProfile(projectRoot) {
200
295
  export function rebindFlowRunWorkspace(context, input) {
201
296
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
202
297
  const run = resolveRun(context, project.id, input.runId);
298
+ const generation = assertRunMutationAllowed(context, project.id, run.id, input.expectedGeneration);
203
299
  const workspaceRoot = resolveWorkspaceRoot(input.workspaceRoot);
204
300
  const index = authoritativeIndex(run);
301
+ const receipt = input.routeReceipt ? { ...input.routeReceipt, policy: { ...input.routeReceipt.policy,
302
+ worktree_path: input.routeReceipt.policy.worktree_path ? resolveWorkspaceRoot(input.routeReceipt.policy.worktree_path) : null } } : undefined;
303
+ if (index.execution_bundle && !["vnext_specify", "vnext_protocolize"].includes(run.flow_kind)) {
304
+ if (workspaceRoot === run.workspace_root)
305
+ return { ok: true, changed: false };
306
+ throw new AppError("workspace_route_invalid", "Only SDLC may provision a different RUN workspace", 1, { effect: "no_effect" });
307
+ }
308
+ if (workspaceRoot === run.workspace_root) {
309
+ if (receipt && index.workspace_route && hybridDigest(retainedWorkspaceRoute(context, run).receipt) !== hybridDigest(receipt)) {
310
+ throw new AppError("workspace_route_conflict", "Committed workspace route differs from the repeated request", 1, { effect: "no_effect" });
311
+ }
312
+ publishRunProjections(context, project.id, run.id);
313
+ return { ok: true, changed: false, workspace_root: workspaceRoot };
314
+ }
315
+ if (index.workspace_route)
316
+ throw new AppError("workspace_route_conflict", "RUN already has a committed workspace route", 1, { effect: "no_effect" });
317
+ if (input.expectedRevision !== undefined && (index.runtime_revision ?? 0) !== input.expectedRevision)
318
+ throw new AppError("run_revision_changed", "RUN changed while its workspace was provisioned", 1, { effect: "no_effect", expected_revision: input.expectedRevision, actual_revision: index.runtime_revision ?? 0 });
319
+ if (index.execution_bundle && !receipt)
320
+ throw new AppError("workspace_route_missing", "Bundled SDLC rebind requires its prepared route receipt", 1, { effect: "no_effect" });
321
+ if (receipt && (run.workspace_root !== project.root || receipt.schema_id !== "dd-flow/vnext-workspace-route@1" || receipt.policy.route !== "feature_worktree"
322
+ || !receipt.policy.provisioned || receipt.policy.worktree_path !== workspaceRoot || !receipt.created || !receipt.bootstrap)) {
323
+ throw new AppError("workspace_route_invalid", "Prepared workspace route does not match this RUN", 1, { effect: "no_effect" });
324
+ }
205
325
  const now = context.now();
206
326
  const facts = gitFacts(workspaceRoot);
327
+ if (receipt && (facts.branch !== receipt.policy.feature_branch || !receipt.policy.base_ref || !facts.head
328
+ || spawnSync("git", ["-C", workspaceRoot, "merge-base", "--is-ancestor", receipt.policy.base_ref, facts.head]).status !== 0)) {
329
+ throw new AppError("workspace_route_invalid", "Prepared checkout differs from the feature branch/base receipt", 1, { effect: "no_effect", run_id: run.id });
330
+ }
207
331
  index.workspace = { ...index.workspace, workspace_path: workspaceRoot, branch: facts.branch ?? "unknown", base: facts.head ?? "unknown", manager: "official-worktrunk" };
208
332
  index.execution = { ...index.execution, project_root: project.root, workspace_root: workspaceRoot, git: facts };
209
333
  index.updated_at = now;
210
- context.db.run("UPDATE runs SET workspace_root = ? WHERE project_id = ? AND id = ?", [workspaceRoot, project.id, run.id]);
211
- persistRunState(context, project, { ...run, workspace_root: workspaceRoot }, index);
212
- appendAudit(context, { projectId: project.id, eventType: "flow_run.workspace_rebound", payload: { run_id: run.id, workspace_root: workspaceRoot, reason: input.reason } });
213
- appendRunTimeline(runArtifactRoot(run), { at: now, type: "workspace_rebound", run_id: run.id, workspace_root: workspaceRoot, reason: input.reason, git: facts });
334
+ const route = receipt ? { schema_id: "dd-flow/run-workspace-route@1", run_id: run.id, project_id: project.id, project_root: project.root,
335
+ generation, previous_workspace: run.workspace_root, workspace_root: workspaceRoot, revision: (index.runtime_revision ?? 0) + 1,
336
+ git: facts, receipt, receipt_sha256: hybridDigest(receipt) } : undefined;
337
+ if (route)
338
+ index.workspace_route = { revision: route.revision, sha256: hybridDigest(route) };
339
+ context.db.writeTransaction(() => {
340
+ assertRunMutationAllowed(context, project.id, run.id, generation);
341
+ const latest = resolveRun(context, project.id, run.id);
342
+ if (latest.workspace_root !== run.workspace_root)
343
+ throw new AppError("workspace_route_changed", "RUN route changed during preparation", 1, { effect: "no_effect" });
344
+ context.db.run("UPDATE runs SET workspace_root = ? WHERE project_id = ? AND id = ?", [workspaceRoot, project.id, run.id]);
345
+ persistRunState(context, project, { ...run, workspace_root: workspaceRoot }, index);
346
+ appendAudit(context, { projectId: project.id, eventType: "flow_run.workspace_rebound", payload: { run_id: run.id, workspace_root: workspaceRoot, reason: input.reason, ...(route ? { route } : {}) } });
347
+ appendRunTimeline(context, project.id, run.id, { at: now, type: "workspace_rebound", run_id: run.id, workspace_root: workspaceRoot, reason: input.reason, git: facts });
348
+ }, "runs.rebindWorkspace");
214
349
  return { ok: true, run_id: run.id, project_root: project.root, workspace_root: workspaceRoot, git: facts };
215
350
  }
351
+ function retainedWorkspaceRoute(context, run) {
352
+ const index = authoritativeIndex(run), binding = index.workspace_route;
353
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.workspace_rebound' AND json_extract(payload_json, '$.run_id') = ? AND json_extract(payload_json, '$.route.revision') = ?", [run.project_id, run.id, binding?.revision ?? -1]);
354
+ const original = rows.length === 1 ? JSON.parse(rows[0].payload_json).route : undefined;
355
+ if (!binding || !original || original.schema_id !== "dd-flow/run-workspace-route@1" || hybridDigest(original) !== binding.sha256 || hybridDigest(original.receipt) !== original.receipt_sha256) {
356
+ throw new AppError("workspace_route_invalid", "RUN committed workspace route evidence changed", 1, { effect: "no_effect", run_id: run.id });
357
+ }
358
+ // Relocation receipts are admitted by snapshot import; never rewrite bundle
359
+ // or source route authority merely because a physical checkout moved.
360
+ const route = JSON.parse(projectPacketLocators(JSON.stringify(original), runContextRelocations(context, run.project_id, run.id)));
361
+ if (route.receipt_sha256 !== original.receipt_sha256
362
+ || route.run_id !== run.id || route.project_id !== run.project_id || route.project_root !== run.project_root || route.workspace_root !== run.workspace_root
363
+ || index.workspace.workspace_path !== run.workspace_root || index.execution?.workspace_root !== run.workspace_root || index.execution?.project_root !== run.project_root
364
+ || index.workspace.manager !== "official-worktrunk" || index.workspace.branch !== route.receipt.policy.feature_branch
365
+ // execution.git is the latest observation (stage completion/refresh updates
366
+ // it). The immutable review base is workspace.base, not that moving HEAD.
367
+ || index.workspace.base !== route.git.head
368
+ || !Number.isInteger(route.generation) || route.generation < 0
369
+ || route.receipt.policy.worktree_path !== run.workspace_root) {
370
+ throw new AppError("workspace_route_invalid", "RUN has no exact coherent committed workspace route", 1, { effect: "no_effect", run_id: run.id });
371
+ }
372
+ return route;
373
+ }
216
374
  function loadUpgradeFlowContract(context, projectRoot) {
217
375
  const canon = resolveCanonRoot(context, context.env.DD_MEMORYBANK ? { explicitRoot: context.env.DD_MEMORYBANK } : {});
218
376
  if (!canon.ok || !canon.canon) {
@@ -224,19 +382,24 @@ function loadUpgradeFlowContract(context, projectRoot) {
224
382
  }
225
383
  return loadCanonicalFlowContract(canon.canon.flow_root);
226
384
  }
227
- export function getFlowRunStatus(context, input) {
385
+ export function getFlowRunStatus(context, input, preparedBundle) {
228
386
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
229
387
  const run = resolveRun(context, project.id, input.runId);
388
+ if (preparedBundle)
389
+ assertPreparedRunBundleSqlAuthority(context, { projectId: project.id, runId: run.id }, preparedBundle);
230
390
  const index = authoritativeIndex(run);
231
391
  const runtime = readRuntimeSnapshot(run.runtime_path);
232
- return { ok: true, run: flowRunSummary(run), index, continuation: runContinuation(context, run, index), ...(runtime ? { runtime } : {}), flow_guidance: guidanceForRun(context, run, index) };
392
+ return { ok: true, run: flowRunSummary(run), index, continuation: runContinuation(context, run, index, preparedBundle), ...(runtime ? { runtime } : {}), flow_guidance: guidanceForRun(context, run, index), technical_notes: readTechnicalNotes(context, project.id, run.id) };
233
393
  }
234
394
  /** A read-only projection of the next legal lifecycle action.
235
395
  * It is derived from the persisted RUN/Work facts so controllers never infer a
236
396
  * transition from a model's prose or a hard-coded stage array. */
237
- function runContinuation(context, run, index) {
397
+ function runContinuation(context, run, index, preparedBundle) {
238
398
  if (["done", "failed", "cancelled"].includes(run.status))
239
399
  return { kind: "terminal", stage: null, attempt: null, work_id: null, reason: run.status, command: null };
400
+ const control = runControlDirective(context, run.project_id, run.id);
401
+ if (control)
402
+ return { kind: "wait", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: "operator_control", command: null, control };
240
403
  const active = index.stage_runs.find((stage) => !["done", "skipped"].includes(stage.status));
241
404
  if (active) {
242
405
  const runningWork = context.db.get("SELECT work_id FROM works WHERE project_id = ? AND run_id = ? AND status = 'running' ORDER BY updated_at DESC LIMIT 1", [run.project_id, run.id]);
@@ -247,13 +410,27 @@ function runContinuation(context, run, index) {
247
410
  return { kind: "paused", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: run.status, command: null };
248
411
  if (run.status === "blocked")
249
412
  return { kind: "blocked", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: run.status, command: null };
413
+ if (index.execution_bundle) {
414
+ const bundle = preparedBundle ?? readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id });
415
+ const next = bundledNextStage(bundle.descriptor, index);
416
+ if (!next)
417
+ return { kind: "terminal", stage: null, attempt: null, work_id: null, reason: "flow_complete", command: null };
418
+ const command = bundle.engine().command(context.ddFlowHome, ["stage", "start", run.id, "--stage", next, "--project-root", run.project_root, "--json"]);
419
+ return { kind: "start_stage", stage: next, attempt: null, work_id: null, reason: "legal_successor", command,
420
+ execution: { status: "resolved", ...bundle.execution.stage(next) } };
421
+ }
250
422
  if (run.flow_kind === "vnext_specify" || run.flow_kind === "vnext_protocolize") {
251
423
  const completed = index.stage_runs.filter((stage) => stage.status === "done" || stage.status === "skipped").sort((left, right) => right.order - left.order)[0];
252
424
  const next = completed ? vnextStages.find((stage) => stage.id === completed.stage)?.next.find((candidate) => candidate !== "plan-review" || index.settings?.plan_review?.mode !== "off") : "specify";
253
425
  if (!next)
254
426
  return { kind: "terminal", stage: null, attempt: null, work_id: null, reason: "flow_complete", command: null };
255
- const command = `DD_FLOW_HOME=${JSON.stringify(context.ddFlowHome)} dd-flow stage start ${run.id} --stage ${next} --project-root ${JSON.stringify(run.project_root)} --json`;
256
- return { kind: "start_stage", stage: next, attempt: null, work_id: null, reason: "legal_successor", command };
427
+ const command = `${flowCommand(context)} stage start ${run.id} --stage ${next} --project-root ${JSON.stringify(run.project_root)} --json`;
428
+ const profile = index.execution_profile;
429
+ const execution = profile?.settings.execution ? profile.agent_profiles
430
+ ? { status: "resolved", ...resolveStageTransition({ ddFlowHome: context.ddFlowHome, policy: profile.settings.execution, profiles: profile.agent_profiles, projectRoot: run.project_root, workspaceRoot: run.workspace_root, ...(completed ? { fromStage: completed.stage } : {}), stage: next, stageSessionMode: profile.settings.stage_session_mode, mergeMode: profile.settings.merge_mode }) }
431
+ : { status: "unfrozen", reason: "execution_profile_not_frozen" }
432
+ : undefined;
433
+ return { kind: "start_stage", stage: next, attempt: null, work_id: null, reason: "legal_successor", command, ...(execution ? { execution } : {}) };
257
434
  }
258
435
  return { kind: "wait", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: "no_vnext_continuation", command: null };
259
436
  }
@@ -276,9 +453,12 @@ export function getFlowRunVariables(context, input) {
276
453
  return { ok: true, run_id: run.id, variables };
277
454
  }
278
455
  export function setFlowRunVariable(context, input) {
456
+ return setRunVariable(context, input, input.value, prepareFlowRunVariable(context, input));
457
+ }
458
+ export function prepareFlowRunVariable(context, input) {
279
459
  if (!input.key.startsWith("user."))
280
460
  throw new AppError("validation", "Only user.* RUN variables may be set directly", 2, { key: input.key });
281
- return setRunVariable(context, input, input.value);
461
+ return prepareRunVariable(context, input);
282
462
  }
283
463
  /** Controllers own policy.* and runtime.* values; agents never call this directly. */
284
464
  export function setRuntimeRunVariable(context, input) {
@@ -287,20 +467,7 @@ export function setRuntimeRunVariable(context, input) {
287
467
  return setRunVariable(context, input, input.value);
288
468
  }
289
469
  export function setFlowRunConfig(context, input) {
290
- if (input.key !== "plan_review.mode" && input.key !== "code_review.mode") {
291
- throw new AppError("validation", `Unknown RUN config key: ${input.key}`, 2);
292
- }
293
- if (!["auto", "off", "standard", "deep"].includes(input.value)) {
294
- throw new AppError("validation", "--value for review mode must be auto, off, standard, or deep", 2);
295
- }
296
- const reason = requiredPlain(input.reason, "reason");
297
- const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
298
- const run = resolveRun(context, project.id, input.runId);
299
- const index = authoritativeIndex(run);
300
- const stageName = input.key === "plan_review.mode" ? "plan-review" : "code-review";
301
- if (index.stage_runs.some((stage) => stage.stage === stageName)) {
302
- throw new AppError("invalid_run_config_state", `${input.key} is frozen after ${stageName} starts`, 2, { run_id: run.id });
303
- }
470
+ const { reason, project, run, index } = prepareFlowRunConfig(context, input);
304
471
  const now = context.now();
305
472
  const settings = runSettings(index);
306
473
  const previous = input.key === "plan_review.mode" ? settings.plan_review : settings.code_review;
@@ -315,13 +482,32 @@ export function setFlowRunConfig(context, input) {
315
482
  index.updated_at = now;
316
483
  persistRunState(context, project, run, index);
317
484
  appendAudit(context, { projectId: project.id, eventType: "flow_run.config_set", payload: { run_id: run.id, key: input.key, value: mode, source, reason } });
318
- appendRunTimeline(runArtifactRoot(run), { at: now, type: "run_config_set", run_id: run.id, key: input.key, value: mode, source, reason });
485
+ appendRunTimeline(context, project.id, run.id, { at: now, type: "run_config_set", run_id: run.id, key: input.key, value: mode, source, reason });
319
486
  return { ok: true, run_id: run.id, settings: runSettings(index), idempotent: false };
320
487
  }
488
+ export function prepareFlowRunConfig(context, input) {
489
+ if (input.key !== "plan_review.mode" && input.key !== "code_review.mode") {
490
+ throw new AppError("validation", `Unknown RUN config key: ${input.key}`, 2);
491
+ }
492
+ if (!["auto", "off", "standard", "deep"].includes(input.value)) {
493
+ throw new AppError("validation", "--value for review mode must be auto, off, standard, or deep", 2);
494
+ }
495
+ const reason = requiredPlain(input.reason, "reason");
496
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
497
+ const run = resolveRun(context, project.id, input.runId);
498
+ assertRunMutationAllowed(context, project.id, run.id);
499
+ const index = authoritativeIndex(run);
500
+ const stageName = input.key === "plan_review.mode" ? "plan-review" : "code-review";
501
+ if (index.stage_runs.some((stage) => stage.stage === stageName)) {
502
+ throw new AppError("invalid_run_config_state", `${input.key} is frozen after ${stageName} starts`, 2, { run_id: run.id });
503
+ }
504
+ return { reason, project, run, index };
505
+ }
321
506
  /** Freeze an automatic review request once from already accepted policy or PLAN facts. */
322
507
  export function freezeFlowRunReviewMode(context, input) {
323
508
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
324
509
  const run = resolveRun(context, project.id, input.runId);
510
+ assertRunMutationAllowed(context, project.id, run.id);
325
511
  const index = authoritativeIndex(run);
326
512
  const settings = runSettings(index);
327
513
  const key = input.review === "plan" ? "plan_review" : "code_review";
@@ -338,8 +524,8 @@ export function getFlowRunFlagsStatus(context, input) {
338
524
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
339
525
  const run = resolveRun(context, project.id, input.runId);
340
526
  const index = authoritativeIndex(run);
341
- const runtime = readRuntimeSnapshot(run.runtime_path);
342
- const flags = runtime?.flow_flags ?? index.flow_flags;
527
+ const legacyRuntime = index.flow_flags ? null : readRuntimeSnapshot(run.runtime_path);
528
+ const flags = index.flow_flags ?? legacyRuntime?.flow_flags;
343
529
  if (!flags) {
344
530
  return {
345
531
  ok: true,
@@ -357,29 +543,31 @@ export function getFlowRunFlagsStatus(context, input) {
357
543
  snapshot_revision: flags.snapshot_revision,
358
544
  snapshot_checksum: flags.snapshot_checksum,
359
545
  flow_flags: flags,
360
- runtime_revision: runtime?.runtime_revision ?? index.runtime_revision ?? null
546
+ runtime_revision: index.runtime_revision ?? legacyRuntime?.runtime_revision ?? null
361
547
  };
362
548
  }
363
549
  export function reviseFlowRunFlags(context, input) {
364
550
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
365
551
  const run = resolveRun(context, project.id, input.runId);
552
+ assertRunMutationAllowed(context, project.id, run.id);
366
553
  const idempotencyKey = requiredPlain(input.idempotencyKey, "idempotency-key");
367
- context.db.exec("BEGIN IMMEDIATE");
368
- let committed = false;
369
- try {
554
+ const priorMutation = context.db.get("SELECT result_json FROM flow_run_flag_mutations WHERE project_id = ? AND run_id = ? AND idempotency_key = ?", [project.id, run.id, idempotencyKey]);
555
+ if (priorMutation)
556
+ return { ...JSON.parse(priorMutation.result_json), idempotent: true };
557
+ // Canonical contract bytes are prepared before taking the writer. Revision
558
+ // and idempotency are compared again against SQL authority under the lock.
559
+ const contract = loadProjectFlowContract(run.project_root);
560
+ return context.db.writeTransaction(() => {
370
561
  const existingMutation = context.db.get("SELECT result_json FROM flow_run_flag_mutations WHERE project_id = ? AND run_id = ? AND idempotency_key = ?", [project.id, run.id, idempotencyKey]);
371
562
  if (existingMutation) {
372
563
  const result = { ...JSON.parse(existingMutation.result_json), idempotent: true };
373
- context.db.exec("COMMIT");
374
- committed = true;
375
564
  return result;
376
565
  }
377
566
  // Read the authority only after the write lock: two callers with the same
378
567
  // expected revision must not both pass the compare-and-set check.
379
568
  const lockedRun = requireRunById(context, project.id, run.id);
380
569
  const index = authoritativeIndex(lockedRun);
381
- const runtime = readRuntimeSnapshot(lockedRun.runtime_path);
382
- const current = runtime?.flow_flags ?? index.flow_flags;
570
+ const current = index.flow_flags;
383
571
  if (!current) {
384
572
  throw new AppError("validation", "Legacy RUN has no flow-flag snapshot; revise is unavailable", 2, { run_id: run.id });
385
573
  }
@@ -396,7 +584,6 @@ export function reviseFlowRunFlags(context, input) {
396
584
  });
397
585
  }
398
586
  const now = context.now();
399
- const contract = loadProjectFlowContract(lockedRun.project_root);
400
587
  const next = resolveFlowFlags(contract, {
401
588
  flowKind: index.flow_kind,
402
589
  preset: input.preset ?? current.preset.applied,
@@ -452,14 +639,12 @@ export function reviseFlowRunFlags(context, input) {
452
639
  context.db.run(`INSERT INTO flow_run_flag_mutations
453
640
  (project_id, run_id, idempotency_key, expected_revision, resulting_revision, result_json, created_at)
454
641
  VALUES (?, ?, ?, ?, ?, ?, ?)`, [project.id, run.id, idempotencyKey, input.expectedRevision, next.snapshot_revision, JSON.stringify(result), now]);
455
- context.db.exec("COMMIT");
456
- committed = true;
457
642
  appendAudit(context, {
458
643
  projectId: project.id,
459
644
  eventType: "flow_run.flags_revised",
460
645
  payload: { run_id: run.id, snapshot_revision: next.snapshot_revision, snapshot_checksum: next.snapshot_checksum, ...(reason ? { reason } : {}) }
461
646
  });
462
- appendRunTimeline(runArtifactRoot(run), {
647
+ appendRunTimeline(context, project.id, run.id, {
463
648
  at: now,
464
649
  type: "flow_flags_revised",
465
650
  run_id: run.id,
@@ -469,12 +654,7 @@ export function reviseFlowRunFlags(context, input) {
469
654
  ...(reason ? { reason } : {})
470
655
  });
471
656
  return result;
472
- }
473
- catch (error) {
474
- if (!committed)
475
- context.db.exec("ROLLBACK");
476
- throw error;
477
- }
657
+ }, "runs.reviseFlowFlags");
478
658
  }
479
659
  export function listFlowRuns(context, input) {
480
660
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
@@ -485,113 +665,273 @@ export function listFlowRuns(context, input) {
485
665
  .map(flowRunSummary);
486
666
  return { ok: true, project, runs };
487
667
  }
488
- export function attachFlowRunStage(context, input) {
668
+ export function prepareFlowRunStageAttachment(context, input, archive = false) {
489
669
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
490
670
  const run = resolveRun(context, project.id, input.runId);
491
- const index = authoritativeIndex(run);
492
- const now = context.now();
671
+ const generation = assertRunMutationAllowed(context, project.id, run.id);
493
672
  const stage = requiredPlain(input.stage, "stage");
494
- const dir = requiredStageDir(input.dir);
673
+ const dir = prepareStageDirectory(input.dir);
674
+ const index = authoritativeIndex(run);
495
675
  const status = parseStageStatus(input.status);
496
676
  const existing = index.stage_runs.find((item) => item.stage === stage);
497
- assertVnextStageStart(run, index, stage, status, existing);
498
- if (status === "running" && existing) {
499
- archiveExistingStageAttempt(context, project.id, run.id, runArtifactRoot(run), dir);
500
- }
501
- const stageRun = {
502
- ...(existing ?? { order: index.stage_runs.length + 1 }),
503
- stage,
504
- dir,
505
- status,
506
- ...(input.dataSchemaId ? { data_schema_id: input.dataSchemaId } : existing?.data_schema_id ? { data_schema_id: existing.data_schema_id } : {}),
507
- updated_at: now
508
- };
509
- if (status === "running") {
510
- stageRun.started_at = now;
511
- delete stageRun.completed_at;
512
- stageRun.duration_ms = null;
513
- stageRun.git = gitFacts(run.workspace_root);
514
- stageRun.attempt = nextAttempt(existing);
515
- }
516
- upsertStage(index, stageRun);
517
- index.current_stage = stage;
518
- index.attempts = index.stage_runs.map((item) => ({ stage: item.stage, attempt_number: Number((item.attempt ?? "try-001").replace("try-", "")) || 1, root: item.dir, archive: null, status: item.status, started_at: item.started_at ?? now, finished_at: item.completed_at ?? null }));
519
- index.updated_at = now;
520
- persistRunState(context, project, run, index);
521
- appendAudit(context, {
522
- projectId: project.id,
523
- eventType: "flow_run.stage_attached",
524
- payload: { run_id: run.id, stage, dir, status }
525
- });
526
- appendRunTimeline(runArtifactRoot(run), { at: now, type: "stage_attached", run_id: run.id, stage, status, attempt: stageRun.attempt ?? null });
527
- // A stage transition must not rewrite every Session in the RUN. A RUN may
528
- // contain independent reviewer/worker sessions; their stage is set by the
529
- // stage/work binding that actually owns that Session.
530
- if (status === "running")
531
- refreshRunSessionProjection(context, project.id, run.id);
677
+ const bundle = index.execution_bundle ? readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id }) : null;
678
+ if (bundle)
679
+ assertBundledStageStart(bundle.descriptor, index, stage, dir, status, existing);
680
+ else
681
+ assertVnextStageStart(run, index, stage, status, existing);
682
+ if (archive) {
683
+ const runRoot = runArtifactRoot(run);
684
+ if (fs.existsSync(path.join(runRoot, `.archive-${dir}.json`)) || status === "running" && existing) {
685
+ archiveExistingStageAttempt(context, project.id, run.id, runRoot, dir);
686
+ }
687
+ }
688
+ return { project, run, index, stage, dir, status, existing, generation, bundle, attempt: status === "running" ? nextAttempt(existing) : existing?.attempt ?? "try-001", git: status === "running" ? gitFacts(run.workspace_root) : undefined };
689
+ }
690
+ export function attachFlowRunStage(context, input, prepared = prepareFlowRunStageAttachment(context, input, true)) {
691
+ // Filesystem archive and Git inspection are preparation, never writer work.
692
+ const { project, run, stage, dir, status, generation, git } = prepared;
693
+ const attached = context.db.writeTransaction(() => {
694
+ assertAssignedSettlementCurrent(context);
695
+ assertRunMutationAllowed(context, project.id, run.id, generation);
696
+ const latest = requireRunById(context, project.id, run.id);
697
+ const index = authoritativeIndex(latest);
698
+ const existing = index.stage_runs.find((item) => item.stage === stage);
699
+ if ((index.runtime_revision ?? 0) !== (prepared.index.runtime_revision ?? 0) || existing?.attempt !== prepared.existing?.attempt) {
700
+ throw new AppError("stage_revision_changed", "Stage changed during start preparation", 1, { run_id: run.id, stage });
701
+ }
702
+ if (prepared.bundle) {
703
+ if (index.execution_bundle?.manifest_sha256 !== prepared.bundle.sha256)
704
+ throw new AppError("run_bundle_anchor_invalid", "RUN bundle changed during stage preparation", 1);
705
+ assertBundledStageStart(prepared.bundle.descriptor, index, stage, dir, status, existing);
706
+ }
707
+ else
708
+ assertVnextStageStart(latest, index, stage, status, existing);
709
+ const now = context.now();
710
+ const stageRun = {
711
+ ...(existing ?? { order: index.stage_runs.length + 1 }),
712
+ stage,
713
+ dir,
714
+ status,
715
+ ...(input.inputSha256 ? { input_sha256: input.inputSha256 } : {}),
716
+ ...(input.dataSchemaId ? { data_schema_id: input.dataSchemaId } : existing?.data_schema_id ? { data_schema_id: existing.data_schema_id } : {}),
717
+ updated_at: now
718
+ };
719
+ if (status === "running") {
720
+ stageRun.started_at = now;
721
+ delete stageRun.completed_at;
722
+ stageRun.duration_ms = null;
723
+ if (git)
724
+ stageRun.git = git;
725
+ stageRun.attempt = prepared.attempt;
726
+ }
727
+ upsertStage(index, stageRun);
728
+ index.current_stage = stage;
729
+ index.attempts = index.stage_runs.map((item) => ({ stage: item.stage, attempt_number: Number((item.attempt ?? "try-001").replace("try-", "")) || 1, root: item.dir, archive: null, status: item.status, started_at: item.started_at ?? now, finished_at: item.completed_at ?? null }));
730
+ index.updated_at = now;
731
+ persistRunState(context, project, run, index);
732
+ appendAudit(context, {
733
+ projectId: project.id,
734
+ eventType: "flow_run.stage_attached",
735
+ payload: { run_id: run.id, stage, dir, status }
736
+ });
737
+ appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_attached", run_id: run.id, stage, status, attempt: stageRun.attempt ?? null });
738
+ context.db.afterCommit(() => reconcileRunStageReports(context, project.id, run.id, stage));
739
+ // A stage transition must not rewrite every Session in the RUN. A RUN may
740
+ // contain independent reviewer/worker sessions; their stage is set by the
741
+ // stage/work binding that actually owns that Session.
742
+ if (status === "running")
743
+ context.db.afterCommit(() => refreshRunSessionProjection(context, project.id, run.id));
744
+ return { stageRun, index };
745
+ }, "stage.attach");
532
746
  const updatedRun = requireRunById(context, project.id, run.id);
533
- return { ok: true, run: flowRunSummary(updatedRun), stage_run: stageRun, index, flow_guidance: guidanceForRun(context, updatedRun, index) };
534
- }
535
- export function completeFlowRunStage(context, input) {
747
+ return { ok: true, run: flowRunSummary(updatedRun), stage_run: attached.stageRun, index: attached.index, flow_guidance: guidanceForRun(context, updatedRun, attached.index) };
748
+ }
749
+ /** Keep synchronous stage settlement atomic; files are projections, SQLite is authoritative. */
750
+ export function withStageSettlement(context, operation) {
751
+ return context.db.writeTransaction(() => {
752
+ assertAssignedSettlementCurrent(context);
753
+ return operation();
754
+ }, "stage.settle");
755
+ }
756
+ export function prepareFlowRunStageCompletion(context, input, admittedBundle) {
757
+ for (const [name, value] of Object.entries({ stageReport: input.stageReport, data: input.data, dataSchemaId: input.dataSchemaId, report: input.report })) {
758
+ if (value !== undefined)
759
+ requiredPlain(value, name);
760
+ }
761
+ if (input.aliases !== undefined) {
762
+ if (!Array.isArray(input.aliases))
763
+ throw new AppError("validation", "artifact aliases must be an array", 2);
764
+ for (const alias of input.aliases)
765
+ requiredPlain(alias, "artifact alias");
766
+ }
536
767
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
537
768
  const run = resolveRun(context, project.id, input.runId);
769
+ if (["done", "failed", "skipped"].includes(input.status))
770
+ assertStageSettlementAllowed(context, project.id, run.id, input.stage);
771
+ else
772
+ assertRunMutationAllowed(context, project.id, run.id);
538
773
  const index = authoritativeIndex(run);
539
- const now = context.now();
540
774
  const stage = requiredPlain(input.stage, "stage");
541
775
  const status = parseStageStatus(input.status);
542
776
  const existing = index.stage_runs.find((item) => item.stage === stage);
543
777
  if (!existing) {
544
778
  throw new AppError("not_found", `Run stage is not attached: ${stage}`, 1, { run_id: run.id, stage });
545
779
  }
546
- const stageRun = {
547
- ...existing,
548
- status,
549
- ...(input.stageReport ? { stage_report: input.stageReport } : {}),
550
- ...(input.data ? { data: input.data } : {}),
551
- ...(input.dataSchemaId ? { data_schema_id: input.dataSchemaId } : {}),
552
- ...(input.report ? { report: input.report } : {}),
553
- ...(input.aliases && input.aliases.length > 0 ? { artifact_aliases: input.aliases } : {}),
554
- updated_at: now,
555
- ...(status === "done" || status === "waiting_for_user" || status === "blocked" || status === "failed" || status === "skipped"
556
- ? { completed_at: now, duration_ms: durationMs(existing.started_at ?? null, now), git: gitFacts(run.workspace_root) }
557
- : {})
558
- };
559
- upsertStage(index, stageRun);
560
- if (status === "waiting_for_user") {
561
- index.status = "waiting_for_user";
562
- index.verdict = "waiting_for_user";
563
- }
564
- else if (status === "blocked" || status === "failed") {
565
- index.status = status;
566
- index.verdict = status;
780
+ // Bound completion must admit the same immutable descriptor/engine authority
781
+ // as stage entry. Keep filesystem admission outside the SQLite writer.
782
+ const bundle = admittedBundle === undefined
783
+ ? readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id }) : admittedBundle;
784
+ if (bundle && !index.execution_bundle)
785
+ throw new AppError("run_bundle_anchor_invalid", "RUN bundle anchor disappeared after finish admission", 1);
786
+ if (bundle)
787
+ assertCompletionBundleAuthority(context, run, index);
788
+ if (index.execution_bundle && (!bundle || bundle.sha256 !== index.execution_bundle.manifest_sha256
789
+ || bundle.manifest.descriptor.sha256 !== index.execution_bundle.descriptor_sha256
790
+ || hybridDigest(bundleAuthority(bundle.admission())) !== index.execution_bundle.admission_sha256)) {
791
+ throw new AppError("run_bundle_anchor_invalid", "Prepared finish bundle does not match authoritative RUN admission", 1);
567
792
  }
568
- else if (status === "done" && index.status === "waiting_for_user") {
569
- index.status = "running";
570
- index.verdict = "running";
793
+ if (bundle) {
794
+ const declared = bundle.descriptor.stage(stage);
795
+ if (declared.directory !== existing.dir || index.current_stage !== stage) {
796
+ throw new AppError("flow_stage_completion_invalid", "Completion does not belong to the active descriptor stage", 2, { phase: "prepare", effect: "no_effect", run_id: run.id, stage });
797
+ }
571
798
  }
572
- index.current_stage = stage;
573
- index.attempts = index.stage_runs.map((item) => ({ stage: item.stage, attempt_number: Number((item.attempt ?? "try-001").replace("try-", "")) || 1, root: item.dir, archive: null, status: item.status, started_at: item.started_at ?? now, finished_at: item.completed_at ?? null }));
574
- index.updated_at = now;
575
- index.execution = {
576
- project_root: index.execution?.project_root ?? run.project_root,
577
- workspace_root: index.execution?.workspace_root ?? run.workspace_root,
578
- git: gitFacts(run.workspace_root)
579
- };
580
- persistRunState(context, project, run, index);
581
- refreshRunSessionProjection(context, project.id, run.id);
582
- appendAudit(context, {
583
- projectId: project.id,
584
- eventType: "flow_run.stage_completed",
585
- payload: { run_id: run.id, stage, status, stage_report: input.stageReport ?? null, data: input.data ?? null }
586
- });
587
- appendRunTimeline(runArtifactRoot(run), { at: now, type: "stage_completed", run_id: run.id, stage, status, attempt: stageRun.attempt ?? null });
799
+ const dataPath = input.data ? path.join(runArtifactRoot(run), existing.dir, input.data) : null;
800
+ const dataSha256 = input.dataSha256 ?? (dataPath && fs.existsSync(dataPath) ? crypto.createHash("sha256").update(fs.readFileSync(dataPath)).digest("hex") : null);
801
+ if (dataSha256 && !/^[a-f0-9]{64}$/.test(dataSha256))
802
+ throw new AppError("validation", "Stage data SHA-256 is invalid", 2);
803
+ return { project, run, index, stage, status, existing, bundle, dataSha256, git: input.git ?? gitFacts(run.workspace_root), generation: ["done", "failed", "skipped"].includes(status) ? assertStageSettlementAllowed(context, project.id, run.id, stage) : assertRunMutationAllowed(context, project.id, run.id) };
804
+ }
805
+ /** Raw historical control is not a finish-sealing bypass for a descriptor-bound RUN. */
806
+ export function prepareRawFlowRunStageCompletion(context, input) {
807
+ const prepared = prepareFlowRunStageCompletion(context, input);
808
+ if (prepared.bundle)
809
+ throw new AppError("stage_finish_required", "Descriptor-bound stages must settle through their registered stage finish handler", 2, { phase: "prepare", effect: "no_effect", run_id: prepared.run.id, stage: prepared.stage });
810
+ return prepared;
811
+ }
812
+ function stageUsageProjection(context, run, stage, startedAt) {
813
+ if (!startedAt)
814
+ return { status: "unavailable", input_tokens: null, cached_input_tokens: null, output_tokens: null, total_tokens: null, session_count: 0, observed_at: context.now() };
815
+ const value = recalculateRunUsage(context, { projectId: run.project_id, runId: run.id, stage: { name: stage, startedAt, completedAt: context.now() } });
816
+ const totals = value.totals, coverage = value.coverage;
817
+ const count = (name) => typeof coverage?.[name] === "number" ? coverage[name] : 0;
818
+ const measured = count("measured"), other = Object.values(coverage ?? {}).reduce((total, item) => total + (typeof item === "number" ? item : 0), 0) - measured;
819
+ const tokens = (name) => typeof totals?.[name] === "number" ? totals[name] : null;
820
+ return { status: measured === 0 ? "unavailable" : other > 0 || ["input_tokens", "cache_read_input_tokens", "output_tokens", "total_tokens"].some(name => tokens(name) === null) ? "partial" : "measured", input_tokens: tokens("input_tokens"), cached_input_tokens: tokens("cache_read_input_tokens"), output_tokens: tokens("output_tokens"), total_tokens: tokens("total_tokens"), session_count: Array.isArray(value.sessions) ? value.sessions.length : 0, observed_at: typeof value.observed_at === "string" ? value.observed_at : context.now() };
821
+ }
822
+ export function completeFlowRunStage(context, input, prepared = prepareFlowRunStageCompletion(context, input)) {
823
+ const { project, run, stage, status, dataSha256, git, generation } = prepared;
824
+ const completed = context.db.writeTransaction(() => {
825
+ assertAssignedSettlementCurrent(context);
826
+ if (["done", "failed", "skipped"].includes(status))
827
+ assertStageSettlementAllowed(context, project.id, run.id, stage, generation);
828
+ else
829
+ assertRunMutationAllowed(context, project.id, run.id, generation);
830
+ const latest = requireRunById(context, project.id, run.id);
831
+ const index = authoritativeIndex(latest);
832
+ const existing = index.stage_runs.find((item) => item.stage === stage);
833
+ if (!existing || existing.attempt !== prepared.existing.attempt || index.runtime_revision !== prepared.index.runtime_revision) {
834
+ throw new AppError("stage_revision_changed", "Stage attempt changed during finish preparation", 1, { run_id: run.id, stage, expected_attempt: prepared.existing.attempt, actual_attempt: existing?.attempt ?? null });
835
+ }
836
+ if (hybridDigest(index.execution_bundle ?? null) !== hybridDigest(prepared.index.execution_bundle ?? null)) {
837
+ throw new AppError("run_bundle_anchor_invalid", "RUN bundle authority changed during finish preparation", 1);
838
+ }
839
+ if (prepared.bundle && (index.current_stage !== stage || existing.dir !== prepared.bundle.descriptor.stage(stage).directory)) {
840
+ throw new AppError("flow_stage_completion_invalid", "Active descriptor stage changed during finish preparation", 1);
841
+ }
842
+ if (prepared.bundle)
843
+ assertCompletionBundleAuthority(context, latest, index);
844
+ const now = context.now();
845
+ const stageRun = {
846
+ ...existing,
847
+ status,
848
+ ...(input.stageReport ? { stage_report: input.stageReport } : {}),
849
+ ...(input.data ? { data: input.data } : {}),
850
+ ...(dataSha256 ? { data_sha256: dataSha256 } : {}),
851
+ ...(input.dataSchemaId ? { data_schema_id: input.dataSchemaId } : {}),
852
+ ...(input.report ? { report: input.report } : {}),
853
+ usage: input.usage ?? stageUsageProjection(context, latest, stage, existing.started_at),
854
+ ...(input.aliases && input.aliases.length > 0 ? { artifact_aliases: input.aliases } : {}),
855
+ updated_at: now,
856
+ ...(status === "done" || status === "waiting_for_user" || status === "blocked" || status === "failed" || status === "skipped"
857
+ ? { completed_at: now, duration_ms: durationMs(existing.started_at ?? null, now), git }
858
+ : {})
859
+ };
860
+ upsertStage(index, stageRun);
861
+ if (status === "waiting_for_user") {
862
+ index.status = "waiting_for_user";
863
+ index.verdict = "waiting_for_user";
864
+ }
865
+ else if (status === "blocked" || status === "failed") {
866
+ index.status = status;
867
+ index.verdict = status;
868
+ }
869
+ else if (status === "done" && index.status === "waiting_for_user") {
870
+ index.status = "running";
871
+ index.verdict = "running";
872
+ }
873
+ index.current_stage = stage;
874
+ index.attempts = index.stage_runs.map((item) => ({ stage: item.stage, attempt_number: Number((item.attempt ?? "try-001").replace("try-", "")) || 1, root: item.dir, archive: null, status: item.status, started_at: item.started_at ?? now, finished_at: item.completed_at ?? null }));
875
+ index.updated_at = now;
876
+ index.execution = {
877
+ project_root: index.execution?.project_root ?? run.project_root,
878
+ workspace_root: index.execution?.workspace_root ?? run.workspace_root,
879
+ git
880
+ };
881
+ persistRunState(context, project, latest, index);
882
+ context.db.afterCommit(() => refreshRunSessionProjection(context, project.id, run.id));
883
+ appendAudit(context, {
884
+ projectId: project.id,
885
+ eventType: "flow_run.stage_completed",
886
+ payload: { run_id: run.id, stage, status, stage_report: input.stageReport ?? null, data: input.data ?? null }
887
+ });
888
+ appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_completed", run_id: run.id, stage, status, attempt: stageRun.attempt ?? null });
889
+ return { index, stageRun };
890
+ }, "runs.completeFlowRunStage");
588
891
  const updatedRun = requireRunById(context, project.id, run.id);
589
- return { ok: true, run: flowRunSummary(updatedRun), stage_run: stageRun, index, flow_guidance: guidanceForRun(context, updatedRun, index) };
892
+ return { ok: true, run: flowRunSummary(updatedRun), stage_run: completed.stageRun, index: completed.index, flow_guidance: guidanceForRun(context, updatedRun, completed.index) };
893
+ }
894
+ /** Writer-safe readback of independently retained SQL authority; no filesystem admission. */
895
+ function assertCompletionBundleAuthority(context, run, index) {
896
+ const anchor = index.execution_bundle;
897
+ if (!anchor)
898
+ throw new AppError("run_bundle_anchor_invalid", "RUN bundle anchor disappeared during finish", 1);
899
+ assertRunBundleSqlAuthority(context, { projectId: run.project_id, runId: run.id }, anchor);
900
+ }
901
+ /** Metadata-only fencing shared by lifecycle ownership and the settlement writer.
902
+ * False means genuine historical authority, never a lost/orphaned bundle anchor.
903
+ * Physical descriptor/engine admission remains an independent outside-writer check.
904
+ */
905
+ export function assertRunBundleSqlAuthority(context, scope, anchor) {
906
+ const hasAuthority = context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_bundle_authorities'");
907
+ const row = hasAuthority ? context.db.get("SELECT manifest_sha256, admission_json FROM run_bundle_authorities WHERE project_id = ? AND run_id = ?", [scope.projectId, scope.runId]) : undefined;
908
+ if (!anchor) {
909
+ if (row)
910
+ throw new AppError("run_bundle_anchor_invalid", "Retained RUN authority exists without its execution anchor", 1);
911
+ return false;
912
+ }
913
+ if (anchor.schema_id !== "dd-flow/run-execution-bundle@1" || typeof anchor.flow_id !== "string"
914
+ || [anchor.manifest_sha256, anchor.descriptor_sha256, anchor.admission_sha256].some(hash => typeof hash !== "string" || !/^[a-f0-9]{64}$/.test(hash)))
915
+ throw new AppError("run_bundle_anchor_invalid", "RUN bundle anchor is invalid", 1);
916
+ if (!row || row.manifest_sha256 !== anchor.manifest_sha256)
917
+ throw new AppError("run_bundle_authority_missing", "RUN bundle admission authority disappeared during finish", 1);
918
+ let valid = false;
919
+ try {
920
+ if (Buffer.byteLength(row.admission_json, "utf8") <= 1024 * 1024) {
921
+ const retained = JSON.parse(row.admission_json);
922
+ valid = retained.runId === scope.runId && hybridDigest(retained) === anchor.admission_sha256;
923
+ }
924
+ }
925
+ catch { /* Corrupt authority cannot be replaced by a prepared object. */ }
926
+ if (!valid)
927
+ throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority changed during finish", 1);
928
+ return true;
590
929
  }
591
930
  /** Pause one live stage without completing it or creating a new attempt. */
592
931
  export function pauseFlowRunStage(context, input) {
593
932
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
594
933
  const run = resolveRun(context, project.id, input.runId);
934
+ assertRunMutationAllowed(context, project.id, run.id);
595
935
  const index = authoritativeIndex(run);
596
936
  const stage = requiredPlain(input.stage, "stage");
597
937
  const stageRun = index.stage_runs.find((item) => item.stage === stage);
@@ -620,13 +960,14 @@ export function pauseFlowRunStage(context, input) {
620
960
  persistRunState(context, project, run, index);
621
961
  refreshRunSessionProjection(context, project.id, run.id);
622
962
  appendAudit(context, { projectId: project.id, eventType: "flow_run.stage_paused", payload: { run_id: run.id, stage, work_id: input.workId, pause_id: input.pauseId, reason: "waiting_for_user" } });
623
- appendRunTimeline(runArtifactRoot(run), { at: now, type: "stage_waiting_for_user", run_id: run.id, stage, work_id: input.workId, pause_id: input.pauseId, question_path: input.questionPath });
963
+ appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_waiting_for_user", run_id: run.id, stage, work_id: input.workId, pause_id: input.pauseId, question_path: input.questionPath });
624
964
  return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
625
965
  }
626
966
  /** Resume the same stage, Work and attempt after a user answer is recorded. */
627
967
  export function resumeFlowRunStage(context, input) {
628
968
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
629
969
  const run = resolveRun(context, project.id, input.runId);
970
+ assertRunMutationAllowed(context, project.id, run.id);
630
971
  const index = authoritativeIndex(run);
631
972
  const stage = requiredPlain(input.stage, "stage");
632
973
  const stageRun = index.stage_runs.find((item) => item.stage === stage);
@@ -650,19 +991,28 @@ export function resumeFlowRunStage(context, input) {
650
991
  persistRunState(context, project, run, index);
651
992
  refreshRunSessionProjection(context, project.id, run.id);
652
993
  appendAudit(context, { projectId: project.id, eventType: "flow_run.stage_resumed", payload: { run_id: run.id, stage, work_id: input.workId, pause_id: pause.id, answer_path: input.answerPath } });
653
- appendRunTimeline(runArtifactRoot(run), { at: now, type: "user_answer_received", run_id: run.id, stage, work_id: input.workId, pause_id: pause.id, answer_path: input.answerPath });
654
- appendRunTimeline(runArtifactRoot(run), { at: now, type: "stage_resumed", run_id: run.id, stage, work_id: input.workId, pause_id: pause.id, paused_ms: pausedMs });
994
+ appendRunTimeline(context, project.id, run.id, { at: now, type: "user_answer_received", run_id: run.id, stage, work_id: input.workId, pause_id: pause.id, answer_path: input.answerPath });
995
+ appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_resumed", run_id: run.id, stage, work_id: input.workId, pause_id: pause.id, paused_ms: pausedMs });
655
996
  return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
656
997
  }
657
998
  /** Pause the current stage for a non-user runtime blocker. */
658
- export function blockFlowRunStage(context, input) {
999
+ export function prepareFlowRunStageBlock(context, input) {
1000
+ if (!["engine", "harness", "environment"].includes(input.kind) || typeof input.retryable !== "boolean")
1001
+ throw new AppError("validation", "stage block requires a valid kind and boolean retryable", 2);
1002
+ requiredPlain(input.code, "code");
1003
+ requiredPlain(input.summary, "summary");
659
1004
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
660
1005
  const run = resolveRun(context, project.id, input.runId);
1006
+ assertRunMutationAllowed(context, project.id, run.id);
661
1007
  const index = authoritativeIndex(run);
662
1008
  const stage = requiredPlain(input.stage, "stage");
663
1009
  const stageRun = index.stage_runs.find((item) => item.stage === stage);
664
1010
  if (!stageRun || stageRun.status !== "running")
665
1011
  throw new AppError("invalid_stage_state", "Only a running stage can be blocked", 1, { run_id: run.id, stage, status: stageRun?.status ?? "missing" });
1012
+ return { project, run, index, stage, stageRun };
1013
+ }
1014
+ export function blockFlowRunStage(context, input, prepared = prepareFlowRunStageBlock(context, input)) {
1015
+ const { project, run, index, stage, stageRun } = prepared;
666
1016
  const now = context.now();
667
1017
  stageRun.status = "blocked";
668
1018
  stageRun.updated_at = now;
@@ -676,20 +1026,24 @@ export function blockFlowRunStage(context, input) {
676
1026
  persistRunState(context, project, run, index);
677
1027
  refreshRunSessionProjection(context, project.id, run.id);
678
1028
  appendAudit(context, { projectId: project.id, eventType: "flow_run.stage_blocked", payload: { run_id: run.id, stage, work_id: input.workId, kind: input.kind, code: input.code, retryable: input.retryable } });
679
- appendRunTimeline(runArtifactRoot(run), { at: now, type: "stage_blocked", run_id: run.id, stage, work_id: input.workId, kind: input.kind, code: input.code, summary: input.summary, retryable: input.retryable });
1029
+ appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_blocked", run_id: run.id, stage, work_id: input.workId, kind: input.kind, code: input.code, summary: input.summary, retryable: input.retryable });
680
1030
  return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
681
1031
  }
682
1032
  /** Continue the same stage after its non-user blocker was repaired externally. */
683
- export function unblockFlowRunStage(context, input) {
1033
+ export function prepareFlowRunStageUnblock(context, input) {
684
1034
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
685
1035
  const run = resolveRun(context, project.id, input.runId);
1036
+ assertRunMutationAllowed(context, project.id, run.id);
686
1037
  const index = authoritativeIndex(run);
687
1038
  const stage = requiredPlain(input.stage, "stage");
688
1039
  const stageRun = index.stage_runs.find((item) => item.stage === stage);
689
1040
  if (!stageRun || stageRun.status !== "blocked" || !stageRun.blocker || stageRun.blocker.work_id !== input.workId)
690
1041
  throw new AppError("invalid_stage_state", "Stage unblock requires its matching runtime blocker", 1, { run_id: run.id, stage, work_id: input.workId, status: stageRun?.status ?? "missing" });
1042
+ return { project, run, index, stage, stageRun, blocker: stageRun.blocker };
1043
+ }
1044
+ export function unblockFlowRunStage(context, input, prepared = prepareFlowRunStageUnblock(context, input)) {
1045
+ const { project, run, index, stage, stageRun, blocker } = prepared;
691
1046
  const now = context.now();
692
- const blocker = stageRun.blocker;
693
1047
  const blockedMs = durationMs(blocker.blocked_at, now) ?? 0;
694
1048
  stageRun.status = "running";
695
1049
  stageRun.updated_at = now;
@@ -704,7 +1058,7 @@ export function unblockFlowRunStage(context, input) {
704
1058
  persistRunState(context, project, run, index);
705
1059
  refreshRunSessionProjection(context, project.id, run.id);
706
1060
  appendAudit(context, { projectId: project.id, eventType: "flow_run.stage_unblocked", payload: { run_id: run.id, stage, work_id: input.workId, code: blocker.code } });
707
- appendRunTimeline(runArtifactRoot(run), { at: now, type: "stage_unblocked", run_id: run.id, stage, work_id: input.workId, code: blocker.code, blocked_ms: blockedMs });
1061
+ appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_unblocked", run_id: run.id, stage, work_id: input.workId, code: blocker.code, blocked_ms: blockedMs });
708
1062
  return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
709
1063
  }
710
1064
  function operationalRunStatus(context, projectId, runId) {
@@ -713,11 +1067,76 @@ function operationalRunStatus(context, projectId, runId) {
713
1067
  const leaves = unfinished.filter((work) => !parents.has(work.work_id));
714
1068
  return leaves.length > 0 && leaves.every((work) => work.status === "paused") ? "paused" : "running";
715
1069
  }
716
- export function completeFlowRun(context, input) {
717
- const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
1070
+ /** Keep the logical RUN projection aligned with recovery authority. Stage/Work
1071
+ * evidence and verdicts survive a reversible stop; terminal runs never reopen. */
1072
+ export function setFlowRunRecoveryPaused(context, projectId, runId, paused) {
1073
+ const run = resolveRun(context, projectId, runId);
1074
+ if (!["running", "paused"].includes(run.status))
1075
+ return;
1076
+ const index = authoritativeIndex(run);
1077
+ const status = paused || index.stage_runs.some(stage => ["paused", "waiting_for_user", "blocked"].includes(stage.status))
1078
+ ? "paused" : operationalRunStatus(context, projectId, runId);
1079
+ if (run.status === status && index.status === status)
1080
+ return;
1081
+ index.status = status;
1082
+ index.updated_at = context.now();
1083
+ const project = requireProjectByRoot(context, run.project_root);
1084
+ persistRunState(context, project, run, index);
1085
+ }
1086
+ /** Reconciliation repairs projections under the same revision CAS as normal
1087
+ * lifecycle writes. Unknown physical ownership never closes child records. */
1088
+ export function reconcileStoredFlowRun(context, expected, health, transition, childrenSettled = false) {
1089
+ const run = resolveRun(context, expected.project_id, expected.id);
1090
+ if (run.index_json !== expected.index_json || run.status !== expected.status)
1091
+ throw new AppError("run_revision_changed", "RUN changed during health observation", 1);
1092
+ const index = authoritativeIndex(run), now = context.now();
1093
+ index.health = health;
1094
+ if (transition && !["done", "failed", "cancelled", "discarded"].includes(run.status)) {
1095
+ index.status = transition;
1096
+ if (transition === "failed") {
1097
+ index.verdict = "failed";
1098
+ // Observation time is not the historical exit time. Without a retained
1099
+ // finish receipt, keep timing unknown rather than manufacturing days of work.
1100
+ delete index.completed_at;
1101
+ delete index.finished_at;
1102
+ index.duration_ms = null;
1103
+ index.timing_status = 'legacy_incomplete';
1104
+ closeOpenStagesForOverride(index, "failed", now, false);
1105
+ closeOpenWorksForOverride(context, run.project_id, run.id, "failed", now, false);
1106
+ }
1107
+ else if (childrenSettled && health.state === "owner_lost") {
1108
+ for (const stage of index.stage_runs)
1109
+ if (stage.status === "running") {
1110
+ stage.status = "paused";
1111
+ stage.updated_at = now;
1112
+ }
1113
+ }
1114
+ }
1115
+ if (childrenSettled && ["failed", "cancelled", "discarded"].includes(index.status)) {
1116
+ closeOpenStagesForOverride(index, index.status === "failed" ? "failed" : "cancelled", now, false);
1117
+ closeOpenWorksForOverride(context, run.project_id, run.id, index.status === "failed" ? "failed" : "cancelled", now, false);
1118
+ context.db.run("UPDATE sessions SET status = 'stopped', stop_reason = ?, updated_at = ? WHERE project_id = ? AND run_id = ? AND status IN ('pending','active','waiting_user','blocked','stopping')", [`run_${index.status}`, now, run.project_id, run.id]);
1119
+ context.db.run("UPDATE check_receipts SET status = 'aborted' WHERE project_id = ? AND run_id = ? AND status = 'running'", [run.project_id, run.id]);
1120
+ }
1121
+ // Health observation is not semantic progress or a new completion timestamp.
1122
+ const project = requireProjectByRoot(context, run.project_root);
1123
+ persistRunState(context, project, run, index, { healthOnly: true });
1124
+ appendAudit(context, { projectId: project.id, eventType: "flow_run.health_reconciled", payload: { run_id: run.id, previous_status: run.status, status: index.status, health } });
1125
+ }
1126
+ export function prepareFlowRunCompletion(context, input) {
1127
+ if (input.manualOverrideReason !== undefined) {
1128
+ requiredPlain(input.manualOverrideReason, "override reason");
1129
+ if (!["cancelled", "failed"].includes(input.status))
1130
+ throw new AppError("validation", "Manual RUN override may only cancel or fail a RUN", 2);
1131
+ }
1132
+ if (input.verdict !== undefined)
1133
+ requiredPlain(input.verdict, "verdict");
1134
+ if (input.nextAction !== undefined)
1135
+ requiredPlain(input.nextAction, "next action");
1136
+ const project = requireProjectByRoot(context, input.manualOverrideReason ? canonicalPath(input.projectRoot, false) : resolveProjectRoot(input.projectRoot));
718
1137
  const run = resolveRun(context, project.id, input.runId);
1138
+ assertRunMutationAllowed(context, project.id, run.id);
719
1139
  const index = authoritativeIndex(run);
720
- const now = context.now();
721
1140
  const status = parseRunStatus(input.status);
722
1141
  if (!['done', 'cancelled', 'failed'].includes(status)) {
723
1142
  throw new AppError('validation', 'completeFlowRun accepts only done, cancelled, or failed; use stage block/unblock for a recoverable blocker', 2, { status });
@@ -727,6 +1146,11 @@ export function completeFlowRun(context, input) {
727
1146
  if (activeWork)
728
1147
  throw new AppError("active_work", "RUN cannot complete while Work remains active", 2, { run_id: run.id, work_id: activeWork.work_id, status: activeWork.status });
729
1148
  }
1149
+ return { project, run, index, status };
1150
+ }
1151
+ export function completeFlowRun(context, input) {
1152
+ const { project, run, index, status } = prepareFlowRunCompletion(context, input);
1153
+ const now = context.now();
730
1154
  if (input.manualOverrideReason && (status === "cancelled" || status === "failed")) {
731
1155
  closeOpenStagesForOverride(index, status, now);
732
1156
  closeOpenWorksForOverride(context, project.id, run.id, status, now);
@@ -760,25 +1184,65 @@ export function completeFlowRun(context, input) {
760
1184
  eventType: input.manualOverrideReason ? "flow_run.manual_override" : "flow_run.completed",
761
1185
  payload: { run_id: run.id, status, verdict: index.verdict, next_action: index.next_action, ...(input.manualOverrideReason ? { reason: input.manualOverrideReason } : {}) }
762
1186
  });
763
- appendRunTimeline(runArtifactRoot(run), { at: now, type: input.manualOverrideReason ? "run_manual_override" : "run_completed", run_id: run.id, status, ...(input.manualOverrideReason ? { reason: input.manualOverrideReason } : {}) });
1187
+ appendRunTimeline(context, project.id, run.id, { at: now, type: input.manualOverrideReason ? "run_manual_override" : "run_completed", run_id: run.id, status, ...(input.manualOverrideReason ? { reason: input.manualOverrideReason } : {}) });
764
1188
  const updatedRun = requireRunById(context, project.id, run.id);
765
1189
  return { ok: true, run: flowRunSummary(updatedRun), index, usage, flow_guidance: guidanceForRun(context, updatedRun, index) };
766
1190
  }
1191
+ /** SQL-only terminal writer for an independently prepared descriptor handler.
1192
+ * This closes runtime state, not semantic or Git acceptance. The caller owns
1193
+ * the outer transaction, root settlement and the handler-specific proof fence.
1194
+ */
1195
+ export function completeDescriptorRunState(context, scope, bundle, git) {
1196
+ assertPreparedRunBundleSqlAuthority(context, scope, bundle);
1197
+ assertRunMutationAllowed(context, scope.projectId, scope.runId);
1198
+ const run = requireRunById(context, scope.projectId, scope.runId);
1199
+ const index = authoritativeIndex(run);
1200
+ const terminal = index.stage_runs.at(-1);
1201
+ if (run.status !== "running" || !terminal || terminal.status !== "done"
1202
+ || !bundle.descriptor.descriptor.terminal_stages.includes(terminal.stage)
1203
+ || index.stage_runs.some(stage => ["running", "paused", "waiting_for_user", "blocked", "pending"].includes(stage.status))
1204
+ || context.db.get("SELECT 1 FROM works WHERE project_id=? AND run_id=? AND status IN ('created','running','paused') LIMIT 1", [scope.projectId, scope.runId])
1205
+ || context.db.get("SELECT 1 FROM work_sessions ws JOIN works w ON w.work_id=ws.work_id WHERE w.project_id=? AND w.run_id=? AND ws.status='running' LIMIT 1", [scope.projectId, scope.runId])) {
1206
+ throw new AppError("descriptor_run_settlement_invalid", "Descriptor RUN is not ready for terminal runtime settlement", 1, { effect: "no_effect" });
1207
+ }
1208
+ const project = context.db.get("SELECT * FROM projects WHERE id=?", [scope.projectId]);
1209
+ if (!project)
1210
+ throw new AppError("not_found", "Project is not registered", 1);
1211
+ const now = context.now();
1212
+ index.status = "done";
1213
+ index.verdict = "accepted";
1214
+ index.next_action = null;
1215
+ index.updated_at = now;
1216
+ index.completed_at = now;
1217
+ index.finished_at = now;
1218
+ index.duration_ms = durationMs(index.started_at ?? run.created_at, now);
1219
+ index.execution = { project_root: run.project_root, workspace_root: run.workspace_root, git: { ...git } };
1220
+ persistRunState(context, project, run, index);
1221
+ closeRunOrchestrators(context, scope.projectId, run, "done", now);
1222
+ refreshRunSessionProjection(context, scope.projectId, scope.runId);
1223
+ appendAudit(context, { projectId: scope.projectId, eventType: "flow_run.completed", payload: { run_id: scope.runId, status: "done", verdict: "accepted", next_action: null } });
1224
+ appendRunTimeline(context, scope.projectId, scope.runId, { at: now, type: "run_completed", run_id: scope.runId, status: "done" });
1225
+ }
767
1226
  /** Update a live RUN between stages without manufacturing terminal evidence. */
768
1227
  export function advanceFlowRun(context, input) {
769
1228
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
770
1229
  const run = resolveRun(context, project.id, input.runId);
771
1230
  const index = authoritativeIndex(run);
1231
+ if (input.settlementStage && input.status === "running" && index.current_stage === input.settlementStage && index.stage_runs.some(stage => stage.stage === input.settlementStage && ["done", "skipped"].includes(stage.status)))
1232
+ assertStageSettlementAllowed(context, project.id, run.id, input.settlementStage);
1233
+ else
1234
+ assertRunMutationAllowed(context, project.id, run.id);
1235
+ const control = runControlDirective(context, project.id, run.id);
772
1236
  const now = context.now();
773
1237
  index.status = input.status;
774
1238
  index.verdict = input.verdict;
775
- index.next_action = input.nextAction ?? null;
1239
+ index.next_action = control ? null : input.nextAction ?? null;
776
1240
  index.updated_at = now;
777
1241
  persistRunState(context, project, run, index);
778
- appendAudit(context, { projectId: project.id, eventType: "flow_run.progressed", payload: { run_id: run.id, status: input.status, verdict: input.verdict, next_action: index.next_action } });
779
- appendRunTimeline(runArtifactRoot(run), { at: now, type: "run_progressed", run_id: run.id, status: input.status, next_action: index.next_action });
1242
+ appendAudit(context, { projectId: project.id, eventType: "flow_run.progressed", payload: { run_id: run.id, status: index.status, verdict: input.verdict, next_action: index.next_action } });
1243
+ appendRunTimeline(context, project.id, run.id, { at: now, type: "run_progressed", run_id: run.id, status: index.status, next_action: index.next_action });
780
1244
  const updatedRun = requireRunById(context, project.id, run.id);
781
- return { ok: true, run: flowRunSummary(updatedRun), index, flow_guidance: guidanceForRun(context, updatedRun, index) };
1245
+ return { ok: true, ...(control ? { control } : {}), run: flowRunSummary(updatedRun), index, flow_guidance: guidanceForRun(context, updatedRun, index) };
782
1246
  }
783
1247
  /**
784
1248
  * The only legal backward edge in vNext: an integration gate exposed a
@@ -788,7 +1252,11 @@ export function advanceFlowRun(context, input) {
788
1252
  export function prepareVnextMergeSourceRepairAttempt(context, input) {
789
1253
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
790
1254
  const run = resolveRun(context, project.id, input.runId);
1255
+ assertRunMutationAllowed(context, project.id, run.id);
791
1256
  const index = authoritativeIndex(run);
1257
+ const priorRepair = index.variables?.["merge.source_repair"];
1258
+ if (priorRepair?.merge_request_id === input.mergeRequestId && priorRepair.cycle)
1259
+ return { run_id: run.id, cycle: priorRepair.cycle };
792
1260
  const code = index.stage_runs.find((item) => item.stage === "code");
793
1261
  const review = index.stage_runs.find((item) => item.stage === "code-review");
794
1262
  const merge = index.stage_runs.find((item) => item.stage === "merge");
@@ -837,18 +1305,24 @@ export function prepareVnextMergeSourceRepairAttempt(context, input) {
837
1305
  }));
838
1306
  persistRunState(context, project, run, index);
839
1307
  appendAudit(context, { projectId: project.id, eventType: "flow_run.merge_source_repair_prepared", payload: { run_id: run.id, merge_request_id: input.mergeRequestId, cycle } });
840
- appendRunTimeline(root, { at: now, type: "merge_source_repair_prepared", run_id: run.id, merge_request_id: input.mergeRequestId, cycle });
1308
+ appendRunTimeline(context, project.id, run.id, { at: now, type: "merge_source_repair_prepared", run_id: run.id, merge_request_id: input.mergeRequestId, cycle });
841
1309
  return { run_id: run.id, cycle };
842
1310
  }
843
- function closeOpenStagesForOverride(index, status, now) {
1311
+ function closeOpenStagesForOverride(index, status, now, timingKnown = true) {
844
1312
  const stageStatus = status === "cancelled" ? "skipped" : "failed";
845
1313
  for (const stage of index.stage_runs) {
846
- if (stage.status !== "running" && stage.status !== "pending" && stage.status !== "paused" && stage.status !== "blocked")
1314
+ if (stage.status !== "running" && stage.status !== "paused" && stage.status !== "waiting_for_user" && stage.status !== "blocked")
847
1315
  continue;
848
1316
  stage.status = stageStatus;
849
1317
  stage.updated_at = now;
850
- stage.completed_at = now;
851
- stage.duration_ms = durationMs(stage.started_at ?? null, now);
1318
+ if (timingKnown) {
1319
+ stage.completed_at = now;
1320
+ stage.duration_ms = durationMs(stage.started_at ?? null, now);
1321
+ }
1322
+ else {
1323
+ delete stage.completed_at;
1324
+ stage.duration_ms = null;
1325
+ }
852
1326
  }
853
1327
  index.attempts = index.stage_runs.map((stage) => ({
854
1328
  stage: stage.stage,
@@ -856,20 +1330,20 @@ function closeOpenStagesForOverride(index, status, now) {
856
1330
  root: stage.dir,
857
1331
  archive: null,
858
1332
  status: stage.status,
859
- started_at: stage.started_at ?? now,
1333
+ started_at: stage.started_at ?? null,
860
1334
  finished_at: stage.completed_at ?? null
861
1335
  }));
862
1336
  }
863
- function closeOpenWorksForOverride(context, projectId, runId, status, now) {
1337
+ function closeOpenWorksForOverride(context, projectId, runId, status, now, timingKnown = true) {
864
1338
  const workStatus = status === "cancelled" ? "cancelled" : "failed";
865
1339
  context.db.run(`UPDATE works
866
- SET status = ?, updated_at = ?, completed_at = ?
1340
+ SET status = ?, updated_at = ?, completed_at = CASE WHEN ? = 1 THEN ? ELSE completed_at END
867
1341
  WHERE project_id = ? AND run_id = ?
868
- AND status IN ('created', 'running', 'paused')`, [workStatus, now, now, projectId, runId]);
1342
+ AND status IN ('created', 'running', 'paused')`, [workStatus, now, timingKnown ? 1 : 0, now, projectId, runId]);
869
1343
  context.db.run(`UPDATE work_sessions
870
- SET status = ?, updated_at = ?, completed_at = ?
1344
+ SET status = ?, updated_at = ?, completed_at = CASE WHEN ? = 1 THEN ? ELSE completed_at END
871
1345
  WHERE work_id IN (SELECT work_id FROM works WHERE project_id = ? AND run_id = ?)
872
- AND status <> 'completed'`, [workStatus, now, now, projectId, runId]);
1346
+ AND status <> 'completed'`, [workStatus, now, timingKnown ? 1 : 0, now, projectId, runId]);
873
1347
  }
874
1348
  function closeRunOrchestrators(context, projectId, run, status, now) {
875
1349
  if (!["done", "blocked", "cancelled", "failed"].includes(status))
@@ -886,7 +1360,7 @@ function closeRunOrchestrators(context, projectId, run, status, now) {
886
1360
  context.db.run(`UPDATE flow_session_segments SET ended_at = ?
887
1361
  WHERE project_id = ? AND run_id = ? AND session_id IN (${active.map(() => "?").join(", ")}) AND ended_at IS NULL`, [now, projectId, run.id, ...active.map((session) => session.session_id)]);
888
1362
  for (const session of active) {
889
- appendRunTimeline(runArtifactRoot(run), {
1363
+ appendRunTimeline(context, projectId, run.id, {
890
1364
  at: now,
891
1365
  type: "session_stopped",
892
1366
  run_id: run.id,
@@ -1009,7 +1483,7 @@ export function getFlowRunSessions(context, input) {
1009
1483
  }
1010
1484
  export function appendFlowRunTimelineEvent(context, projectId, runId, event) {
1011
1485
  const run = requireRunById(context, projectId, runId);
1012
- appendRunTimeline(runArtifactRoot(run), { run_id: run.id, ...event });
1486
+ appendRunTimeline(context, projectId, run.id, { run_id: run.id, ...event });
1013
1487
  }
1014
1488
  function guidanceForRun(context, run, index) {
1015
1489
  if (run.flow_kind === "vnext_specify")
@@ -1104,32 +1578,33 @@ function vnextSpecifyGuidance(run, index) {
1104
1578
  };
1105
1579
  }
1106
1580
  function persistRunState(context, project, run, index, options = {}) {
1581
+ // Fenced productive admission is a logical pause, even while physical work
1582
+ // drains. Late admitted Stage results must not revive a stopped RUN.
1583
+ if (index.status === "running" && (!options.healthOnly || context.db.get("SELECT 1 FROM sqlite_schema WHERE name = 'run_recovery_guards'")) && ["draining", "sealed", "resuming"].includes(recoveryGuard(context, project.id, run.id)?.status ?? ""))
1584
+ index.status = "paused";
1585
+ if (!options.healthOnly)
1586
+ delete index.health;
1107
1587
  const runtimePath = run.runtime_path || projectRunJsonPath(context.ddFlowHome, project.id, run.id);
1108
- const previousRuntime = readRuntimeSnapshot(runtimePath);
1109
- const runtimeRevision = Math.max(index.runtime_revision ?? 0, previousRuntime?.runtime_revision ?? 0) + 1;
1110
- index.runtime_revision = runtimeRevision;
1111
- const history = [...(previousRuntime?.flag_revision_history ?? [])];
1112
- if (options.flagRevision) {
1113
- history.push(options.flagRevision);
1114
- }
1115
- const persistedIndex = persistedVnextIndex(index);
1116
- ensureDir(path.dirname(runtimePath));
1117
- writeJsonFile(runtimePath, runtimeSnapshotForIndex(persistedIndex, runtimeRevision, history));
1118
- context.db.run(`UPDATE runs
1119
- SET status = ?, verdict = ?, next_action = ?, index_json = ?, runtime_path = ?, run_index_path = ?,
1120
- updated_at = ?, completed_at = ?
1121
- WHERE project_id = ? AND id = ?`, [
1122
- index.status,
1123
- index.verdict,
1124
- index.next_action,
1125
- JSON.stringify(persistedIndex),
1126
- runtimePath,
1127
- runtimePath,
1128
- index.updated_at,
1129
- index.completed_at ?? null,
1130
- project.id,
1131
- run.id
1132
- ]);
1588
+ context.db.writeTransaction(() => {
1589
+ const stored = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, run.id]);
1590
+ if (!stored)
1591
+ throw new AppError("not_found", "RUN is not registered", 1, { run_id: run.id });
1592
+ const previous = JSON.parse(stored.index_json);
1593
+ const actualRevision = previous.runtime_revision ?? 0;
1594
+ const expectedRevision = index.runtime_revision ?? 0;
1595
+ if (actualRevision !== expectedRevision)
1596
+ throw new AppError("run_revision_changed", "RUN changed during preparation", 1, { run_id: run.id, expected_revision: expectedRevision, actual_revision: actualRevision });
1597
+ index.runtime_revision = actualRevision + 1;
1598
+ const history = [...(previous.flag_revision_history ?? [])];
1599
+ if (options.flagRevision)
1600
+ history.push(options.flagRevision);
1601
+ index.flag_revision_history = history.slice(-32);
1602
+ const updated = options.healthOnly ? context.db.run("UPDATE runs SET status = ?, verdict = ?, next_action = ?, index_json = ?, updated_at = ?, completed_at = ? WHERE project_id = ? AND id = ? AND index_json = ?", [index.status, index.verdict, index.next_action, JSON.stringify(persistedVnextIndex(index)), index.updated_at, index.completed_at ?? null, project.id, run.id, stored.index_json]) : context.db.run(`UPDATE runs SET status = ?, verdict = ?, next_action = ?, index_json = ?, runtime_path = ?, run_index_path = ?, updated_at = ?, completed_at = ?
1603
+ WHERE project_id = ? AND id = ? AND index_json = ?`, [index.status, index.verdict, index.next_action, JSON.stringify(persistedVnextIndex(index)), runtimePath, runtimePath, index.updated_at, index.completed_at ?? null, project.id, run.id, stored.index_json]);
1604
+ if (updated.changes !== 1)
1605
+ throw new AppError("run_revision_changed", "RUN changed during preparation", 1, { run_id: run.id });
1606
+ context.db.afterCommit(() => publishRunProjections(context, project.id, run.id));
1607
+ }, "runs.persistRunState");
1133
1608
  }
1134
1609
  /** vNext stores derived stage/work state only once: stage_runs, Works and Sessions. */
1135
1610
  function persistedVnextIndex(index) {
@@ -1171,35 +1646,63 @@ function materializedRunVariables(index) {
1171
1646
  ...(index.variables ?? {})
1172
1647
  };
1173
1648
  }
1174
- function setRunVariable(context, input, value) {
1649
+ function prepareRunVariable(context, input) {
1175
1650
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
1176
1651
  const run = resolveRun(context, project.id, input.runId);
1652
+ assertRunMutationAllowed(context, project.id, run.id);
1177
1653
  const index = authoritativeIndex(run);
1654
+ return { project, run, index };
1655
+ }
1656
+ function setRunVariable(context, input, value, prepared = prepareRunVariable(context, input)) {
1657
+ const { project, run, index } = prepared;
1178
1658
  if (index.variables?.[input.key] === value)
1179
1659
  return { ok: true, run_id: run.id, key: input.key, value, idempotent: true };
1180
1660
  index.variables = { ...(index.variables ?? {}), [input.key]: value };
1181
1661
  index.updated_at = context.now();
1182
1662
  persistRunState(context, project, run, index);
1183
- appendRunTimeline(runArtifactRoot(run), { at: index.updated_at, type: "run_variable_set", run_id: run.id, key: input.key });
1663
+ appendRunTimeline(context, project.id, run.id, { at: index.updated_at, type: "run_variable_set", run_id: run.id, key: input.key });
1184
1664
  return { ok: true, run_id: run.id, key: input.key, value, idempotent: false };
1185
1665
  }
1186
- function appendRunTimeline(runHome, event) {
1187
- ensureDir(runHome);
1188
- const timelinePath = path.join(runHome, "timeline.jsonl");
1189
- let sequence = 0;
1190
- if (fs.existsSync(timelinePath)) {
1191
- const lines = fs.readFileSync(timelinePath, "utf8").trim().split(/\r?\n/).filter(Boolean);
1192
- const last = lines.length > 0 ? JSON.parse(lines[lines.length - 1]) : undefined;
1193
- sequence = typeof last?.sequence === "number" ? last.sequence : lines.length;
1194
- }
1195
- const safeEvent = sanitizeTimelineValue(event);
1196
- const enriched = {
1197
- event_id: crypto.randomUUID(),
1198
- sequence: sequence + 1,
1199
- at: typeof safeEvent.at === "string" ? safeEvent.at : new Date().toISOString(),
1200
- ...safeEvent
1201
- };
1202
- fs.appendFileSync(timelinePath, `${JSON.stringify(enriched)}\n`);
1666
+ function appendRunTimeline(context, projectId, runId, event) {
1667
+ context.db.writeTransaction(() => {
1668
+ appendTimelineAudit(context, projectId, runId, event);
1669
+ context.db.afterCommit(() => publishRunProjections(context, projectId, runId));
1670
+ }, "runs.appendRunTimeline");
1671
+ }
1672
+ /** Rebuild projections from committed SQL after taking a per-RUN filesystem lock. */
1673
+ export function publishRunProjections(context, projectId, runId) {
1674
+ const run = context.db.get("SELECT * FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
1675
+ if (!run)
1676
+ throw new AppError("not_found", "RUN is not registered", 1, { run_id: runId });
1677
+ const runtimePath = run.runtime_path || projectRunJsonPath(context.ddFlowHome, projectId, runId);
1678
+ ensureDir(path.dirname(runtimePath));
1679
+ withProjectionLock(`${runtimePath}.publish-lock`, () => {
1680
+ // A delayed publisher must observe the latest commit after acquiring its lock.
1681
+ const latest = context.db.get("SELECT * FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
1682
+ if (!latest)
1683
+ throw new AppError("not_found", "RUN is not registered", 1, { run_id: runId });
1684
+ const index = authoritativeIndex(latest);
1685
+ if (index.workspace_route) {
1686
+ const route = retainedWorkspaceRoute(context, latest);
1687
+ const file = path.join(runArtifactRoot(latest), "02-protocolize", "workspace-route.json");
1688
+ ensureDir(path.dirname(file));
1689
+ try {
1690
+ writeJsonFile(file, route.receipt);
1691
+ }
1692
+ catch (error) {
1693
+ throw new AppError("workspace_route_publication_failed", "Committed route projection could not be published", 1, { effect: "committed", run_id: runId, route_sha256: index.workspace_route.sha256, cause: String(error) });
1694
+ }
1695
+ }
1696
+ const mutations = context.db.get("SELECT 1 FROM sqlite_schema WHERE name = 'flow_run_flag_mutations'") ? context.db.all("SELECT resulting_revision, result_json, created_at, idempotency_key FROM flow_run_flag_mutations WHERE project_id = ? AND run_id = ? ORDER BY resulting_revision DESC LIMIT 32", [projectId, runId]) : [];
1697
+ const history = mutations.reverse().map((row) => ({
1698
+ revision: row.resulting_revision,
1699
+ checksum: String(JSON.parse(row.result_json).snapshot_checksum ?? ""),
1700
+ at: row.created_at,
1701
+ idempotency_key: row.idempotency_key
1702
+ }));
1703
+ writeJsonFile(runtimePath, runtimeSnapshotForIndex(index, index.runtime_revision ?? 1, history.length ? history : index.flag_revision_history ?? []));
1704
+ writeTimelineProjection(context, projectId, runId, latest.run_root);
1705
+ });
1203
1706
  }
1204
1707
  function runtimeSnapshotForIndex(index, runtimeRevision, flagRevisionHistory = []) {
1205
1708
  const rest = { ...index };
@@ -1225,25 +1728,191 @@ function readRuntimeSnapshot(file) {
1225
1728
  function authoritativeIndex(run) {
1226
1729
  try {
1227
1730
  const state = JSON.parse(run.index_json);
1228
- return { ...state, schema_id: runSchemaId };
1731
+ if (!state || typeof state !== "object" || Array.isArray(state) || !Array.isArray(state.stage_runs))
1732
+ throw new Error("RUN index must contain stage_runs");
1733
+ const derivedStage = ["vnext_specify", "vnext_protocolize"].includes(run.flow_kind)
1734
+ ? [...state.stage_runs].sort((left, right) => right.order - left.order)[0]?.stage : undefined;
1735
+ return { ...state, schema_id: runSchemaId, ...(state.current_stage === undefined && derivedStage ? { current_stage: derivedStage } : {}) };
1229
1736
  }
1230
1737
  catch (error) {
1231
- throw new AppError("validation", "RUN mechanical state in SQLite is missing or invalid", 2, { run_id: run.id, cause: String(error) });
1738
+ throw new AppError("runtime_state_invalid", "RUN mechanical state in SQLite is missing or invalid", 1, { run_id: run.id, cause: String(error) });
1232
1739
  }
1233
1740
  }
1234
- function sanitizeTimelineValue(value, key) {
1235
- if (key && /(secret|password|authorization|cookie|api[_-]?key|private[_-]?key|access[_-]?token|refresh[_-]?token|transcript|prompt|tool[_-]?output|document[_-]?content)/i.test(key)) {
1236
- return "[REDACTED]";
1741
+ function assertBundleWorkspaceIdentity(context, run, bundle) {
1742
+ const scope = currentBundleScope(context, run, bundle);
1743
+ const route = authoritativeIndex(run).workspace_route ? retainedWorkspaceRoute(context, run) : undefined;
1744
+ if (scope.workspace === run.workspace_root)
1745
+ return true;
1746
+ if (bundle.descriptor.descriptor.id !== "sdlc" || !["vnext_specify", "vnext_protocolize"].includes(run.flow_kind) || scope.workspace !== run.project_root)
1747
+ return false;
1748
+ if (!route)
1749
+ return false;
1750
+ return route.previous_workspace === scope.workspace && route.receipt.policy.route === "feature_worktree"
1751
+ && route.receipt.policy.base_ref !== null && route.receipt.policy.feature_branch !== null;
1752
+ }
1753
+ function currentBundleScope(context, run, bundle) {
1754
+ return JSON.parse(projectPacketLocators(JSON.stringify(bundle.execution.snapshot().scope), runContextRelocations(context, run.project_id, run.id)));
1755
+ }
1756
+ function assertBundleRunIdentity(context, run, bundle) {
1757
+ const scope = currentBundleScope(context, run, bundle);
1758
+ const flowId = bundle.descriptor.descriptor.id;
1759
+ const matchesFlow = flowId === "sdlc" ? ["vnext_specify", "vnext_protocolize"].includes(run.flow_kind) : flowId === run.flow_kind;
1760
+ if (bundle.manifest.run_id !== run.id || scope.projectRoot !== run.project_root || !assertBundleWorkspaceIdentity(context, run, bundle) || !matchesFlow) {
1761
+ throw new AppError("run_bundle_identity_invalid", "Execution bundle belongs to another RUN or workspace", 1, { phase: "prepare", effect: "no_effect", run_id: run.id });
1237
1762
  }
1238
- if (typeof value === "string") {
1239
- return value.length > 512 ? `${value.slice(0, 512)}…[TRUNCATED]` : value;
1763
+ const blobs = new Map(bundle.manifest.inventory.map(ref => [ref.path, bundle.blob(ref.path)]));
1764
+ // File-owned handler inventories can never authorize executable code.
1765
+ admitBundledFlow(bundle.blob(bundle.manifest.descriptor.path), bundle.manifest.descriptor.sha256, blobs);
1766
+ }
1767
+ function bundleAuthority(admission) {
1768
+ const authority = { runId: admission.runId, executionScope: structuredClone(admission.executionScope),
1769
+ receipts: structuredClone([...(admission.receipts ?? [])].sort(([left], [right]) => left.localeCompare(right, "en"))) };
1770
+ for (const key of ["descriptorSha256", "engineChecksum", "engineBindingSha256", "engineManifestSha256", "bundleProjectRoot", "bundleProvenance", "bootstrapIdentity"]) {
1771
+ if (admission[key] !== undefined)
1772
+ Object.assign(authority, { [key]: structuredClone(admission[key]) });
1240
1773
  }
1241
- if (Array.isArray(value))
1242
- return value.map((item) => sanitizeTimelineValue(item));
1243
- if (value && typeof value === "object") {
1244
- return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [entryKey, sanitizeTimelineValue(entryValue, entryKey)]));
1774
+ return authority;
1775
+ }
1776
+ /** Copies the same SQL authority representation used by legacy bundle binding. */
1777
+ export function runBundleAuthorityMetadata(bundle) {
1778
+ if (!(bundle instanceof RetainedExecutionBundle))
1779
+ throw new AppError("run_bundle_anchor_invalid", "Bundle must be admitted before RUN allocation", 1);
1780
+ const admissionJson = JSON.stringify(bundleAuthority(bundle.admission())), admissionSha256 = hybridDigest(JSON.parse(admissionJson));
1781
+ if (Buffer.byteLength(admissionJson) > 1024 * 1024)
1782
+ throw new AppError("run_bundle_authority_invalid", "RUN admission exceeds its bound", 1);
1783
+ return { admissionJson, anchor: { schema_id: "dd-flow/run-execution-bundle@1",
1784
+ manifest_sha256: bundle.sha256, descriptor_sha256: bundle.manifest.descriptor.sha256,
1785
+ admission_sha256: admissionSha256, flow_id: bundle.manifest.flow_id } };
1786
+ }
1787
+ /** Recheck SQL authority and compiled descriptor admission. Schema loading
1788
+ * belongs outside SQL writers; use the explicit SQL-only fence inside them.
1789
+ */
1790
+ export function assertPreparedRunBundleAuthority(context, scope, bundle) {
1791
+ const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
1792
+ assertBundleRunIdentity(context, run, bundle);
1793
+ }
1794
+ /** SQL-only fence after the caller has admitted compiled handlers outside the
1795
+ * writer. Descriptor schema loading is deliberately excluded from this fence.
1796
+ */
1797
+ export function assertPreparedRunBundleSqlAuthority(context, scope, bundle) {
1798
+ if (!(bundle instanceof RetainedExecutionBundle))
1799
+ throw new AppError("run_bundle_anchor_invalid", "Prepared bundle is not admitted", 1);
1800
+ const run = context.db.get("SELECT * FROM runs WHERE project_id = ? AND id = ?", [scope.projectId, scope.runId]);
1801
+ if (!run)
1802
+ throw new AppError("not_found", "Prepared bundle RUN is unavailable", 1);
1803
+ const index = authoritativeIndex(run), anchor = index.execution_bundle;
1804
+ if (!assertRunBundleSqlAuthority(context, scope, anchor) || !anchor || anchor.manifest_sha256 !== bundle.sha256
1805
+ || anchor.descriptor_sha256 !== bundle.manifest.descriptor.sha256 || anchor.flow_id !== bundle.manifest.flow_id
1806
+ || anchor.admission_sha256 !== hybridDigest(bundleAuthority(bundle.admission())))
1807
+ throw new AppError("run_bundle_anchor_invalid", "Prepared bundle differs from authoritative RUN", 1);
1808
+ const execution = currentBundleScope(context, run, bundle);
1809
+ const flow = bundle.descriptor.descriptor.id;
1810
+ if (bundle.manifest.run_id !== run.id || execution.projectRoot !== run.project_root || !assertBundleWorkspaceIdentity(context, run, bundle)
1811
+ || !(flow === "sdlc" ? ["vnext_specify", "vnext_protocolize"].includes(run.flow_kind) : flow === run.flow_kind)) {
1812
+ throw new AppError("run_bundle_identity_invalid", "Execution bundle belongs to another RUN or workspace", 1, { effect: "no_effect", run_id: run.id });
1245
1813
  }
1246
- return value;
1814
+ return run;
1815
+ }
1816
+ export function readFlowRunExecutionBundle(context, input) {
1817
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
1818
+ const run = resolveRun(context, project.id, input.runId), anchor = authoritativeIndex(run).execution_bundle;
1819
+ if (!anchor) {
1820
+ // Genuine historical RUNs have neither authority nor anchor. Lost new-RUN
1821
+ // metadata must not silently switch their execution to the legacy engine path.
1822
+ const hasAuthority = context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_bundle_authorities'");
1823
+ if (hasAuthority && context.db.get("SELECT 1 FROM run_bundle_authorities WHERE project_id = ? AND run_id = ?", [project.id, run.id])) {
1824
+ throw new AppError("run_bundle_anchor_invalid", "Retained RUN authority exists without its execution anchor", 1);
1825
+ }
1826
+ return null;
1827
+ }
1828
+ if (anchor.schema_id !== "dd-flow/run-execution-bundle@1" || !/^[a-f0-9]{64}$/.test(anchor.manifest_sha256)
1829
+ || !/^[a-f0-9]{64}$/.test(anchor.descriptor_sha256) || !/^[a-f0-9]{64}$/.test(anchor.admission_sha256))
1830
+ throw new AppError("run_bundle_anchor_invalid", "RUN bundle anchor is invalid", 1);
1831
+ const hasAuthority = context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_bundle_authorities'");
1832
+ const row = hasAuthority ? context.db.get("SELECT admission_json, manifest_sha256 FROM run_bundle_authorities WHERE project_id = ? AND run_id = ?", [project.id, run.id]) : undefined;
1833
+ if (!row || row.manifest_sha256 !== anchor.manifest_sha256)
1834
+ throw new AppError("run_bundle_authority_missing", "RUN bundle admission authority is unavailable", 1);
1835
+ if (Buffer.byteLength(row.admission_json) > 1024 * 1024)
1836
+ throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority exceeds its bound", 1);
1837
+ let retained;
1838
+ try {
1839
+ retained = JSON.parse(row.admission_json);
1840
+ }
1841
+ catch {
1842
+ throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority is invalid", 1);
1843
+ }
1844
+ if (hybridDigest(retained) !== anchor.admission_sha256 || !Array.isArray(retained.receipts)
1845
+ || input.admission && hybridDigest(bundleAuthority(input.admission)) !== anchor.admission_sha256) {
1846
+ throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority changed", 1);
1847
+ }
1848
+ const admission = { ...retained, receipts: new Map(retained.receipts), descriptorContext: bundledHandlerContext(new Map()) };
1849
+ const relocations = runContextRelocations(context, run.project_id, run.id);
1850
+ const binding = relocations.length ? readRunEngineBinding(path.join(runArtifactRoot(run), "engine-binding.json")) : null;
1851
+ const relocation = binding?.source === "fork_upgrade" ? { binding, relocations } : undefined;
1852
+ const bundle = RetainedExecutionBundle.read(path.join(runArtifactRoot(run), "execution-bundle"), anchor.manifest_sha256, admission, relocation);
1853
+ assertBundleRunIdentity(context, run, bundle);
1854
+ if (bundle.manifest.descriptor.sha256 !== anchor.descriptor_sha256 || bundle.manifest.flow_id !== anchor.flow_id) {
1855
+ throw new AppError("run_bundle_anchor_invalid", "RUN bundle descriptor identity changed", 1);
1856
+ }
1857
+ return bundle;
1858
+ }
1859
+ /** Freeze once before physical owners exist. Publication precedes the fenced SQLite anchor. */
1860
+ export function bindFlowRunExecutionBundle(context, input) {
1861
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
1862
+ const run = resolveRun(context, project.id, input.runId), index = authoritativeIndex(run);
1863
+ const generation = assertRunMutationAllowed(context, project.id, run.id);
1864
+ assertBundleRunIdentity(context, run, input.bundle);
1865
+ if (input.bundle.manifest.flow_id !== "sdlc") {
1866
+ throw new AppError("run_bundle_start_required", "Memory RUN and its bundle authority must be allocated together through the request-bound start", 1, { phase: "prepare", effect: "no_effect", run_id: run.id });
1867
+ }
1868
+ if (index.execution_bundle) {
1869
+ if (index.execution_bundle.manifest_sha256 !== input.bundle.sha256)
1870
+ throw new AppError("run_bundle_immutable", "RUN bundle cannot be replaced", 1);
1871
+ readFlowRunExecutionBundle(context, { ...input, admission: input.bundle.admission() });
1872
+ publishRunProjections(context, project.id, run.id);
1873
+ return { changed: false, sha256: input.bundle.sha256 };
1874
+ }
1875
+ const assertUnstarted = (state) => {
1876
+ if (state.status !== "running" || state.stage_runs.length || state.sessions.length
1877
+ || context.db.get("SELECT 1 FROM sessions WHERE project_id = ? AND run_id = ? LIMIT 1", [project.id, run.id])
1878
+ || context.db.get("SELECT 1 FROM works WHERE project_id = ? AND run_id = ? LIMIT 1", [project.id, run.id])) {
1879
+ throw new AppError("run_bundle_active", "Execution bundle must be frozen before stages, Works or Sessions exist", 1);
1880
+ }
1881
+ };
1882
+ assertUnstarted(index);
1883
+ const authority = bundleAuthority(input.bundle.admission());
1884
+ // Hash the JSON representation actually retained, not optional undefined fields.
1885
+ const authorityJson = JSON.stringify(authority), authoritySha256 = hybridDigest(JSON.parse(authorityJson));
1886
+ if (Buffer.byteLength(authorityJson) > 1024 * 1024)
1887
+ throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority exceeds its bound", 1);
1888
+ input.bundle.materialize(runArtifactRoot(run));
1889
+ try {
1890
+ context.db.writeTransaction(() => {
1891
+ assertRunMutationAllowed(context, project.id, run.id, generation);
1892
+ const latest = resolveRun(context, project.id, run.id), state = authoritativeIndex(latest);
1893
+ assertUnstarted(state);
1894
+ if (state.runtime_revision !== index.runtime_revision || state.execution_bundle || latest.workspace_root !== run.workspace_root
1895
+ || latest.project_root !== run.project_root || latest.flow_kind !== run.flow_kind || latest.run_root !== run.run_root) {
1896
+ throw new AppError("run_revision_changed", "RUN changed during bundle publication", 1);
1897
+ }
1898
+ state.execution_bundle = { schema_id: "dd-flow/run-execution-bundle@1", manifest_sha256: input.bundle.sha256,
1899
+ descriptor_sha256: input.bundle.manifest.descriptor.sha256, admission_sha256: authoritySha256, flow_id: input.bundle.manifest.flow_id };
1900
+ context.db.execSchema(`CREATE TABLE IF NOT EXISTS run_bundle_authorities (
1901
+ project_id TEXT NOT NULL, run_id TEXT NOT NULL, manifest_sha256 TEXT NOT NULL, admission_json TEXT NOT NULL,
1902
+ PRIMARY KEY(project_id, run_id), FOREIGN KEY(project_id, run_id) REFERENCES runs(project_id, id)
1903
+ )`);
1904
+ context.db.run("INSERT INTO run_bundle_authorities (project_id, run_id, manifest_sha256, admission_json) VALUES (?, ?, ?, ?)", [project.id, run.id, input.bundle.sha256, authorityJson]);
1905
+ state.updated_at = context.now();
1906
+ persistRunState(context, project, latest, state);
1907
+ }, "runs.bindExecutionBundle");
1908
+ }
1909
+ catch (error) {
1910
+ throw new AppError("run_bundle_binding_incomplete", "Bundle retained but RUN binding requires recovery", 1, {
1911
+ phase: "publication", effect: "partial", run_id: run.id, sha256: input.bundle.sha256,
1912
+ reason: error instanceof AppError ? error.code : "binding_io_failure",
1913
+ });
1914
+ }
1915
+ return { changed: true, sha256: input.bundle.sha256 };
1247
1916
  }
1248
1917
  function durationMs(startedAt, completedAt) {
1249
1918
  if (!startedAt || !completedAt)
@@ -1251,12 +1920,30 @@ function durationMs(startedAt, completedAt) {
1251
1920
  const value = Date.parse(completedAt) - Date.parse(startedAt);
1252
1921
  return Number.isFinite(value) && value >= 0 ? value : null;
1253
1922
  }
1254
- function nextAttempt(existing) {
1923
+ export function nextAttempt(existing) {
1255
1924
  if (!existing?.attempt)
1256
1925
  return "try-001";
1257
1926
  const number = Number(existing.attempt.replace("try-", ""));
1258
1927
  return `try-${String(Number.isFinite(number) ? number + 1 : 1).padStart(3, "0")}`;
1259
1928
  }
1929
+ function bundledNextStage(flow, index) {
1930
+ const completed = index.stage_runs.filter(stage => ["done", "skipped"].includes(stage.status)).sort((left, right) => right.order - left.order)[0];
1931
+ if (!completed)
1932
+ return flow.descriptor.entry_stage;
1933
+ // Optional SDLC review routing remains a flow-specific policy, not a generic stage list.
1934
+ const outcome = flow.descriptor.id === "sdlc" && completed.stage === "plan" && index.settings?.plan_review?.mode === "off" ? "not-applicable" : "accepted";
1935
+ return flow.successor(completed.stage, outcome);
1936
+ }
1937
+ function assertBundledStageStart(flow, index, stage, dir, status, existing) {
1938
+ const selected = flow.stage(stage);
1939
+ if (selected.directory !== dir)
1940
+ throw new AppError("flow_stage_directory_invalid", "Stage directory differs from the frozen descriptor", 1, { stage });
1941
+ if (status !== "running")
1942
+ return;
1943
+ if (existing || index.stage_runs.some(candidate => !["done", "skipped"].includes(candidate.status)) || bundledNextStage(flow, index) !== stage) {
1944
+ throw new AppError("illegal_stage_transition", "Stage does not follow the frozen descriptor or an attempt already exists", 2, { run_id: index.run_id, stage });
1945
+ }
1946
+ }
1260
1947
  function assertVnextStageStart(run, index, stage, status, existing) {
1261
1948
  if (status !== "running" || (run.flow_kind !== "vnext_specify" && run.flow_kind !== "vnext_protocolize"))
1262
1949
  return;
@@ -1315,19 +2002,6 @@ function requireRunById(context, projectId, runId) {
1315
2002
  }
1316
2003
  return run;
1317
2004
  }
1318
- function nextRunId(context, projectId, slug) {
1319
- const rows = context.db.all("SELECT id FROM runs WHERE project_id = ? AND id LIKE 'RUN-%'", [projectId]);
1320
- const max = rows.reduce((value, row) => {
1321
- try {
1322
- const parsed = parseFullEntityId(row.id);
1323
- return parsed.type === runIdType ? Math.max(value, Number(parsed.shortId.slice(4))) : value;
1324
- }
1325
- catch {
1326
- return value;
1327
- }
1328
- }, 0);
1329
- return formatFullId(runIdType, max + 1, slug);
1330
- }
1331
2005
  function flowRunSummary(run) {
1332
2006
  return {
1333
2007
  id: run.id,
@@ -1383,7 +2057,7 @@ function parseStageStatus(value) {
1383
2057
  }
1384
2058
  return value;
1385
2059
  }
1386
- function requiredStageDir(value) {
2060
+ export function prepareStageDirectory(value) {
1387
2061
  const dir = requiredPlain(value, "dir");
1388
2062
  if (!/^\d{2}-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(dir)) {
1389
2063
  throw new AppError("validation", "--dir must use NN-stage-slug format", 2, { dir });
@@ -1391,6 +2065,8 @@ function requiredStageDir(value) {
1391
2065
  return dir;
1392
2066
  }
1393
2067
  function requiredPlain(value, label) {
2068
+ if (typeof value !== "string")
2069
+ throw new AppError("validation", `${label} must be a string`, 2);
1394
2070
  const trimmed = value.trim();
1395
2071
  if (!trimmed) {
1396
2072
  throw new AppError("validation", `${label} must not be empty`, 2);
@@ -1398,6 +2074,8 @@ function requiredPlain(value, label) {
1398
2074
  return trimmed;
1399
2075
  }
1400
2076
  function normalizeSlug(value) {
2077
+ if (typeof value !== "string")
2078
+ throw new AppError("validation", "--slug must be a string", 2);
1401
2079
  const slug = value
1402
2080
  .normalize("NFKD")
1403
2081
  .replace(/[\u0300-\u036f]/g, "")
@@ -1415,39 +2093,84 @@ function resolveWorkspaceRoot(workspaceRoot) {
1415
2093
  if (!fs.existsSync(absolute)) {
1416
2094
  throw new AppError("not_found", `Workspace root does not exist: ${absolute}`, 1);
1417
2095
  }
2096
+ if (!fs.statSync(absolute).isDirectory())
2097
+ throw new AppError("validation", "Workspace root must be a directory", 2, { workspace_root: absolute });
1418
2098
  return fs.realpathSync(absolute);
1419
2099
  }
1420
2100
  /** Move one completed stage attempt and update only known durable path columns.
1421
2101
  * Receipt bytes remain immutable; readers resolve their new location through the DB. */
1422
- function archiveExistingStageAttempt(context, projectId, runId, runRoot, stageDir) {
2102
+ export function archiveExistingStageAttempt(context, projectId, runId, runRoot, stageDir) {
1423
2103
  const currentStageDir = path.join(runRoot, stageDir);
1424
2104
  if (!fs.existsSync(currentStageDir) || !fs.statSync(currentStageDir).isDirectory()) {
1425
2105
  return;
1426
2106
  }
1427
- const entries = fs.readdirSync(currentStageDir).filter((entry) => !/^try-\d{3}$/.test(entry));
1428
- if (entries.length === 0) {
2107
+ const journal = path.join(runRoot, `.archive-${stageDir}.json`);
2108
+ const pending = fs.existsSync(journal) ? JSON.parse(fs.readFileSync(journal, "utf8")) : null;
2109
+ const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
2110
+ const prior = row ? JSON.parse(row.index_json) : null;
2111
+ const live = prior?.stage_runs.find((stage) => stage.dir === stageDir);
2112
+ const generation = row ? assertRunMutationAllowed(context, projectId, runId) : 0;
2113
+ if (pending && (pending.project_id && pending.project_id !== projectId || pending.run_id && pending.run_id !== runId))
2114
+ throw new AppError("archive_owner_changed", "Archive journal belongs to another RUN", 1, { journal });
2115
+ if (pending?.generation !== undefined && pending.generation !== generation)
2116
+ throw new AppError("archive_owner_changed", "RUN generation changed while an archive was pending", 1, { journal, expected_generation: pending.generation, generation });
2117
+ if (pending?.prior_attempt && live?.attempt !== pending.prior_attempt && live?.attempt !== pending.next_attempt)
2118
+ throw new AppError("archive_owner_changed", "Stage attempt changed while an archive was pending", 1, { journal, prior_attempt: pending.prior_attempt, actual_attempt: live?.attempt ?? null });
2119
+ const entries = pending?.entries ?? fs.readdirSync(currentStageDir).filter((entry) => !/^try-\d{3}$/.test(entry));
2120
+ if (entries.length === 0 && !pending) {
1429
2121
  return;
1430
2122
  }
1431
2123
  let attempt = 1;
1432
2124
  while (fs.existsSync(path.join(currentStageDir, `try-${String(attempt).padStart(3, "0")}`))) {
1433
2125
  attempt += 1;
1434
2126
  }
1435
- const archiveDir = path.join(currentStageDir, `try-${String(attempt).padStart(3, "0")}`);
2127
+ const archiveDir = pending?.archiveDir ?? path.join(currentStageDir, `try-${String(attempt).padStart(3, "0")}`);
2128
+ const intent = pending ?? { archiveDir, entries, stage_run: live ?? null, project_id: projectId, run_id: runId, prior_attempt: live?.attempt ?? null, next_attempt: nextAttempt(live), runtime_revision: prior?.runtime_revision ?? 0, generation };
2129
+ if (!pending) {
2130
+ writeJsonFile(journal, intent);
2131
+ }
1436
2132
  ensureDir(archiveDir);
1437
2133
  for (const entry of entries) {
1438
- fs.renameSync(path.join(currentStageDir, entry), path.join(archiveDir, entry));
2134
+ const source = path.join(currentStageDir, entry);
2135
+ const target = path.join(archiveDir, entry);
2136
+ if (fs.existsSync(source)) {
2137
+ if (fs.existsSync(target))
2138
+ throw new AppError("archive_conflict", "Both live and archived artifact exist; preserve both for recovery", 2, { source, target });
2139
+ fs.renameSync(source, target);
2140
+ }
2141
+ else if (!fs.existsSync(target))
2142
+ throw new AppError("archive_artifact_missing", "Artifact is absent from both archive locations", 2, { source, target });
1439
2143
  }
1440
- const oldPrefix = `${currentStageDir}${path.sep}`;
1441
- const newPrefix = `${archiveDir}${path.sep}`;
1442
- const rewrite = (table, column, where, values) => {
1443
- context.db.run(`UPDATE ${table} SET ${column} = ? || substr(${column}, ?) WHERE ${column} LIKE ? AND ${where}`, [newPrefix, oldPrefix.length + 1, `${oldPrefix}%`, ...values]);
1444
- };
1445
- const workScope = "work_id IN (SELECT work_id FROM works WHERE project_id = ? AND run_id = ?)";
1446
- rewrite("work_sessions", "prompt_path", workScope, [projectId, runId]);
1447
- rewrite("work_sessions", "result_path", workScope, [projectId, runId]);
1448
- const receiptScope = "project_id = ? AND run_id = ?";
1449
- for (const column of ["stdout_path", "stderr_path", "receipt_path"])
1450
- rewrite("check_receipts", column, receiptScope, [projectId, runId]);
2144
+ context.db.writeTransaction(() => {
2145
+ const current = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
2146
+ const currentIndex = current ? JSON.parse(current.index_json) : null;
2147
+ const currentAttempt = currentIndex?.stage_runs.find((stage) => stage.dir === stageDir)?.attempt;
2148
+ if (currentIndex && intent.runtime_revision !== undefined && (currentIndex.runtime_revision ?? 0) !== intent.runtime_revision && currentAttempt !== intent.next_attempt)
2149
+ throw new AppError("archive_owner_changed", "RUN revision changed while an archive was pending", 1, { journal });
2150
+ if (intent.prior_attempt && currentAttempt !== intent.prior_attempt && currentAttempt !== intent.next_attempt)
2151
+ throw new AppError("archive_owner_changed", "Stage attempt changed while an archive was pending", 1, { journal });
2152
+ if (row && generation !== assertRunMutationAllowed(context, projectId, runId))
2153
+ throw new AppError("archive_owner_changed", "RUN generation changed while an archive was pending", 1, { journal });
2154
+ if (intent.next_attempt === undefined || currentAttempt !== intent.next_attempt) {
2155
+ const oldPrefix = `${currentStageDir}${path.sep}`;
2156
+ const newPrefix = `${archiveDir}${path.sep}`;
2157
+ const rewrite = (table, column, where, values) => {
2158
+ context.db.run(`UPDATE ${table} SET ${column} = ? || substr(${column}, ?) WHERE ${column} LIKE ? AND substr(${column}, ?, 4) != 'try-' AND ${where}`, [newPrefix, oldPrefix.length + 1, `${oldPrefix}%`, oldPrefix.length + 1, ...values]);
2159
+ };
2160
+ const workScope = "work_id IN (SELECT work_id FROM works WHERE project_id = ? AND run_id = ?)";
2161
+ rewrite("work_sessions", "prompt_path", workScope, [projectId, runId]);
2162
+ rewrite("work_sessions", "result_path", workScope, [projectId, runId]);
2163
+ const receiptScope = "project_id = ? AND run_id = ?";
2164
+ for (const column of ["stdout_path", "stderr_path", "receipt_path"])
2165
+ rewrite("check_receipts", column, receiptScope, [projectId, runId]);
2166
+ }
2167
+ context.db.afterCommit(() => {
2168
+ if (!fs.existsSync(journal))
2169
+ return;
2170
+ // Retain original Stage facts with the immutable attempt after SQL paths commit.
2171
+ fs.renameSync(journal, path.join(archiveDir, "archive.json"));
2172
+ });
2173
+ });
1451
2174
  }
1452
2175
  function writeJsonFile(file, value) {
1453
2176
  const tmpFile = `${file}.tmp-${process.pid}-${Date.now()}`;
@@ -1472,3 +2195,5 @@ export function gitFacts(workspaceRoot) {
1472
2195
  status: status === null ? "unavailable" : status.length > 0 ? "dirty" : "clean"
1473
2196
  };
1474
2197
  }
2198
+ import { assertRunMutationAllowed, assertStageSettlementAllowed, recoveryGuard } from "./run-recovery.js";
2199
+ import { runControlDirective } from "./run-control.js";