@deksden-com/dd-flow-cli 0.9.0-beta.15 → 0.9.0-beta.150

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 (557) hide show
  1. package/CHANGELOG.md +895 -0
  2. package/README.md +140 -6
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/argument-values.js +38 -0
  5. package/dist/cli/command-inputs.js +386 -0
  6. package/dist/cli/command-metadata.js +232 -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/diagnostic-invocation.js +46 -0
  11. package/dist/cli/diagnostic.js +120 -0
  12. package/dist/cli/error-output.js +52 -0
  13. package/dist/cli/executor-onboarding.js +76 -0
  14. package/dist/cli/flows.js +29 -0
  15. package/dist/cli/harnesses.js +84 -0
  16. package/dist/cli/help.js +396 -19
  17. package/dist/cli/hook-ingress.js +102 -0
  18. package/dist/cli/input-preparation.js +165 -0
  19. package/dist/cli/memory-flow-admission.js +50 -0
  20. package/dist/cli/memory-init-invocation.js +85 -0
  21. package/dist/cli/memory-init.js +249 -0
  22. package/dist/cli/memory-storage-admission.js +54 -0
  23. package/dist/cli/memory-storage-qualification.js +105 -0
  24. package/dist/cli/memory-storage-transition.js +123 -0
  25. package/dist/cli/memory-upgrade.js +189 -0
  26. package/dist/cli/models.js +39 -0
  27. package/dist/cli/native-hook-ingress.js +90 -0
  28. package/dist/cli/native-hook-preparation.js +55 -0
  29. package/dist/cli/operator-profiles.js +61 -0
  30. package/dist/cli/output-options.js +35 -0
  31. package/dist/cli/response-file.js +72 -0
  32. package/dist/cli/run-cli.js +1734 -550
  33. package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
  34. package/dist/cli/runtime-maintenance-ingress.js +52 -0
  35. package/dist/cli/runtime-process.js +153 -0
  36. package/dist/cli/setup.js +150 -0
  37. package/dist/cli.js +89 -2
  38. package/dist/domain/diagnostic-aspects.js +103 -0
  39. package/dist/domain/diagnostic-flows.js +8 -0
  40. package/dist/domain/stage-catalog.js +1 -1
  41. package/dist/domain/stage-handler-identities.js +28 -0
  42. package/dist/harness-runtime/DD-ZCODE.md +112 -0
  43. package/dist/harness-runtime/OPENCODE-LICENSE.txt +24 -0
  44. package/dist/harness-runtime/bin/dd-agy.mjs +56 -0
  45. package/dist/harness-runtime/bin/dd-codex.mjs +27 -0
  46. package/dist/harness-runtime/bin/dd-droid.mjs +34 -0
  47. package/dist/harness-runtime/bin/dd-grok.mjs +40 -0
  48. package/dist/harness-runtime/bin/dd-opencode.mjs +22 -0
  49. package/dist/harness-runtime/bin/dd-zcode.mjs +87 -0
  50. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  51. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  52. package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
  53. package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
  54. package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
  55. package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
  56. package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
  57. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  58. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  59. package/dist/harness-runtime/lib/daemon-operations.d.mts +2 -0
  60. package/dist/harness-runtime/lib/daemon-operations.mjs +462 -0
  61. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
  62. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +1096 -0
  63. package/dist/harness-runtime/lib/dd-agy.mjs +86 -0
  64. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  65. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +231 -0
  66. package/dist/harness-runtime/lib/dd-codex.d.mts +1 -0
  67. package/dist/harness-runtime/lib/dd-codex.mjs +954 -0
  68. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +141 -0
  69. package/dist/harness-runtime/lib/dd-droid.mjs +626 -0
  70. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +555 -0
  71. package/dist/harness-runtime/lib/dd-grok.mjs +180 -0
  72. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +622 -0
  73. package/dist/harness-runtime/lib/dd-opencode.mjs +390 -0
  74. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +752 -0
  75. package/dist/harness-runtime/lib/dd-zcode.mjs +1175 -0
  76. package/dist/harness-runtime/lib/delegation-instructions.d.mts +14 -0
  77. package/dist/harness-runtime/lib/delegation-instructions.mjs +157 -0
  78. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  79. package/dist/harness-runtime/lib/driver-recovery.mjs +140 -0
  80. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  81. package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
  82. package/dist/harness-runtime/lib/entity-references.mjs +9 -0
  83. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  84. package/dist/harness-runtime/lib/lease-renewal.mjs +37 -0
  85. package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
  86. package/dist/harness-runtime/lib/lifecycle-command.mjs +273 -0
  87. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
  88. package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
  89. package/dist/harness-runtime/lib/managed-daemon.d.mts +16 -0
  90. package/dist/harness-runtime/lib/managed-daemon.mjs +698 -0
  91. package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
  92. package/dist/harness-runtime/lib/model-observations.mjs +101 -0
  93. package/dist/harness-runtime/lib/native-children.d.mts +23 -0
  94. package/dist/harness-runtime/lib/native-children.mjs +219 -0
  95. package/dist/harness-runtime/lib/native-hook-command.d.mts +7 -0
  96. package/dist/harness-runtime/lib/native-hook-command.mjs +251 -0
  97. package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
  98. package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
  99. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  100. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  101. package/dist/harness-runtime/lib/observation-clock.mjs +95 -0
  102. package/dist/harness-runtime/lib/opencode-plugin.mjs +85 -0
  103. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  104. package/dist/harness-runtime/lib/operation-errors.d.mts +16 -0
  105. package/dist/harness-runtime/lib/operation-errors.mjs +127 -0
  106. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  107. package/dist/harness-runtime/lib/operation-progress.mjs +114 -0
  108. package/dist/harness-runtime/lib/process-json.d.mts +5 -0
  109. package/dist/harness-runtime/lib/process-json.mjs +201 -0
  110. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  111. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +15 -0
  112. package/dist/harness-runtime/lib/provider-overload-policy.mjs +151 -0
  113. package/dist/harness-runtime/lib/runner-events.mjs +202 -0
  114. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  115. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  116. package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
  117. package/dist/harness-runtime/lib/setup-progress.mjs +53 -0
  118. package/dist/harness-runtime/lib/tool-observations.d.mts +21 -0
  119. package/dist/harness-runtime/lib/tool-observations.mjs +228 -0
  120. package/dist/harness-runtime/lib/zcode-usage.mjs +55 -0
  121. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-1y4a697q.js +81 -0
  122. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-2ny4vxct.js +92 -0
  123. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-6mptkzbc.js +0 -0
  124. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-7x67cf1q.js +52 -0
  125. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9rqn6x4v.js +16 -0
  126. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9vdz0zwn.js +1661 -0
  127. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ecs5ftka.js +1466 -0
  128. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-frbwqjmf.js +145 -0
  129. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gne7h7mq.js +50 -0
  130. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gx9wnkzr.js +105 -0
  131. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-m2eywc8w.js +0 -0
  132. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-nwcpakzx.js +12 -0
  133. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-q4g3afnw.js +623 -0
  134. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-qav6mfdz.js +0 -0
  135. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-sxt88q69.js +12 -0
  136. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-t1xa31vm.js +39 -0
  137. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ve4aws36.js +103 -0
  138. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vmytr4q8.js +4 -0
  139. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vt9jbch8.js +36 -0
  140. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vyas3vgt.js +0 -0
  141. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-wjyeq25y.js +30 -0
  142. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-xgnj2w4v.js +20 -0
  143. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-y2etg5bs.js +9 -0
  144. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ydhz1zrh.js +238 -0
  145. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-z6qdwcsf.js +62 -0
  146. package/dist/harness-runtime/vendor/opencode-client/contract.d.ts +1 -0
  147. package/dist/harness-runtime/vendor/opencode-client/contract.js +12 -0
  148. package/dist/harness-runtime/vendor/opencode-client/effect/api/api.d.ts +2806 -0
  149. package/dist/harness-runtime/vendor/opencode-client/effect/api/api.js +0 -0
  150. package/dist/harness-runtime/vendor/opencode-client/effect/api.d.ts +4 -0
  151. package/dist/harness-runtime/vendor/opencode-client/effect/api.js +0 -0
  152. package/dist/harness-runtime/vendor/opencode-client/effect/client.d.ts +2530 -0
  153. package/dist/harness-runtime/vendor/opencode-client/effect/client.js +15 -0
  154. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.d.ts +7 -0
  155. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.js +6 -0
  156. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.d.ts +2529 -0
  157. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.js +8 -0
  158. package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.d.ts +2 -0
  159. package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.js +12 -0
  160. package/dist/harness-runtime/vendor/opencode-client/effect/index.d.ts +35 -0
  161. package/dist/harness-runtime/vendor/opencode-client/effect/index.js +75 -0
  162. package/dist/harness-runtime/vendor/opencode-client/effect/rpc.d.ts +20 -0
  163. package/dist/harness-runtime/vendor/opencode-client/effect/rpc.js +11 -0
  164. package/dist/harness-runtime/vendor/opencode-client/effect/service.d.ts +90 -0
  165. package/dist/harness-runtime/vendor/opencode-client/effect/service.js +187 -0
  166. package/dist/harness-runtime/vendor/opencode-client/package.json +84 -0
  167. package/dist/harness-runtime/vendor/opencode-client/promise/api.d.ts +21 -0
  168. package/dist/harness-runtime/vendor/opencode-client/promise/api.js +0 -0
  169. package/dist/harness-runtime/vendor/opencode-client/promise/client.d.ts +252 -0
  170. package/dist/harness-runtime/vendor/opencode-client/promise/client.js +14 -0
  171. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.d.ts +8 -0
  172. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.js +6 -0
  173. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.d.ts +259 -0
  174. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.js +7 -0
  175. package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.d.ts +3 -0
  176. package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.js +79 -0
  177. package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.d.ts +8932 -0
  178. package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.js +70 -0
  179. package/dist/harness-runtime/vendor/opencode-client/promise/index.d.ts +6 -0
  180. package/dist/harness-runtime/vendor/opencode-client/promise/index.js +83 -0
  181. package/dist/harness-runtime/vendor/opencode-client/promise/rpc.d.ts +32 -0
  182. package/dist/harness-runtime/vendor/opencode-client/promise/rpc.js +7 -0
  183. package/dist/harness-runtime/vendor/opencode-client/promise/service.d.ts +24 -0
  184. package/dist/harness-runtime/vendor/opencode-client/promise/service.js +187 -0
  185. package/dist/harness-runtime/vendor/opencode-client/pty-handoff.d.ts +12 -0
  186. package/dist/harness-runtime/vendor/opencode-client/pty-handoff.js +14 -0
  187. package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.d.ts +20 -0
  188. package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.js +14 -0
  189. package/dist/harness-runtime/vendor/opencode-client/service-contender.d.ts +37 -0
  190. package/dist/harness-runtime/vendor/opencode-client/service-contender.js +10 -0
  191. package/dist/harness-runtime/vendor/opencode-client/service-probe.d.ts +70 -0
  192. package/dist/harness-runtime/vendor/opencode-client/service-probe.js +16 -0
  193. package/dist/harness-runtime/vendor/opencode-client/service-timing.d.ts +12 -0
  194. package/dist/harness-runtime/vendor/opencode-client/service-timing.js +10 -0
  195. package/dist/harness-runtime/vendor/opencode-client/service-version.d.ts +2 -0
  196. package/dist/harness-runtime/vendor/opencode-client/service-version.js +6 -0
  197. package/dist/harness-runtime/vendor/opencode-client/service.d.ts +52 -0
  198. package/dist/harness-runtime/vendor/opencode-client/service.js +1 -0
  199. package/dist/harness-runtime/vendor/opencode-client/shared-events.d.ts +11 -0
  200. package/dist/harness-runtime/vendor/opencode-client/shared-events.js +8 -0
  201. package/dist/harness-runtime/vendor/opencode-client/solid/connection.d.ts +37 -0
  202. package/dist/harness-runtime/vendor/opencode-client/solid/connection.js +10 -0
  203. package/dist/harness-runtime/vendor/opencode-client/solid/data.d.ts +211 -0
  204. package/dist/harness-runtime/vendor/opencode-client/solid/data.js +18 -0
  205. package/dist/harness-runtime/vendor/opencode-client/solid/index.d.ts +3 -0
  206. package/dist/harness-runtime/vendor/opencode-client/solid/index.js +32 -0
  207. package/dist/harness-runtime/vendor/opencode-client/solid/pty.d.ts +22 -0
  208. package/dist/harness-runtime/vendor/opencode-client/solid/pty.js +8 -0
  209. package/dist/harness-runtime/vendor/opencode-plugin.mjs +3 -0
  210. package/dist/protocol/local-files.js +18 -9
  211. package/dist/runtime/context.js +8 -4
  212. package/dist/schemas/agent-profile.schema.json +1 -1
  213. package/dist/schemas/code-review-decision.schema.json +9 -0
  214. package/dist/schemas/code-review-result.schema.json +1 -1
  215. package/dist/schemas/code-verification.schema.json +7 -5
  216. package/dist/schemas/code-work-batch.schema.json +2 -2
  217. package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
  218. package/dist/schemas/diagnostic-work-result.schema.json +296 -0
  219. package/dist/schemas/discovery-result.schema.json +184 -0
  220. package/dist/schemas/execution-bundle.schema.json +211 -0
  221. package/dist/schemas/execution-settings.schema.json +162 -0
  222. package/dist/schemas/flow-descriptor.schema.json +54 -0
  223. package/dist/schemas/flow-run.schema.json +29 -0
  224. package/dist/schemas/harness-catalog.schema.json +80 -0
  225. package/dist/schemas/mb-init-compose-result.schema.json +457 -0
  226. package/dist/schemas/mb-init-discover-result.schema.json +500 -0
  227. package/dist/schemas/mb-init-finish-result.schema.json +422 -0
  228. package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
  229. package/dist/schemas/mb-init-review-result.schema.json +396 -0
  230. package/dist/schemas/mb-init-verify-result.schema.json +391 -0
  231. package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
  232. package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
  233. package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
  234. package/dist/schemas/memory-composition-work-result.schema.json +38 -0
  235. package/dist/schemas/memory-repair-work-result.schema.json +56 -0
  236. package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
  237. package/dist/schemas/memory-review-work-result.schema.json +43 -0
  238. package/dist/schemas/model-catalog.schema.json +153 -0
  239. package/dist/schemas/plan-review-decision.schema.json +12 -3
  240. package/dist/schemas/plan-review-result.schema.json +2 -2
  241. package/dist/schemas/profile-settings.schema.json +122 -0
  242. package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
  243. package/dist/schemas/release-impact.schema.json +3 -3
  244. package/dist/schemas/release-manifest.schema.json +291 -0
  245. package/dist/schemas/run-control-receipt.schema.json +118 -0
  246. package/dist/schemas/run-control-request.schema.json +36 -0
  247. package/dist/schemas/run-engine-binding.schema.json +1 -1
  248. package/dist/schemas/stage-start-response.schema.json +4 -0
  249. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  250. package/dist/services/accepted-intent.js +139 -0
  251. package/dist/services/branch-context.js +3 -3
  252. package/dist/services/canon.js +54 -15
  253. package/dist/services/check-environment.js +21 -0
  254. package/dist/services/check-runner-evidence.js +562 -0
  255. package/dist/services/cleanup.js +133 -40
  256. package/dist/services/cli-operation-classifier.js +26 -63
  257. package/dist/services/cli-upgrade-allowlist.js +27 -0
  258. package/dist/services/code-checks.js +1398 -119
  259. package/dist/services/codex-hook-delivery.js +32 -0
  260. package/dist/services/command-context.js +80 -0
  261. package/dist/services/compatibility-preflight.js +16 -3
  262. package/dist/services/config.js +11 -8
  263. package/dist/services/continuation-outcome.js +20 -0
  264. package/dist/services/controller-child-capacity.js +13 -0
  265. package/dist/services/controller-child-observer.js +115 -0
  266. package/dist/services/controller-execution.js +94 -0
  267. package/dist/services/controller-fanout.js +226 -0
  268. package/dist/services/controller-memory-task.js +45 -0
  269. package/dist/services/controller-memory-worker.js +49 -0
  270. package/dist/services/dashboard.js +143 -58
  271. package/dist/services/descriptor-coordinator-session.js +35 -0
  272. package/dist/services/descriptor-root-work.js +53 -0
  273. package/dist/services/descriptor-run-settlement.js +107 -0
  274. package/dist/services/descriptor-run-start.js +475 -0
  275. package/dist/services/descriptor-stage-coordinator.js +250 -0
  276. package/dist/services/descriptor-stage-settlement.js +223 -0
  277. package/dist/services/descriptor-stage-start.js +175 -0
  278. package/dist/services/diagnostic-evidence.js +73 -0
  279. package/dist/services/diagnostic-report-publication.js +72 -0
  280. package/dist/services/diagnostic-report.js +67 -0
  281. package/dist/services/diagnostic-run-bundle.js +74 -0
  282. package/dist/services/diagnostic-snapshot.js +159 -0
  283. package/dist/services/diagnostic-stage-handlers.js +108 -0
  284. package/dist/services/diagnostic-stage-results.js +132 -0
  285. package/dist/services/diagnostic-start.js +84 -0
  286. package/dist/services/diagnostic-work-batch.js +34 -0
  287. package/dist/services/diagnostic-work-collection.js +97 -0
  288. package/dist/services/diagnostic-work-registration.js +101 -0
  289. package/dist/services/diagnostic-work-results.js +93 -0
  290. package/dist/services/discovery/agy-discovery.js +2 -0
  291. package/dist/services/discovery/codex-discovery.js +2 -0
  292. package/dist/services/discovery/droid-discovery.js +2 -0
  293. package/dist/services/discovery/grok-discovery.js +2 -0
  294. package/dist/services/discovery/opencode-discovery.js +2 -0
  295. package/dist/services/discovery/static.js +31 -0
  296. package/dist/services/discovery/zcode-discovery.js +2 -0
  297. package/dist/services/engines.js +354 -163
  298. package/dist/services/eval-snapshots.js +1316 -150
  299. package/dist/services/execution-bundles.js +275 -0
  300. package/dist/services/execution-policy.js +185 -0
  301. package/dist/services/executor-onboarding.js +56 -0
  302. package/dist/services/external-work-launch.js +310 -0
  303. package/dist/services/flow-catalog.js +30 -0
  304. package/dist/services/flow-descriptors.js +82 -0
  305. package/dist/services/frozen-engine.js +387 -0
  306. package/dist/services/generic-execution.js +164 -0
  307. package/dist/services/harness-adapter.js +355 -36
  308. package/dist/services/harness-config.js +2 -0
  309. package/dist/services/harness-discovery.js +29 -0
  310. package/dist/services/harness-doctor.js +75 -0
  311. package/dist/services/harness-probe-effects.js +51 -0
  312. package/dist/services/harness-version-probe.js +100 -0
  313. package/dist/services/hook-memory-routing.js +188 -0
  314. package/dist/services/hook-recovery-guard.js +14 -0
  315. package/dist/services/hook-session-observation.js +24 -0
  316. package/dist/services/hooks.js +791 -326
  317. package/dist/services/hybrid-contracts.js +474 -0
  318. package/dist/services/hybrid-digest.js +14 -0
  319. package/dist/services/ids.js +29 -1
  320. package/dist/services/installation-lock.js +95 -0
  321. package/dist/services/installed-flow-source.js +226 -0
  322. package/dist/services/lanes.js +78 -65
  323. package/dist/services/lifecycle-command.js +1 -288
  324. package/dist/services/lifecycle-contract.js +1 -0
  325. package/dist/services/lifecycle-invocations.js +2148 -0
  326. package/dist/services/managed-daemon-binding.js +38 -0
  327. package/dist/services/managed-lease-monitor.js +133 -0
  328. package/dist/services/managed-processes.js +449 -69
  329. package/dist/services/memory-accepted-composition.js +78 -0
  330. package/dist/services/memory-baseline-preservation.js +65 -0
  331. package/dist/services/memory-completed-correction-wave.js +328 -0
  332. package/dist/services/memory-compose-handler.js +400 -0
  333. package/dist/services/memory-composition-acceptance.js +375 -0
  334. package/dist/services/memory-composition-graph.js +165 -0
  335. package/dist/services/memory-composition-registration.js +161 -0
  336. package/dist/services/memory-composition-work-custody.js +156 -0
  337. package/dist/services/memory-composition-work-results.js +108 -0
  338. package/dist/services/memory-corrected-review-acceptance.js +129 -0
  339. package/dist/services/memory-corrected-review-batch.js +37 -0
  340. package/dist/services/memory-corrected-review-findings.js +108 -0
  341. package/dist/services/memory-corrected-review-inputs.js +215 -0
  342. package/dist/services/memory-corrected-review-projection.js +78 -0
  343. package/dist/services/memory-corrected-review-receipt.js +178 -0
  344. package/dist/services/memory-corrected-review-registration.js +192 -0
  345. package/dist/services/memory-correction-check-receipt.js +235 -0
  346. package/dist/services/memory-correction-input-owner.js +6 -0
  347. package/dist/services/memory-custody-snapshot.js +36 -0
  348. package/dist/services/memory-discover-handler.js +104 -0
  349. package/dist/services/memory-finish-evidence.js +457 -0
  350. package/dist/services/memory-finish-handler.js +229 -0
  351. package/dist/services/memory-finish-integration.js +450 -0
  352. package/dist/services/memory-finish-readiness.js +169 -0
  353. package/dist/services/memory-git-integration.js +220 -0
  354. package/dist/services/memory-historical-corrected-review.js +212 -0
  355. package/dist/services/memory-historical-original-review.js +143 -0
  356. package/dist/services/memory-historical-repair-registration.js +105 -0
  357. package/dist/services/memory-historical-second-review.js +147 -0
  358. package/dist/services/memory-init-bootstrap-readiness.js +210 -0
  359. package/dist/services/memory-init-contexts.js +131 -0
  360. package/dist/services/memory-init-controller-start.js +103 -0
  361. package/dist/services/memory-init-engine-release.js +223 -0
  362. package/dist/services/memory-init-git-policy.js +165 -0
  363. package/dist/services/memory-init-lint-capability.js +203 -0
  364. package/dist/services/memory-init-native-capacity.js +309 -0
  365. package/dist/services/memory-init-preflight.js +334 -0
  366. package/dist/services/memory-init-project-bootstrap.js +599 -0
  367. package/dist/services/memory-init-request-candidate.js +213 -0
  368. package/dist/services/memory-init-request-input.js +82 -0
  369. package/dist/services/memory-init-resource-storage.js +125 -0
  370. package/dist/services/memory-init-run-allocation.js +81 -0
  371. package/dist/services/memory-init-run-bundle.js +135 -0
  372. package/dist/services/memory-init-start.js +181 -0
  373. package/dist/services/memory-init-startup-custody.js +80 -0
  374. package/dist/services/memory-init-startup-input.js +240 -0
  375. package/dist/services/memory-init-storage.js +91 -0
  376. package/dist/services/memory-init-stores.js +359 -0
  377. package/dist/services/memory-init-worktrunk-capability.js +174 -0
  378. package/dist/services/memory-layer-publication.js +611 -0
  379. package/dist/services/memory-layer.js +221 -0
  380. package/dist/services/memory-lint-config.js +194 -0
  381. package/dist/services/memory-native-review-copy.js +103 -0
  382. package/dist/services/memory-normal-flow-entry.js +87 -0
  383. package/dist/services/memory-ordinary-defaults.js +129 -0
  384. package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
  385. package/dist/services/memory-preflight-handler.js +274 -0
  386. package/dist/services/memory-repair-current-view.js +299 -0
  387. package/dist/services/memory-repair-document-delta.js +488 -0
  388. package/dist/services/memory-repair-history.js +496 -0
  389. package/dist/services/memory-repair-physical-binding.js +194 -0
  390. package/dist/services/memory-repair-publication-files.js +152 -0
  391. package/dist/services/memory-repair-publication-intent.js +27 -0
  392. package/dist/services/memory-repair-publication-plan.js +162 -0
  393. package/dist/services/memory-repair-publication-source.js +225 -0
  394. package/dist/services/memory-repair-publication.js +795 -0
  395. package/dist/services/memory-repair-registration.js +240 -0
  396. package/dist/services/memory-repair-work-baseline.js +415 -0
  397. package/dist/services/memory-repair-work-completion.js +117 -0
  398. package/dist/services/memory-repair-work-custody.js +73 -0
  399. package/dist/services/memory-repair-work-results.js +108 -0
  400. package/dist/services/memory-repair-workspace.js +261 -0
  401. package/dist/services/memory-repair-writer-copy.js +736 -0
  402. package/dist/services/memory-result-binding.js +239 -0
  403. package/dist/services/memory-retained-review-authority.js +138 -0
  404. package/dist/services/memory-retained-review-results.js +70 -0
  405. package/dist/services/memory-retained-second-baseline.js +102 -0
  406. package/dist/services/memory-retained-second-repairs.js +162 -0
  407. package/dist/services/memory-review-acceptance.js +214 -0
  408. package/dist/services/memory-review-correction-plan.js +206 -0
  409. package/dist/services/memory-review-handler.js +585 -0
  410. package/dist/services/memory-review-inputs.js +246 -0
  411. package/dist/services/memory-review-limits.js +10 -0
  412. package/dist/services/memory-review-receipt.js +180 -0
  413. package/dist/services/memory-review-registration.js +117 -0
  414. package/dist/services/memory-review-work-custody.js +126 -0
  415. package/dist/services/memory-review-work-input.js +91 -0
  416. package/dist/services/memory-review-work-results.js +95 -0
  417. package/dist/services/memory-second-corrected-review-inputs.js +167 -0
  418. package/dist/services/memory-second-correction-plan.js +77 -0
  419. package/dist/services/memory-second-repair-baseline.js +91 -0
  420. package/dist/services/memory-second-repair-launch.js +614 -0
  421. package/dist/services/memory-second-repair-registration.js +88 -0
  422. package/dist/services/memory-second-repair-source.js +187 -0
  423. package/dist/services/memory-second-review-acceptance.js +115 -0
  424. package/dist/services/memory-source-observations.js +117 -0
  425. package/dist/services/memory-stage-dispatch.js +65 -0
  426. package/dist/services/memory-stage-results.js +290 -0
  427. package/dist/services/memory-tool-probe.js +25 -0
  428. package/dist/services/memory-upgrade-analysis-handler.js +243 -0
  429. package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
  430. package/dist/services/memory-upgrade-evidence.js +100 -0
  431. package/dist/services/memory-upgrade-inventory.js +442 -0
  432. package/dist/services/memory-upgrade-publication-custody.js +73 -0
  433. package/dist/services/memory-upgrade-publication.js +413 -0
  434. package/dist/services/memory-upgrade-selection.js +430 -0
  435. package/dist/services/memory-upgrade-start.js +60 -0
  436. package/dist/services/memory-upgrade-storage-readiness.js +340 -0
  437. package/dist/services/memory-verification-acceptance.js +156 -0
  438. package/dist/services/memory-verification-checks.js +317 -0
  439. package/dist/services/memory-verification-receipt.js +160 -0
  440. package/dist/services/memory-verify-handler.js +137 -0
  441. package/dist/services/memory-work-result-json.js +12 -0
  442. package/dist/services/memory-work-result-preparation.js +64 -0
  443. package/dist/services/merge-queue.js +228 -149
  444. package/dist/services/merge-server.js +163 -35
  445. package/dist/services/migrations.js +164 -114
  446. package/dist/services/model-catalog.js +109 -0
  447. package/dist/services/model-discovery.js +46 -0
  448. package/dist/services/native-daemon-history.js +50 -0
  449. package/dist/services/native-session-control.js +39 -0
  450. package/dist/services/operator-profiles.js +236 -0
  451. package/dist/services/physical-startup.js +45 -0
  452. package/dist/services/plan-runtime.js +26 -11
  453. package/dist/services/plans.js +28 -25
  454. package/dist/services/project-registry.js +97 -0
  455. package/dist/services/project-summary.js +1 -1
  456. package/dist/services/projects.js +17 -87
  457. package/dist/services/prompt-delivery.js +71 -0
  458. package/dist/services/prompts.js +28 -17
  459. package/dist/services/protocols.js +163 -87
  460. package/dist/services/provider-capacity.js +455 -0
  461. package/dist/services/recovery-observation-budget.js +121 -0
  462. package/dist/services/recovery-snapshot-database.js +588 -0
  463. package/dist/services/release-archive-producer.js +52 -0
  464. package/dist/services/release-archive.js +143 -0
  465. package/dist/services/release-cache.js +150 -0
  466. package/dist/services/release-compatibility.js +18 -0
  467. package/dist/services/release-download.js +67 -0
  468. package/dist/services/release-envelope.js +104 -0
  469. package/dist/services/release-manifest.js +45 -0
  470. package/dist/services/release-payload.js +114 -0
  471. package/dist/services/release-producer.js +63 -0
  472. package/dist/services/release-provenance.js +160 -0
  473. package/dist/services/release-registry.js +66 -0
  474. package/dist/services/release-source.js +194 -0
  475. package/dist/services/repair-intents.js +31 -0
  476. package/dist/services/repair-receipt.js +13 -0
  477. package/dist/services/review-copy.js +165 -0
  478. package/dist/services/review-deferral.js +23 -0
  479. package/dist/services/run-control-receipt.js +56 -0
  480. package/dist/services/run-control-worker.js +428 -0
  481. package/dist/services/run-control.js +1092 -0
  482. package/dist/services/run-controller-adapter.js +400 -0
  483. package/dist/services/run-controller-capture.js +186 -0
  484. package/dist/services/run-controller-process.js +205 -0
  485. package/dist/services/run-controller-recovery.js +295 -0
  486. package/dist/services/run-controller-state.js +97 -0
  487. package/dist/services/run-controller.js +1326 -0
  488. package/dist/services/run-engine-bindings.js +1 -1
  489. package/dist/services/run-fork.js +147 -0
  490. package/dist/services/run-health.js +149 -0
  491. package/dist/services/run-observations.js +152 -0
  492. package/dist/services/run-projection.js +22 -64
  493. package/dist/services/run-recovery-guard.js +107 -0
  494. package/dist/services/run-recovery-runtime.js +112 -0
  495. package/dist/services/run-recovery.js +456 -0
  496. package/dist/services/run-start-custody.js +189 -0
  497. package/dist/services/runs.js +1023 -246
  498. package/dist/services/runtime-budget.js +446 -0
  499. package/dist/services/runtime-command.js +43 -0
  500. package/dist/services/runtime-scope-capture.js +52 -0
  501. package/dist/services/runtime-scope-control.js +468 -0
  502. package/dist/services/runtime-scope-resume.js +605 -0
  503. package/dist/services/runtime-scope-stop.js +211 -0
  504. package/dist/services/runtime-scope-worker.js +314 -0
  505. package/dist/services/runtime-service.js +19 -14
  506. package/dist/services/schema-validation.js +10 -8
  507. package/dist/services/scope-writer-cleanup.js +27 -0
  508. package/dist/services/sessions.js +36 -91
  509. package/dist/services/setup.js +427 -0
  510. package/dist/services/stage-blocker.js +17 -7
  511. package/dist/services/stage-context.js +193 -19
  512. package/dist/services/stage-handlers.js +89 -0
  513. package/dist/services/stage-lifecycle.js +152 -116
  514. package/dist/services/stage-pause.js +202 -91
  515. package/dist/services/stage-report-renderer.js +234 -6
  516. package/dist/services/stage-work-graph.js +52 -0
  517. package/dist/services/tooling.js +41 -30
  518. package/dist/services/usage.js +49 -101
  519. package/dist/services/verification-matrix.js +393 -0
  520. package/dist/services/version-status.js +1 -1
  521. package/dist/services/vnext-code-review.js +235 -88
  522. package/dist/services/vnext-code.js +327 -226
  523. package/dist/services/vnext-contracts.js +10 -1
  524. package/dist/services/vnext-execution-profile.js +80 -5
  525. package/dist/services/vnext-fanout.js +355 -21
  526. package/dist/services/vnext-merge.js +332 -111
  527. package/dist/services/vnext-plan-review.js +287 -132
  528. package/dist/services/vnext-plan.js +228 -104
  529. package/dist/services/vnext-protocolize.js +533 -95
  530. package/dist/services/vnext-specify.js +123 -81
  531. package/dist/services/vnext-workspace-policy.js +7 -2
  532. package/dist/services/work-registry.js +1706 -200
  533. package/dist/services/workspace-bootstrap.js +53 -15
  534. package/dist/services/workspace-files.js +85 -0
  535. package/dist/services/worktrees.js +105 -48
  536. package/dist/shared/entity-references.js +1 -0
  537. package/dist/shared/errors.js +27 -0
  538. package/dist/storage/agent-output.js +26 -0
  539. package/dist/storage/data-preservation.js +65 -0
  540. package/dist/storage/database.js +930 -40
  541. package/dist/storage/foreign-key-transaction.js +39 -0
  542. package/dist/storage/immutable-input.js +45 -0
  543. package/dist/storage/import-guard.js +159 -0
  544. package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
  545. package/dist/storage/native-cleanup.js +52 -0
  546. package/dist/storage/paired-writer-migration.js +424 -0
  547. package/dist/storage/paths.js +17 -4
  548. package/dist/storage/projection-lock.js +73 -0
  549. package/dist/storage/work-references.js +23 -0
  550. package/dist/storage/writer-contract.js +96 -0
  551. package/dist/storage/writer-migration.js +346 -0
  552. package/dist/storage/writer-restore.js +217 -0
  553. package/package.json +50 -13
  554. package/tools/create-canon-release.mjs +33 -0
  555. package/tools/probe-codex-history.mjs +37 -0
  556. package/tools/produce-hybrid-release.mjs +41 -0
  557. package/tools/repair-paused-run-status.mjs +84 -0
@@ -1,171 +1,240 @@
1
+ export const planCheckInstruction = "Use protocol-plan@6. Its top-level checks[] is the single check catalog. Every check has id, command, purpose, run_at and availability. available means executable now. planned means one named P* Work first creates a NEW @check/... alias: planned therefore always needs provided_by and the exact alias definition. Every semantic @check alias, including an existing one, repeats its exact accepted profile command in definition so later stages can detect drift. Items and acceptance entries use check_refs only; never duplicate command declarations.";
2
+ import { managedLifecycleCommand } from "./lifecycle-invocations.js";
3
+ import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
1
4
  import crypto from "node:crypto";
5
+ import { withStageSettlement, prepareFlowRunStageAttachment, prepareFlowRunStageCompletion } from "./runs.js";
2
6
  import fs from "node:fs";
3
7
  import path from "node:path";
4
8
  import { AppError } from "../shared/errors.js";
5
- import { effectiveCheckDeclarations, readCodeCheckProfile, validateCheckDeclaration, validateCheckPlacement, validateCodeCheckCommands } from "./code-checks.js";
9
+ import { effectiveCheckDeclarations, readCodeCheckProfile, validateCheckDeclaration, validateCheckPlacement, validateCodeCheckCommands, validateTerminatingCheckCommand } from "./code-checks.js";
6
10
  import { requireProjectByRoot } from "./projects.js";
7
- import { resolveProjectRoot } from "../storage/paths.js";
11
+ import { canonicalPath, resolveProjectRoot } from "../storage/paths.js";
8
12
  import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRunStage, getFlowRunVariables, gitFacts } from "./runs.js";
9
13
  import { validateSchema } from "./schema-validation.js";
10
- import { bindRunningWorkSession, bindStageCoordinatorWork, ensureWorkRegistry, refreshRunWorkProjection, validateWorkBatchFile } from "./work-registry.js";
14
+ import { bindRunningWorkSession, bindStageCoordinatorWork, ensureWorkRegistry, refreshRunWorkProjection, validateWorkBatch } from "./work-registry.js";
11
15
  import { flowCommand, stagePauseCommand, stagePauseCommandTemplate } from "./stage-pause.js";
12
16
  import { assertStageStartHookEvent } from "./hooks.js";
13
17
  import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
14
18
  import { readVnextSpecifyResult } from "./vnext-specify.js";
15
19
  import { nextWorkId } from "./ids.js";
16
20
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
17
- import { writeStageReport } from "./stage-report-renderer.js";
18
- import { applyExternalStageContext } from "./stage-context.js";
21
+ import { acceptStageReport, prepareStageReport } from "./stage-report-renderer.js";
22
+ import { currentStagePrompt, applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
19
23
  import { subagentCapacityKey } from "./vnext-fanout.js";
24
+ import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
25
+ import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, verificationMatrixInstruction } from "./verification-matrix.js";
26
+ import { stageSuccessor } from "./vnext-contracts.js";
20
27
  export function isVnextPlanRun(context, input) {
21
28
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
22
29
  return Boolean(context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ? AND flow_kind = 'vnext_protocolize'", [project.id, input.runId]));
23
30
  }
24
- export function startVnextPlan(context, input) {
25
- ensureWorkRegistry(context);
31
+ export function prepareVnextPlanStart(context, input) {
26
32
  const projectRoot = resolveProjectRoot(input.projectRoot);
27
33
  const project = requireProjectByRoot(context, projectRoot);
28
34
  const run = requireRun(context, projectRoot, input.runId);
29
35
  const home = requireHome(run);
30
- assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage: "plan", projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
31
- const workspaceRoute = requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage: "plan" });
36
+ const workspaceRoute = requireVnextWorkspaceRoute(context, { projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage: "plan" });
32
37
  const protocols = protocolIds(home);
33
38
  if (!protocols.length)
34
- throw new AppError("not_found", "PLAN requires accepted PROTOCOLIZE protocols", 1);
39
+ throw new AppError("runtime_artifact_missing", "PLAN requires accepted PROTOCOLIZE protocols", 1);
35
40
  // Static inputs are checked before PLAN creates a Work or materializes a
36
- // draft. A rejected start is therefore side-effect free and safe to retry.
41
+ // draft. Retained corruption is still an infrastructure failure, not a typo.
37
42
  const template = read(path.join(projectRoot, ".memory-bank", "dd-flow", "vnext", "plan.md"));
38
43
  assertProtocolWorkspace(run.workspace_root, protocols);
39
44
  const { profile: codeCheckProfile } = readCodeCheckProfile(run.workspace_root);
40
45
  if (context.db.get("SELECT 1 FROM works WHERE project_id = ? AND run_id = ? AND task = ? AND status = 'running'", [project.id, run.id, planTask]))
41
46
  throw new AppError("invalid_work_state", "PLAN already has a running Work", 1, { run_id: run.id });
47
+ const attachment = prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage: "plan", dir: "03-plan", status: "running", dataSchemaId: "dd-flow/protocol-plan@6" });
42
48
  const root = path.join(home, vnextStageDirectory("plan"));
43
- fs.mkdirSync(root, { recursive: true });
44
- const now = context.now();
49
+ if (input.externalContext)
50
+ prepareExternalStageContext({ stageRoot: root, loaded: input.externalContext });
45
51
  const rootWork = context.db.get("SELECT work_id FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NULL ORDER BY created_at LIMIT 1", [project.id, run.id]);
46
52
  if (!rootWork)
47
53
  throw new AppError("runtime_missing", "vNext RUN has no root Work", 1);
54
+ const planPaths = protocols.map(id => path.join(run.workspace_root, ".memory-bank", "protocol", id, "plan.json"));
55
+ const mapPaths = protocols.map(id => path.join(root, id, "aspect-map.json"));
56
+ const owned = protocolOwnership(home, protocols);
57
+ const identities = protocols.map(protocolId => planIdentity(home, run.id, protocolId, owned.get(protocolId) ?? []));
58
+ const aspects = aspectCatalog(run.workspace_root);
59
+ const runVariables = getFlowRunVariables(context, { projectRoot, runId: run.id });
60
+ const mergeRequired = runEndsAtMerge(context, projectRoot, run.id);
61
+ const git = gitFacts(run.workspace_root);
62
+ for (const file of [...planPaths, ...mapPaths, ...["stage-prompt.md", "work-context.json", "fixture-root.prompt.md"].map(name => path.join(root, name))])
63
+ prepareOutputFile(file);
64
+ const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage: "plan", role: "input", stageAttempt: attachment.attempt });
65
+ return { projectRoot, project, run, home, root, rootWork, workspaceRoute, protocols, template, codeCheckProfile, planPaths, mapPaths, owned, identities, aspects, runVariables, mergeRequired, git, matrix };
66
+ }
67
+ export function startVnextPlan(context, input, prepared = prepareVnextPlanStart(context, input)) {
68
+ const { projectRoot, project, run, root, rootWork, workspaceRoute, protocols, template, codeCheckProfile, planPaths, mapPaths, owned, identities, aspects, runVariables, mergeRequired, git, home } = prepared;
69
+ ensureWorkRegistry(context);
70
+ assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage: "plan", projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
71
+ if (context.db.get("SELECT 1 FROM works WHERE project_id = ? AND run_id = ? AND task = ? AND status = 'running'", [project.id, run.id, planTask]))
72
+ throw new AppError("invalid_work_state", "PLAN already has a running Work", 1, { run_id: run.id });
73
+ prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage: "plan", dir: "03-plan", status: "running", dataSchemaId: "dd-flow/protocol-plan@6" }, true);
74
+ fs.mkdirSync(root, { recursive: true });
75
+ const now = context.now();
48
76
  // Imported stage fixtures have a synthetic running root with no prior agent.
49
77
  // Bind it on the first real stage start so its child PLAN Work has a real parent.
50
78
  if (!context.db.get("SELECT 1 FROM work_sessions WHERE work_id = ? LIMIT 1", [rootWork.work_id])) {
51
79
  bindRunningWorkSession(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, promptPath: path.join(root, "fixture-root.prompt.md") });
52
80
  }
53
- context.db.exec("BEGIN IMMEDIATE");
54
- let planWorkId;
55
- try {
56
- planWorkId = nextWorkId(context, project.id, "plan");
81
+ const planWorkId = context.db.writeTransaction(() => {
82
+ if (context.db.get("SELECT 1 FROM works WHERE project_id = ? AND run_id = ? AND task = ? AND status = 'running'", [project.id, run.id, planTask]))
83
+ throw new AppError("invalid_work_state", "PLAN already has a running Work", 1, { run_id: run.id });
84
+ const planWorkId = nextWorkId(context, project.id, "plan");
57
85
  context.db.run(`INSERT INTO works (work_id, project_id, run_id, parent_work_id, task, launch_policy, result_schema, depends_on_json, status, result, started_at, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, ?, 'reuse_allowed', NULL, '[]', 'running', NULL, ?, ?, ?, NULL)`, [planWorkId, project.id, run.id, rootWork.work_id, planTask, now, now, now]);
58
- context.db.exec("COMMIT");
59
- }
60
- catch (error) {
61
- context.db.exec("ROLLBACK");
62
- throw error;
63
- }
86
+ return planWorkId;
87
+ });
64
88
  // A Desktop task may start above the materialized repository. Lifecycle
65
89
  // prompts therefore hand agents write targets as absolute paths: relative
66
90
  // `.memory-bank/...` paths would otherwise silently land in the parent cwd.
67
- const planPaths = protocols.map((id) => path.join(run.workspace_root, ".memory-bank", "protocol", id, "plan.json"));
68
- const mapPaths = protocols.map((id) => `${path.join(root, id, "aspect-map.json")}`);
69
- const owned = protocolOwnership(home, protocols);
70
- const identities = protocols.map((protocolId) => planIdentity(home, run.id, protocolId, owned.get(protocolId) ?? []));
71
91
  planPaths.forEach((file, index) => ensurePlanSkeleton(file, protocols[index], identities[index]));
72
- mapPaths.forEach((file, index) => ensureAspectMapSkeleton(file, protocols[index], identities[index], run.workspace_root));
73
- const finishCommand = `${flowCommand(context)} stage finish ${run.id} --stage plan --project-root ${JSON.stringify(projectRoot)} --json`;
92
+ mapPaths.forEach((file, index) => ensureAspectMapSkeleton(file, protocols[index], identities[index], aspects));
93
+ const finishCommand = managedLifecycleCommand(context, `${flowCommand(context)} stage finish ${run.id} --stage plan --project-root ${JSON.stringify(projectRoot)} --json`);
74
94
  const pauseCommand = stagePauseCommand(context, { runId: run.id, stage: "plan", workId: planWorkId, projectRoot });
75
95
  const pauseCommandTemplate = stagePauseCommandTemplate(pauseCommand);
76
96
  const validationCommands = protocols.flatMap((_, index) => [
77
97
  `${flowCommand(context)} schema validate --schema vnext-protocol-plan --file ${JSON.stringify(planPaths[index])} --project-root ${JSON.stringify(run.workspace_root)} --run ${run.id} --json`,
78
98
  `${flowCommand(context)} schema validate --schema plan-aspect-map --file ${JSON.stringify(mapPaths[index])} --project-root ${JSON.stringify(run.workspace_root)} --run ${run.id} --json`
79
99
  ]);
80
- const runVariables = getFlowRunVariables(context, { projectRoot, runId: run.id });
81
100
  const measuredCapacity = runVariables.variables[subagentCapacityKey];
82
- const mergeRequired = runEndsAtMerge(context, projectRoot, run.id);
101
+ const planReviewMode = requestedPlanReviewMode(context, project.id, run.id);
83
102
  const capacityContext = typeof measuredCapacity === "number" && Number.isInteger(measuredCapacity) && measuredCapacity >= 0
84
103
  ? `- The qualified reviewer capacity is ${measuredCapacity}. This is a runtime fact for later PLAN-REVIEW dispatch; do not repeat qualification or invent a different value.`
85
104
  : "- Reviewer capacity is not qualified yet. PLAN must not qualify or launch reviewers; an external harness controller supplies it before fan-out.";
86
105
  const reviewGroupingRule = "Group only semantically compatible applicable aspects, preserving real trust, irreversible, high-risk and hard-dependency boundaries. Prefer the fewest groups that retain independent review value, normally one review wave. Put two or three compatible aspects in a group; do not create one group per aspect merely for convenience. A later PLAN-REVIEW dispatch uses externally qualified capacity to schedule these semantic groups into waves; do not invent a capacity value here.";
87
- const checkProfile = path.join(run.workspace_root, ".memory-bank", "spec", "engineering", "code-check-profile.json");
88
106
  const policyMergeAliases = codeCheckProfile?.mandatory_by_gate.merge ?? [];
89
107
  const mergeContract = mergeRequired
90
108
  ? ["<merge_gate_contract>", ...(policyMergeAliases.length
91
109
  ? [`This RUN must reach MERGE. Project policy already supplies the mandatory merge gate${policyMergeAliases.length === 1 ? "" : "s"}: ${policyMergeAliases.join(", ")}. Do not duplicate them in semantic checks[]. Add another merge check only when the task genuinely needs additional evidence.`]
92
110
  : ["This RUN must reach MERGE and project policy supplies no merge gate. Select at least one real top-level checks[] entry with run_at: merge. It may use an existing project alias or a planned alias materialised by a named P* provider Work. This is a planning obligation: do not defer it to CODE-REVIEW or MERGE."]), "The CLI validates the effective merge gate but never invents one or migrates an incompatible project policy.", "</merge_gate_contract>", ""]
93
111
  : [];
94
- const prompt = ["<stage_identity>", `- RUN: ${run.id}`, `- Work: ${planWorkId}`, "- stage: plan", "</stage_identity>", "", "<trusted_runtime_context>", "These facts were collected by dd-flow. Trust them; do not repeat CLI, Git, compatibility or permission discovery.", `- Project root: ${projectRoot}`, `- Workspace: ${run.workspace_root}`, `- Stage workspace: ${root}`, `- Git: ${JSON.stringify(gitFacts(run.workspace_root))}`, capacityContext, "</trusted_runtime_context>", "", "<workspace_contract>", `- route: ${workspaceRoute.route}`, `- feature branch: ${workspaceRoute.feature_branch ?? "not applicable"}`, `- base commit: ${workspaceRoute.base_ref ?? "not applicable"}`, `- write workspace: ${run.workspace_root}`, "The CLI has verified this frozen route. All project reads and writes for PLAN and later CODE happen in the write workspace; project root is only the stable runtime identity for lifecycle commands. Do not create, switch, merge or delete branches/worktrees.", "Keep the task runner's current cwd. Use the absolute paths in this packet instead of trying to set the provisioned workspace as a tool workdir.", "</workspace_contract>", "", "<accepted_inputs>", `- ${path.join(home, "01-specify", "specify.json")}`, `- ${path.join(home, "02-protocolize", "protocolize-result.json")}`, ...protocols.map((id) => `- ${path.join(run.workspace_root, ".memory-bank", "protocol", id, "summary.md")}`), "</accepted_inputs>", "", ...(fs.existsSync(checkProfile) ? ["<code_check_policy>", "You, not the CLI, select evidence for every accepted requirement and acceptance criterion. The profile only lists reusable aliases, mandatory project policy gates and guarded raw command prefixes. Inspect relevant package/test manifests before choosing a check. Do not classify checks by weight and do not omit a needed check because it looks expensive.", fs.readFileSync(checkProfile, "utf8").trim(), "</code_check_policy>", ""] : []), ...mergeContract, "<artifacts>", "The CLI has already materialized every artifact below as a partially filled draft. Edit these files in place; do not create replacements elsewhere.", "Prefilled and CLI-owned plan fields: schema_id, plan_id, protocol_id, initial revision and source_refs.", "Prefilled and CLI-owned aspect-map fields: schema_id, protocol_id, plan_id, plan revision, catalog_ref and every catalog aspect_id.", "You own the remaining semantic fields. Empty or missing semantic values are intentional draft markers and must be completed before validation.", ...planPaths.map((value) => `- partially filled plan: ${value}`), ...mapPaths.map((value) => `- partially filled aspect map: ${value}`), "</artifacts>", "", "<output_contract>", "Complete every named plan and aspect map in place. Do not create or edit code-work-batch.json: dd-flow derives it after validation.", "The CLI owns schema_id, plan_id, protocol_id, revision and source_refs. Preserve them exactly.", "Use protocol-plan@6. Its top-level checks[] is the single check catalog. Every check has id, command, purpose, run_at and availability. available means executable now. planned means one named P* Work first creates a NEW @check/... alias: planned therefore always needs provided_by and the exact alias definition. Every semantic @check alias, including an existing one, repeats its exact accepted profile command in definition so later stages can detect drift. Items and acceptance entries use check_refs only; never duplicate command declarations.", "For each R-* and AC-*, choose an actually relevant proof: an existing focused test, a new planned alias plus its provider Work, a project policy gate, or an honestly limited external/manual proof. Every plan item needs at least one check_ref. The CLI validates ids, provider ordering, materialization and guarded command policy; it never chooses a check for you. A provider Work may verify itself with the alias it has just created. A consumer must depend on that provider.", "Each plan item must name concrete existing source/test paths in required_read. planned_write_areas is optional: use stable component directories or files only when they help coordinate parallel Work; it is never a write allowlist. Reference every owned R-* and AC-* in one or more items; every AC-* needs an observable acceptance proof.", "For every selected check, inspect its command's launch path and the runtime entrypoints it starts. The fixture/reset process, service process and client process must observe one intended environment and data world. If a required runtime entrypoint needs a code change, make that change explicit in the Work task and its verification. Use planned_write_areas only to advertise likely concurrent overlap; do not treat it as ownership or assume another Work will repair an omitted change. If an independent infrastructure Work is clearer, plan that Work explicitly and order consumers after it.", reviewGroupingRule, "Complete compact contract and schema paths:", `- protocol plan schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "vnext-protocol-plan.schema.json")}`, `- aspect map schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "plan-aspect-map.schema.json")}`, "Minimal valid protocol-plan shape:", "```json", JSON.stringify(planExample(protocols[0]), null, 2), "```", "Minimal valid aspect-map shape:", "```json", JSON.stringify(aspectMapExample(protocols[0]), null, 2), "```", "</output_contract>", "", "<execution_commands>", "PLAN never launches independent reviewers or registers CODE Work.", "If PLAN needs a material user decision with no reasonable default, run this exact one-command heredoc, replacing only its placeholder body. The heredoc is the permitted stdin form; do not use cat, a pipe, a temporary file or a second shell command:", "```sh", pauseCommandTemplate, "```", "Ask the returned user_message, stop, and resume this same PLAN Work with the exact returned command.", "Validate both partially filled drafts after completing their semantic fields:", ...validationCommands.map((command) => `- ${command}`), "Finish PLAN only after all questions are resolved and both validation commands pass:", finishCommand, "The response returns the only PLAN-REVIEW start command. Follow it; do not start CODE directly.", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n");
112
+ const prompt = ["<stage_identity>", `- RUN: ${run.id}`, `- Work: ${planWorkId}`, "- stage: plan", "</stage_identity>", "", "<trusted_runtime_context>", "These facts were collected by dd-flow. Trust them; do not repeat CLI, Git, compatibility or permission discovery.", `- Project root: ${projectRoot}`, `- Workspace: ${run.workspace_root}`, `- Stage workspace: ${root}`, `- Git: ${JSON.stringify(git)}`, capacityContext, "</trusted_runtime_context>", "", "<workspace_contract>", `- route: ${workspaceRoute.route}`, `- feature branch: ${workspaceRoute.feature_branch ?? "not applicable"}`, `- base commit: ${workspaceRoute.base_ref ?? "not applicable"}`, `- write workspace: ${run.workspace_root}`, "The CLI has verified this frozen route. All project reads and writes for PLAN and later CODE happen in the write workspace; project root is only the stable runtime identity for lifecycle commands. Do not create, switch, merge or delete branches/worktrees.", "Keep the task runner's current cwd. Use the absolute paths in this packet instead of trying to set the provisioned workspace as a tool workdir.", "</workspace_contract>", "", "<accepted_inputs>", `- ${path.join(home, "01-specify", "specify.json")}`, `- ${path.join(home, "02-protocolize", "protocolize-result.json")}`, ...protocols.map((id) => `- ${path.join(run.workspace_root, ".memory-bank", "protocol", id, "summary.md")}`), "</accepted_inputs>", "", ...(codeCheckProfile ? ["<code_check_policy>", "You, not the CLI, select evidence for every accepted requirement and acceptance criterion. The profile only lists reusable aliases, mandatory project policy gates and guarded raw command prefixes. Inspect relevant package/test manifests before choosing a check. Do not classify checks by weight and do not omit a needed check because it looks expensive.", JSON.stringify(codeCheckProfile, null, 2), "</code_check_policy>", ""] : []), ...mergeContract, "<artifacts>", "The CLI has already materialized every artifact below as a partially filled draft. Edit these files in place; do not create replacements elsewhere.", "Prefilled and CLI-owned plan fields: schema_id, plan_id, protocol_id, initial revision and source_refs.", "Prefilled and CLI-owned aspect-map fields: schema_id, protocol_id, plan_id, plan revision, catalog_ref and every catalog aspect_id.", "You own the remaining semantic fields. Empty or missing semantic values are intentional draft markers and must be completed before validation.", ...planPaths.map((value) => `- partially filled plan: ${value}`), ...mapPaths.map((value) => `- partially filled aspect map: ${value}`), "</artifacts>", "", "<output_contract>", "Complete every named plan and aspect map in place. Do not create or edit code-work-batch.json: dd-flow derives it after validation.", "The CLI owns schema_id, plan_id, protocol_id, revision and source_refs. Preserve them exactly.", planCheckInstruction, "For each R-* and AC-*, choose an actually relevant proof: an existing focused test, a new planned alias plus its provider Work, a project policy gate, or an autonomous executable check. Every plan item needs at least one check_ref. The CLI validates ids, provider ordering, materialization and guarded command policy; it never chooses a check for you. A provider Work may verify itself with the alias it has just created. A consumer must depend on that provider.", "Each check must terminate and return an exit status. Never declare a bare dev/start/serve/watch service command as a check; use a finite test that manages its own service lifecycle (for example, a browser test with a webServer fixture).", "Each plan item must name concrete existing source/test paths in required_read. planned_write_areas is optional: use stable component directories or files only when they help coordinate parallel Work; it is never a write allowlist. Reference every owned R-* and AC-* in one or more items; every AC-* needs an observable acceptance proof.", "For every selected check, inspect its command's launch path and the runtime entrypoints it starts. The fixture/reset process, service process and client process must observe one intended environment and data world. If a required runtime entrypoint needs a code change, make that change explicit in the Work task and its verification. Use planned_write_areas only to advertise likely concurrent overlap; do not treat it as ownership or assume another Work will repair an omitted change. If an independent infrastructure Work is clearer, plan that Work explicitly and order consumers after it.", reviewGroupingRule, "Complete compact contract and schema paths:", `- protocol plan schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "vnext-protocol-plan.schema.json")}`, `- aspect map schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "plan-aspect-map.schema.json")}`, "Minimal valid protocol-plan shape:", "```json", JSON.stringify(planExample(protocols[0]), null, 2), "```", "Minimal valid aspect-map shape:", "```json", JSON.stringify(aspectMapExample(protocols[0]), null, 2), "```", "</output_contract>", "", "<execution_commands>", "PLAN never launches independent reviewers or registers CODE Work.", "If PLAN needs a material user decision with no reasonable default, write the question packet to the named text file, then run the separate standalone lifecycle command below. Do not combine file creation and dd-flow in one shell command:", "```sh", pauseCommandTemplate, "```", "Ask the returned user_message, stop, and resume this same PLAN Work with the exact returned command.", "Validate both partially filled drafts after completing their semantic fields:", ...validationCommands.map((command) => `- ${command}`), "Finish PLAN only after all questions are resolved and both validation commands pass:", finishCommand, "The response returns the only PLAN-REVIEW start command. Follow it; do not start CODE directly.", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n");
95
113
  const artifactMaterialization = { status: "materialized", completeness: "partially_filled", plan_paths: planPaths, aspect_map_paths: mapPaths, cli_owned_plan_fields: ["schema_id", "plan_id", "protocol_id", "revision", "source_refs"], cli_owned_aspect_map_fields: ["schema_id", "protocol_id", "plan_id", "plan_revision", "catalog_ref", "aspects[].aspect_id"], validation_commands: validationCommands };
96
114
  const promptPath = path.join(root, "stage-prompt.md");
97
- fs.writeFileSync(promptPath, prompt);
98
- const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
115
+ // This explicit final rule supersedes historical pack wording: a flow gate
116
+ // has only executable autonomous evidence. Human or external confirmation
117
+ // is neither a check nor a permitted completion condition.
118
+ const reviewModeRule = planReviewMode === "standard" || planReviewMode === "deep"
119
+ ? `PLAN-REVIEW mode is ${planReviewMode}: assign at least one genuinely applicable aspect to a review group. Small feature size alone does not make every aspect not applicable; PLAN finish rejects an empty review set.`
120
+ : `PLAN-REVIEW mode is ${planReviewMode}: include review groups only for genuinely applicable aspects; an empty set is allowed.`;
121
+ fs.writeFileSync(promptPath, `${prompt}\n<review_mode_rule>${reviewModeRule}</review_mode_rule>\n<verification_rule>Every flow check must be an autonomous executable command. Do not declare external/manual proof or a human review as a check, evidence substitute, DEF, or gate. The only user interaction supported by this flow is an explicit stage pause for a material unanswered question.</verification_rule>\n${stageLifecycleInstruction}\n`);
122
+ const externalContext = applyExternalStageContext({ context, stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
123
+ fs.appendFileSync(promptPath, verificationMatrixInstruction(prepared.matrix?.metadata ?? null, home, "designing the source PLAN and its tests"));
99
124
  fs.writeFileSync(path.join(root, "work-context.json"), JSON.stringify({ schema_id: "dd-flow/work-context@1", system: { run_id: run.id, work_id: planWorkId, stage: "plan" }, workspace: { project_root: projectRoot, workspace_root: run.workspace_root, stage_root: root }, artifacts: artifactMaterialization, input: { protocols, owned_obligations: Object.fromEntries(owned) } }, null, 2));
100
125
  const binding = bindStageCoordinatorWork(context, { workId: planWorkId, hookEventId: input.hookEventId, stage: "plan", promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
101
- attachFlowRunStage(context, { projectRoot, runId: run.id, stage: "plan", dir: "03-plan", status: "running", dataSchemaId: "dd-flow/protocol-plan@6" });
126
+ const attachmentInput = { projectRoot, runId: run.id, stage: "plan", dir: "03-plan", status: "running", dataSchemaId: "dd-flow/protocol-plan@6" };
127
+ const attachment = prepareFlowRunStageAttachment(context, attachmentInput);
128
+ commitVerificationMatrixStart(context, prepared.matrix, () => attachFlowRunStage(context, attachmentInput, attachment));
102
129
  const workSessionId = String(binding.work_session_id);
103
130
  const sessionId = String(binding.session_id);
104
131
  refreshRunWorkProjection(context, project.id, run.id);
105
132
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "work_session_started", stage: "plan", work_id: planWorkId, id: workSessionId, session_id: sessionId });
106
- return { ok: true, run_id: run.id, work_id: planWorkId, id: workSessionId, artifact_materialization: artifactMaterialization, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), prompt_path: promptPath, next_command: finishCommand, ...(externalContext ? { external_context: externalContext } : {}) };
133
+ return { ok: true, run_id: run.id, work_id: planWorkId, id: workSessionId, artifact_materialization: artifactMaterialization, ...currentStagePrompt(context, promptPath), next_command: finishCommand, ...(externalContext ? { external_context: externalContext } : {}) };
134
+ }
135
+ export function prepareVnextPlanFinish(context, input) {
136
+ const projectRoot = resolveProjectRoot(input.projectRoot);
137
+ const run = requireRun(context, projectRoot, input.runId), home = requireHome(run);
138
+ requireVnextWorkspaceRoute(context, { projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage: "plan" });
139
+ const protocols = protocolIds(home);
140
+ assertProtocolWorkspace(run.workspace_root, protocols);
141
+ const prepared = prepareVnextPlanArtifacts(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home, protocols });
142
+ if (prepared.failures.length)
143
+ throw new AppError("validation", "PLAN artifacts have validation errors", 2, { errors: prepared.failures, phase: "prepare", effect: "no_effect", recoverable: true });
144
+ return prepared;
107
145
  }
108
146
  export function finishVnextPlan(context, input) {
147
+ return finishVnextPlanOwned(context, input);
148
+ }
149
+ function finishVnextPlanOwned(context, input) {
109
150
  const projectRoot = resolveProjectRoot(input.projectRoot);
110
151
  const project = requireProjectByRoot(context, projectRoot);
111
152
  const run = requireRun(context, projectRoot, input.runId);
112
153
  const home = requireHome(run);
113
154
  const root = path.join(home, "03-plan");
114
- requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage: "plan" });
155
+ requireVnextWorkspaceRoute(context, { projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage: "plan" });
115
156
  const protocols = protocolIds(home);
116
157
  if (!protocols.length)
117
- throw new AppError("not_found", "PLAN requires accepted PROTOCOLIZE protocols", 1);
158
+ throw new AppError("runtime_artifact_missing", "PLAN requires accepted PROTOCOLIZE protocols", 1);
118
159
  assertProtocolWorkspace(run.workspace_root, protocols);
119
160
  const planFiles = protocols.map((id) => path.join(run.workspace_root, ".memory-bank", "protocol", id, "plan.json"));
120
161
  const mapFiles = protocols.map((id) => path.join(root, id, "aspect-map.json"));
121
162
  const batch = path.join(root, "code-work-batch.json");
122
- const failures = validateVnextPlanArtifacts(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home, protocols });
123
- if (failures.length)
124
- throw new AppError("validation", "PLAN artifacts have validation errors", 2, { errors: failures });
163
+ const prepared = input.prepared ?? prepareVnextPlanFinish(context, input);
164
+ if (prepared.failures.length)
165
+ throw new AppError("validation", "PLAN artifacts have validation errors", 2, { errors: prepared.failures, phase: "prepare", effect: "no_effect", recoverable: true });
125
166
  const work = context.db.get("SELECT work_id FROM works WHERE project_id = ? AND run_id = ? AND task = ? AND status = 'running' ORDER BY created_at DESC LIMIT 1", [project.id, run.id, planTask]);
126
167
  if (!work)
127
168
  throw new AppError("invalid_work_state", "PLAN has no running Work", 1);
128
- const now = context.now();
129
- const batchChecksum = checksum(batch);
130
- context.db.run("UPDATE works SET status = 'completed', result = ?, completed_at = ?, updated_at = ? WHERE work_id = ?", ["PLAN accepted; awaiting PLAN-REVIEW", now, now, work.work_id]);
131
169
  const workSession = context.db.get("SELECT id, session_id FROM work_sessions WHERE work_id = ? AND status = 'running' ORDER BY created_at DESC LIMIT 1", [work.work_id]);
132
170
  if (!workSession?.session_id)
133
171
  throw new AppError("trusted_session_binding_required", "PLAN finish requires its bound Agent WorkSession", 1, { work_id: work.work_id });
134
- context.db.run("UPDATE work_sessions SET status = 'completed', result_path = ?, updated_at = ?, completed_at = ? WHERE id = ?", [path.join(root, "stage-report.json"), now, now, workSession.id]);
135
- refreshRunWorkProjection(context, project.id, run.id);
136
- const reviewCommand = `${flowCommand(context)} stage start ${run.id} --stage plan-review --project-root ${JSON.stringify(projectRoot)} --json`;
137
- const report = { schema_id: "dd-flow/stage-report@1", run_id: run.id, stage: "plan", generated_at: now, verdict: "done", semantic: { result: `Accepted ${protocols.length} executable PLAN artifact${protocols.length === 1 ? "" : "s"}.`, acceptance: protocols, changed_files: [...planFiles.map((file) => path.relative(projectRoot, file)), ...mapFiles.map((file) => runRef(run.id, home, file)), runRef(run.id, home, batch)], checks: ["protocol-plan schema", "aspect-map schema", "cross-artifact references", "generated CODE batch"], evidence: [runRef(run.id, home, path.join(root, "stage-report.json"))], next_action: "start_plan_review", plans: planFiles.map((file) => path.relative(projectRoot, file)), aspect_maps: mapFiles.map((file) => runRef(run.id, home, file)), code_work_batch: runRef(run.id, home, batch), batch_checksum: batchChecksum }, mechanical: { started_at: stageStartedAt(home, now), finished_at: now, wall_clock_ms: Math.max(0, Date.parse(now) - Date.parse(stageStartedAt(home, now))), git: gitFacts(run.workspace_root), session_stats_command: `${flowCommand(context)} stat run sessions ls --run ${run.id} --project-root ${JSON.stringify(projectRoot)} --json`, usage_stats_command: `${flowCommand(context)} stat usage --run ${run.id} --project-root ${JSON.stringify(projectRoot)} --json`, next_command: reviewCommand }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html", summary: "stage-report.md" }, validation: { permission_scope: "known_targets_only", memory_bank_scope: "changed_files_and_links_only", status: "passed" } };
138
- const reportJson = writeStageReport(root, report).json;
139
- validateSchema({ schemaName: "stage-report", file: reportJson, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
140
- completeFlowRunStage(context, { projectRoot, runId: run.id, stage: "plan", status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html" });
141
- advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "planned", nextAction: "start_plan_review" });
142
- appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_accepted", work_id: work.work_id, protocols, id: workSession.id, next_stage: "plan-review" });
143
- return { ok: true, run_id: run.id, protocols, next_action: "start_plan_review", next_command: reviewCommand, next: { kind: "start_stage", stage: "plan-review", command: reviewCommand } };
172
+ prepared.publish();
173
+ const now = context.now();
174
+ const batchChecksum = checksum(batch);
175
+ const next = stageSuccessor(context, { projectRoot, runId: run.id, stage: "plan-review", command: `${flowCommand(context)} stage start ${run.id} --stage plan-review --project-root ${JSON.stringify(projectRoot)} --json` });
176
+ const startedAt = stageStartedAt(home, now);
177
+ const report = { schema_id: "dd-flow/stage-report@1", run_id: run.id, stage: "plan", generated_at: now, verdict: "done", semantic: { result: `Accepted ${protocols.length} executable PLAN artifact${protocols.length === 1 ? "" : "s"}.`, acceptance: protocols, changed_files: [...planFiles.map((file) => path.relative(projectRoot, file)), ...mapFiles.map((file) => runRef(run.id, home, file)), runRef(run.id, home, batch)], checks: ["protocol-plan schema", "aspect-map schema", "cross-artifact references", "generated CODE batch"], evidence: [runRef(run.id, home, path.join(root, "stage-report.json"))], next_action: "start_plan_review", plans: planFiles.map((file) => path.relative(projectRoot, file)), aspect_maps: mapFiles.map((file) => runRef(run.id, home, file)), code_work_batch: runRef(run.id, home, batch), batch_checksum: batchChecksum }, mechanical: { started_at: startedAt, finished_at: now, wall_clock_ms: Math.max(0, Date.parse(now) - Date.parse(startedAt)), git: gitFacts(run.workspace_root), session_stats_command: `${flowCommand(context)} stat run sessions ls --run ${run.id} --project-root ${JSON.stringify(projectRoot)} --json`, usage_stats_command: `${flowCommand(context)} stat usage --run ${run.id} --project-root ${JSON.stringify(projectRoot)} --json`, ...(next.command ? { next_command: next.command } : {}) }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html", summary: "stage-report.md" }, validation: { permission_scope: "known_targets_only", memory_bank_scope: "changed_files_and_links_only", status: "passed" } };
178
+ Object.assign(report.semantic, {
179
+ plan_checksums: planFiles.map((file) => ({ path: path.relative(run.workspace_root, file).split(path.sep).join("/"), sha256: checksum(file) })),
180
+ aspect_map_checksums: mapFiles.map((file) => ({ path: path.relative(home, file).split(path.sep).join("/"), sha256: checksum(file) }))
181
+ });
182
+ const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage: "plan", role: "output" });
183
+ if (matrix)
184
+ Object.assign(report.semantic, { verification_matrix: matrix.metadata });
185
+ const preparedReport = prepareStageReport(report);
186
+ validateSchema({ schemaName: "stage-report", file: path.join(root, "stage-report.json"), data: preparedReport.normalized, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
187
+ const completionInput = { projectRoot, runId: run.id, stage: "plan", status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: preparedReport.sha256, git: report.mechanical.git };
188
+ const preparedCompletion = prepareFlowRunStageCompletion(context, completionInput);
189
+ return withStageSettlement(context, () => {
190
+ const current = context.db.get("SELECT status FROM works WHERE work_id = ?", [work.work_id]);
191
+ if (current?.status !== "running")
192
+ throw new AppError("invalid_work_state", "PLAN Work changed during finish preparation", 1);
193
+ context.db.run("UPDATE works SET status = 'completed', result = ?, completed_at = ?, updated_at = ? WHERE work_id = ?", ["PLAN accepted; awaiting PLAN-REVIEW", now, now, work.work_id]);
194
+ context.db.run("UPDATE work_sessions SET status = 'completed', result_path = ?, updated_at = ?, completed_at = ? WHERE id = ?", [path.join(root, "stage-report.json"), now, now, workSession.id]);
195
+ acceptVerificationMatrix(context, matrix);
196
+ acceptStageReport(context, { projectId: project.id, runId: run.id, stage: "plan", root, prepared: preparedReport });
197
+ completeFlowRunStage(context, completionInput, preparedCompletion);
198
+ refreshRunWorkProjection(context, project.id, run.id);
199
+ advanceFlowRun(context, { settlementStage: "plan", projectRoot, runId: run.id, status: "running", verdict: "planned", nextAction: "start_plan_review" });
200
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_accepted", work_id: work.work_id, protocols, id: workSession.id, next_stage: "plan-review" });
201
+ return { ok: true, run_id: run.id, protocols, next_action: "start_plan_review", ...(next.command ? { next_command: next.command } : {}), next };
202
+ });
144
203
  }
145
204
  export function validateVnextPlanArtifacts(context, input) {
205
+ // Validation never repairs or republishes accepted artifacts. PLAN and
206
+ // PLAN-REVIEW finish explicitly publish their prepared projection on accept.
207
+ return prepareVnextPlanArtifacts(context, { ...input, publishBatch: false }).failures;
208
+ }
209
+ /** Validate in memory; publish only after the caller accepts its inputs. */
210
+ export function prepareVnextPlanArtifacts(context, input) {
146
211
  const workspaceRoot = input.workspaceRoot ?? input.projectRoot;
147
212
  const planFiles = input.protocols.map((id) => path.join(workspaceRoot, ".memory-bank", "protocol", id, "plan.json"));
148
213
  const mapFiles = input.protocols.map((id) => path.join(input.home, "03-plan", id, "aspect-map.json"));
214
+ let reviewGroupCount = 0;
149
215
  const batch = path.join(input.home, "03-plan", "code-work-batch.json");
150
216
  const ownership = protocolOwnership(input.home, input.protocols);
151
217
  const obligations = acceptedObligations(input.home);
218
+ const mergeEnding = runEndsAtMerge(context, input.projectRoot, input.runId);
152
219
  const frozenDocumentBaselines = input.publishBatch === false && fs.existsSync(batch)
153
220
  ? new Map(readJson(batch).works?.flatMap((work) => typeof work.key === "string" ? (work.document_updates ?? []).flatMap((update) => typeof update.path === "string" && (typeof update.baseline_sha256 === "string" || update.baseline_sha256 === null)
154
221
  ? [[`${work.key}\u0000${update.path}`, update.baseline_sha256]]
155
222
  : []) : []) ?? [])
156
223
  : undefined;
157
224
  const failures = [];
225
+ const publications = new Map();
158
226
  const plans = [];
159
227
  for (const [index, file] of planFiles.entries()) {
160
228
  const protocolId = input.protocols[index];
161
229
  try {
162
- validateSchema({ schemaName: "vnext-protocol-plan", file, projectRoot: workspaceRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.home });
163
- const value = readPlan(file);
230
+ const { value } = prepareJsonSchema(file, "PLAN", { schemaName: "vnext-protocol-plan", projectRoot: workspaceRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.home });
164
231
  assertPlanIdentity(value, planIdentity(input.home, input.runId, protocolId, ownership.get(protocolId) ?? []), file);
165
- validatePlanSemantics(file, new Set(ownership.get(protocolId) ?? []), obligations);
232
+ validatePlanSemantics(file, value, new Set(ownership.get(protocolId) ?? []), obligations, mergeEnding);
166
233
  validateCodeCheckCommands(workspaceRoot, value.checks.filter((check) => check.availability === "available").map((check) => check.command));
167
234
  for (const check of value.checks.filter((item) => item.availability === "available"))
168
235
  validateCheckDeclaration(workspaceRoot, check);
236
+ for (const check of value.checks.filter((item) => item.availability === "planned" && item.definition))
237
+ validateTerminatingCheckCommand(check.definition, check.id);
169
238
  // Placement is a PLAN-time rule. A Work may legitimately materialize a
170
239
  // new aggregate alias later, so rechecking against the changed profile
171
240
  // at CODE entry would retroactively invalidate an accepted plan.
@@ -179,14 +248,25 @@ export function validateVnextPlanArtifacts(context, input) {
179
248
  }
180
249
  for (const file of mapFiles) {
181
250
  try {
182
- normalizeAspectMapRefs(file, workspaceRoot, input.home, input.runId);
183
- validateSchema({ schemaName: "plan-aspect-map", file, projectRoot: workspaceRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.home });
184
- validateAspectMap(file, input.protocols, workspaceRoot);
251
+ const source = prepareJsonSchema(file, "PLAN aspect map", { schemaName: "plan-aspect-map", projectRoot: workspaceRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.home }).text;
252
+ const bytes = normalizedAspectMapRefs(file, source, workspaceRoot, input.home, input.runId);
253
+ if (input.publishBatch === false && bytes !== source)
254
+ throw new AppError("validation", "Accepted aspect-map requires normalization; regenerate it in PLAN", 2, { file });
255
+ const value = JSON.parse(bytes);
256
+ validateAspectMap(file, input.protocols, workspaceRoot, value);
257
+ reviewGroupCount += Array.isArray(value.review_groups) ? value.review_groups.length : 0;
258
+ publications.set(file, bytes);
185
259
  }
186
260
  catch (error) {
187
261
  failures.push(validationFailure(file, error));
188
262
  }
189
263
  }
264
+ if (!failures.length && reviewGroupCount === 0) {
265
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
266
+ const mode = requestedPlanReviewMode(context, project.id, input.runId);
267
+ if (mode === "standard" || mode === "deep")
268
+ failures.push(validationFailure(mapFiles[0] ?? batch, new AppError("validation", "Configured PLAN-REVIEW requires at least one applicable aspect and review group in the PLAN map", 2, { mode })));
269
+ }
190
270
  if (!failures.length) {
191
271
  try {
192
272
  validatePsetCheckIdentity(plans);
@@ -197,35 +277,52 @@ export function validateVnextPlanArtifacts(context, input) {
197
277
  }
198
278
  }
199
279
  if (!failures.length) {
200
- const temporaryBatch = `${batch}.tmp-${crypto.randomUUID()}`;
201
280
  try {
202
281
  const projection = projectCodeWorkBatch({ home: input.home, workspaceRoot, runId: input.runId, plans, protocols: input.protocols, ...(frozenDocumentBaselines ? { frozenDocumentBaselines } : {}) });
203
282
  validateProjectedPaths(projection, workspaceRoot, input.home, input.runId);
204
- fs.writeFileSync(temporaryBatch, `${JSON.stringify(projection, null, 2)}\n`);
205
- validateSchema({ schemaName: "code-work-batch", file: temporaryBatch, projectRoot: workspaceRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.home });
206
- validateWorkBatchFile(temporaryBatch);
283
+ const bytes = `${JSON.stringify(projection, null, 2)}\n`;
284
+ validateSchema({ schemaName: "code-work-batch", file: batch, data: projection, projectRoot: workspaceRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.home });
285
+ validateWorkBatch(projection);
207
286
  if (input.publishBatch !== false) {
208
- fs.renameSync(temporaryBatch, batch);
287
+ publications.set(batch, bytes);
209
288
  }
210
- else if (!fs.existsSync(batch) || checksum(temporaryBatch) !== checksum(batch)) {
289
+ else if (!fs.existsSync(batch) || crypto.createHash("sha256").update(bytes).digest("hex") !== checksum(batch)) {
211
290
  throw new AppError("stale_code_work_batch", "CODE batch no longer matches the accepted semantic PLAN", 2, { batch });
212
291
  }
213
292
  }
214
293
  catch (error) {
215
294
  failures.push(validationFailure(batch, error));
216
295
  }
217
- finally {
218
- fs.rmSync(temporaryBatch, { force: true });
219
- }
220
296
  }
221
- return failures;
297
+ return { failures, batchChecksum: publications.has(batch) ? crypto.createHash("sha256").update(publications.get(batch)).digest("hex") : null,
298
+ publish: () => {
299
+ if (failures.length)
300
+ throw new AppError("validation", "Cannot publish invalid PLAN artifacts", 2, { errors: failures });
301
+ const published = [];
302
+ try {
303
+ for (const [file, bytes] of publications) {
304
+ const temporary = `${file}.tmp-${crypto.randomUUID()}`;
305
+ try {
306
+ fs.writeFileSync(temporary, bytes);
307
+ fs.renameSync(temporary, file);
308
+ published.push(file);
309
+ }
310
+ finally {
311
+ fs.rmSync(temporary, { force: true });
312
+ }
313
+ }
314
+ }
315
+ catch (cause) {
316
+ throw new AppError("plan_publication_failed", cause instanceof Error ? cause.message : String(cause), 1, { effect: "unknown", published_files: published, business_commit: false });
317
+ }
318
+ }
319
+ };
222
320
  }
223
321
  /** CODE entry validates the same PLAN closure without changing an accepted batch. */
224
322
  export function validateVnextCodeHandoff(context, input) {
225
323
  return validateVnextPlanArtifacts(context, {
226
324
  ...input,
227
- protocols: protocolIds(input.home),
228
- publishBatch: false
325
+ protocols: protocolIds(input.home)
229
326
  });
230
327
  }
231
328
  const planTask = "Produce accepted plan.json and aspect-map.json artifacts.";
@@ -250,16 +347,18 @@ function assertProtocolWorkspace(workspaceRoot, protocols) {
250
347
  }
251
348
  }
252
349
  function read(file) { if (!fs.existsSync(file))
253
- throw new AppError("not_found", "vNext PLAN prompt is missing", 1, { file }); return fs.readFileSync(file, "utf8"); }
350
+ throw new AppError("runtime_artifact_missing", "vNext PLAN prompt is missing", 1, { file }); return fs.readFileSync(file, "utf8"); }
254
351
  function readJson(file) { return JSON.parse(fs.readFileSync(file, "utf8")); }
255
352
  function checksum(file) { return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); }
256
- function validateAspectMap(file, protocols, projectRoot) {
257
- let value;
258
- try {
259
- value = JSON.parse(fs.readFileSync(file, "utf8"));
260
- }
261
- catch {
262
- throw new AppError("schema_validation", "aspect-map.json must be valid JSON", 2, { file });
353
+ function validateAspectMap(file, protocols, projectRoot, prepared) {
354
+ let value = prepared;
355
+ if (value === undefined) {
356
+ try {
357
+ value = JSON.parse(fs.readFileSync(file, "utf8"));
358
+ }
359
+ catch {
360
+ throw new AppError("schema_validation", "aspect-map.json must be valid JSON", 2, { file });
361
+ }
263
362
  }
264
363
  const map = value;
265
364
  if (map.schema_id !== "dd-flow/plan-aspect-map@3" || typeof map.protocol_id !== "string" || !protocols.includes(map.protocol_id) || !Array.isArray(map.aspects))
@@ -304,7 +403,13 @@ function stageStartedAt(home, fallback) { try {
304
403
  catch {
305
404
  return fallback;
306
405
  } }
307
- function validationFailure(file, error) { return { file, message: error instanceof Error ? error.message : String(error), ...(error instanceof AppError ? { code: error.code, details: error.details } : {}) }; }
406
+ function validationFailure(file, error) {
407
+ // Aggregate diagnosed artifact mistakes only. Runtime/schema/storage failures
408
+ // must retain their original code rather than becoming a safe PLAN retry.
409
+ if (!(error instanceof AppError) || error.exitCode !== 2 || ["schema_not_found", "invalid_code_check_profile"].includes(error.code))
410
+ throw error;
411
+ return { file, message: error.message, code: error.code, details: error.details };
412
+ }
308
413
  function acceptedObligations(home) {
309
414
  const file = path.join(home, "01-specify", "specify.json");
310
415
  if (!fs.existsSync(file))
@@ -341,7 +446,7 @@ function ensurePlanSkeleton(file, protocolId, identity) {
341
446
  fs.mkdirSync(path.dirname(file), { recursive: true });
342
447
  writeJson(file, { ...identity, title: "", summary: "", goal: { outcome: "", constraints: [], non_goals: [] }, assessment: {}, decisions: [], document_updates: [], checks: [], items: [], acceptance: [] });
343
448
  }
344
- function ensureAspectMapSkeleton(file, protocolId, identity, workspaceRoot) {
449
+ function ensureAspectMapSkeleton(file, protocolId, identity, aspects) {
345
450
  if (fs.existsSync(file))
346
451
  return;
347
452
  fs.mkdirSync(path.dirname(file), { recursive: true });
@@ -354,10 +459,9 @@ function ensureAspectMapSkeleton(file, protocolId, identity, workspaceRoot) {
354
459
  catalog_ref: { path: ".memory-bank/dd-flow/mb-sdlc/plan-aspects/aspects" },
355
460
  routing: { initial_state: "orchestrator_local", groups: [] },
356
461
  review_groups: [],
357
- aspects: aspectCatalog(workspaceRoot).map((aspectId) => ({ aspect_id: aspectId, planned_artifact_refs: [] }))
462
+ aspects: aspects.map((aspectId) => ({ aspect_id: aspectId, planned_artifact_refs: [] }))
358
463
  });
359
464
  }
360
- function readPlan(file) { return JSON.parse(fs.readFileSync(file, "utf8")); }
361
465
  function assertPlanIdentity(value, identity, file) {
362
466
  for (const key of ["schema_id", "plan_id", "protocol_id"])
363
467
  if (value[key] !== identity[key])
@@ -367,13 +471,17 @@ function assertPlanIdentity(value, identity, file) {
367
471
  if (JSON.stringify(value.source_refs) !== JSON.stringify(identity.source_refs))
368
472
  throw new AppError("validation", "PLAN must preserve CLI-owned source_refs", 2, { file });
369
473
  }
370
- function normalizeAspectMapRefs(file, projectRoot, runHome, runId) {
371
- const map = JSON.parse(fs.readFileSync(file, "utf8"));
474
+ function normalizedAspectMapRefs(file, bytes, projectRoot, runHome, runId) {
475
+ projectRoot = canonicalPath(projectRoot);
476
+ runHome = canonicalPath(runHome);
477
+ const map = JSON.parse(bytes);
478
+ if (!map || typeof map !== "object" || Array.isArray(map))
479
+ throw new AppError("schema_validation", "PLAN aspect map must be an object", 2, { file });
372
480
  let changed = false;
373
481
  const normalize = (value) => {
374
482
  if (!path.isAbsolute(value))
375
483
  return value;
376
- const source = path.resolve(value);
484
+ const source = canonicalPath(value, false);
377
485
  const inProject = source.startsWith(`${projectRoot}${path.sep}`);
378
486
  const inRun = source.startsWith(`${runHome}${path.sep}`);
379
487
  if (!inProject && !inRun)
@@ -386,10 +494,9 @@ function normalizeAspectMapRefs(file, projectRoot, runHome, runId) {
386
494
  for (const aspect of map.aspects ?? [])
387
495
  if (Array.isArray(aspect.planned_artifact_refs))
388
496
  aspect.planned_artifact_refs = aspect.planned_artifact_refs.map(normalize);
389
- if (changed)
390
- fs.writeFileSync(file, `${JSON.stringify(map, null, 2)}\n`);
497
+ return changed ? `${JSON.stringify(map, null, 2)}\n` : bytes;
391
498
  }
392
- function projectCodeWorkBatch(input) {
499
+ export function projectCodeWorkBatch(input) {
393
500
  const obligationMap = acceptedObligationMap(input.home);
394
501
  const orientation = projectOrientation(input.workspaceRoot);
395
502
  const works = input.plans.flatMap(({ protocolId, file, value }) => value.items.map((item) => {
@@ -424,10 +531,9 @@ function projectCodeWorkBatch(input) {
424
531
  ])],
425
532
  discovery_boundary: item.execution_context.discovery_boundary,
426
533
  planned_write_areas: [...new Set([...item.execution_context.planned_write_areas, ...documentPaths])],
427
- // A criterion belongs to every listed Work. Its proof must travel with
428
- // those Work packets as well as item-local verification; otherwise CODE
429
- // silently loses readiness/code checks declared only by acceptance.
430
- checks: value.checks.filter((check) => itemCheckRefs(item, value.acceptance).includes(check.id)),
534
+ // CODE still needs acceptance-only aggregate declarations in its packets;
535
+ // work checks belong only to the item that explicitly verifies them.
536
+ checks: value.checks.filter((check) => itemCheckRefs(item, value.acceptance, value.checks).includes(check.id)),
431
537
  provides_checks: value.checks.filter((check) => check.availability === "planned" && check.provided_by === item.id),
432
538
  stop_conditions: item.execution_context.stop_conditions,
433
539
  depends_on: item.depends_on.map((dependency) => `${protocolId}:${dependency}`),
@@ -456,10 +562,15 @@ function projectCodeWorkBatch(input) {
456
562
  const ordered = [...works].sort((a, b) => a.key.localeCompare(b.key));
457
563
  return { schema_id: "dd-flow/code-work-batch@5", sources: input.plans.map(({ protocolId, file, value }) => ({ plan_id: value.plan_id, protocol_id: protocolId, revision: value.revision, sha256: checksum(file) })), works: ordered };
458
564
  }
459
- function itemCheckRefs(item, acceptance) {
460
- return [...new Set([...item.verification.check_refs, ...acceptance.filter((entry) => entry.plan_item_ids.includes(item.id)).flatMap((entry) => entry.check_refs)])];
565
+ function itemCheckRefs(item, acceptance, catalog) {
566
+ const aggregate = new Set(catalog.filter((check) => check.run_at !== "work").map((check) => check.id));
567
+ return [...new Set([...item.verification.check_refs, ...acceptance.filter((entry) => entry.plan_item_ids.includes(item.id)).flatMap((entry) => entry.check_refs.filter((ref) => aggregate.has(ref)))])];
568
+ }
569
+ function renderCodeTask(item, catalog, acceptance = []) {
570
+ const local = catalog.filter((check) => check.run_at === "work" && item.verification.check_refs.includes(check.id));
571
+ const aggregate = catalog.filter((check) => check.run_at !== "work" && itemCheckRefs(item, acceptance, catalog).includes(check.id));
572
+ return [item.title, item.summary, item.details, `Preserve: ${item.semantic_spine.must_preserve.join("; ")}`, `Work checks: ${local.map((check) => `${check.id}:${check.command}`).join("; ")}`, `Later aggregate checks (CODE/MERGE; do not run in this Work): ${aggregate.map((check) => `${check.id}:${check.command}`).join("; ")}`, `Stop: ${item.execution_context.stop_conditions.join("; ")}`].join("\n\n");
461
573
  }
462
- function renderCodeTask(item, catalog, acceptance = []) { const checks = catalog.filter((check) => itemCheckRefs(item, acceptance).includes(check.id)); return [item.title, item.summary, item.details, `Preserve: ${item.semantic_spine.must_preserve.join("; ")}`, `Checks: ${checks.map((check) => `${check.id}:${check.command}`).join("; ")}`, `Stop: ${item.execution_context.stop_conditions.join("; ")}`].join("\n\n"); }
463
574
  function validateProjectedPaths(batch, workspaceRoot, home, runId) {
464
575
  for (const work of batch.works) {
465
576
  for (const write of work.planned_write_areas)
@@ -503,8 +614,7 @@ function resolvePortablePath(value, workspaceRoot, home, runId) {
503
614
  }
504
615
  function writeJson(file, value) { const temporary = `${file}.${crypto.randomUUID()}.tmp`; fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`); fs.renameSync(temporary, file); }
505
616
  function runRef(runId, runHome, file) { return `run://${runId}/${path.relative(runHome, file).split(path.sep).join("/")}`; }
506
- function validatePlanSemantics(file, ownedRefs, acceptedRefs) {
507
- const plan = readPlan(file);
617
+ function validatePlanSemantics(file, plan, ownedRefs, acceptedRefs, mergeEnding) {
508
618
  const items = plan.items ?? [];
509
619
  const ids = new Set(items.map((item) => item.id).filter((id) => Boolean(id)));
510
620
  if (ids.size !== items.length)
@@ -517,6 +627,8 @@ function validatePlanSemantics(file, ownedRefs, acceptedRefs) {
517
627
  for (const check of plan.checks ?? []) {
518
628
  if (checks.has(check.id))
519
629
  throw new AppError("validation", "PLAN check ids must be unique", 2, { file, check_id: check.id });
630
+ if (mergeEnding && check.run_at === "release")
631
+ throw new AppError("check_gate_unreachable", "A RUN ending at MERGE cannot execute RELEASE checks", 2, { file, check_id: check.id });
520
632
  if (check.availability === "planned" && (!check.provided_by || !ids.has(check.provided_by)))
521
633
  throw new AppError("planned_check_provider_missing", "A planned check must name a PLAN item that provides it", 2, { file, check_id: check.id, provided_by: check.provided_by ?? null });
522
634
  if (check.availability === "planned" && !check.command.startsWith("@check/"))
@@ -525,6 +637,8 @@ function validatePlanSemantics(file, ownedRefs, acceptedRefs) {
525
637
  throw new AppError("planned_check_definition_missing", "A planned check must declare its exact alias definition", 2, { file, check_id: check.id });
526
638
  checks.set(check.id, check);
527
639
  }
640
+ const localChecks = new Set(items.flatMap((item) => item.verification.check_refs));
641
+ const acceptanceIds = new Set();
528
642
  const ancestors = (itemId, seen = new Set()) => {
529
643
  for (const dependency of items.find((item) => item.id === itemId)?.depends_on ?? [])
530
644
  if (!seen.has(dependency)) {
@@ -548,7 +662,7 @@ function validatePlanSemantics(file, ownedRefs, acceptedRefs) {
548
662
  const check = checks.get(checkId);
549
663
  if (!check)
550
664
  throw new AppError("check_reference_unknown", "PLAN item references an unknown check", 2, { file, item: item.id, check_id: checkId });
551
- if (check.availability === "planned" && check.provided_by !== item.id && !ancestors(item.id).has(check.provided_by))
665
+ if (check.run_at === "work" && check.availability === "planned" && check.provided_by !== item.id && !ancestors(item.id).has(check.provided_by))
552
666
  throw new AppError("check_consumer_not_ordered_after_provider", "A Work may consume a planned check only after its provider", 2, { file, item: item.id, check_id: checkId, provider: check.provided_by });
553
667
  }
554
668
  }
@@ -564,6 +678,11 @@ function validatePlanSemantics(file, ownedRefs, acceptedRefs) {
564
678
  for (const acceptance of plan.acceptance ?? []) {
565
679
  if (!acceptance.criterion_id || !acceptance.criterion_id.startsWith("AC-") || !ownedRefs.has(acceptance.criterion_id))
566
680
  throw new AppError("validation", "PLAN acceptance references an AC-* owned by this protocol", 2, { file, criterion_id: acceptance.criterion_id });
681
+ if (acceptanceIds.has(acceptance.criterion_id))
682
+ throw new AppError("duplicate_plan_criterion", "PLAN acceptance criterion ids must be unique within a protocol", 2, { file, criterion_id: acceptance.criterion_id });
683
+ acceptanceIds.add(acceptance.criterion_id);
684
+ if (mergeEnding && acceptance.gate === "release")
685
+ throw new AppError("check_gate_unreachable", "A RUN ending at MERGE cannot prove RELEASE acceptance", 2, { file, criterion_id: acceptance.criterion_id });
567
686
  for (const id of acceptance.plan_item_ids ?? [])
568
687
  if (!ids.has(id))
569
688
  throw new AppError("validation", "PLAN acceptance references an unknown item", 2, { file, criterion_id: acceptance.criterion_id, plan_item_id: id });
@@ -571,9 +690,10 @@ function validatePlanSemantics(file, ownedRefs, acceptedRefs) {
571
690
  const check = checks.get(id);
572
691
  if (!check)
573
692
  throw new AppError("check_reference_unknown", "PLAN acceptance references an unknown check", 2, { file, criterion_id: acceptance.criterion_id, check_id: id });
574
- for (const itemId of acceptance.plan_item_ids ?? [])
575
- if (check.availability === "planned" && check.provided_by !== itemId && !ancestors(itemId).has(check.provided_by))
576
- throw new AppError("check_consumer_not_ordered_after_provider", "Acceptance may consume a planned check only after its provider", 2, { file, criterion_id: acceptance.criterion_id, item: itemId, check_id: id, provider: check.provided_by });
693
+ if ((acceptance.gate !== "release" && check.run_at === "release") || (!["merge", "release"].includes(acceptance.gate) && check.run_at === "merge"))
694
+ throw new AppError("check_gate_unreachable", "PLAN acceptance references a check after its gate", 2, { file, criterion_id: acceptance.criterion_id, check_id: id, gate: acceptance.gate, run_at: check.run_at });
695
+ if (check.run_at === "work" && !localChecks.has(id))
696
+ throw new AppError("work_check_unassigned", "A work check referenced by acceptance needs an explicit verifying PLAN item", 2, { file, criterion_id: acceptance.criterion_id, check_id: id });
577
697
  }
578
698
  }
579
699
  for (const obligation of ownedRefs)
@@ -593,6 +713,10 @@ function validatePsetCheckIdentity(plans) {
593
713
  owners.set(check.id, plan.protocolId);
594
714
  }
595
715
  }
716
+ function requestedPlanReviewMode(context, projectId, runId) {
717
+ const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
718
+ return JSON.parse(row?.index_json ?? "{}").settings?.plan_review?.mode ?? "auto";
719
+ }
596
720
  function runEndsAtMerge(context, projectRoot, runId) {
597
721
  const project = requireProjectByRoot(context, resolveProjectRoot(projectRoot));
598
722
  const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, runId]);