@deksden-com/dd-flow-cli 0.9.0-beta.14 → 0.9.0-beta.140

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 (493) hide show
  1. package/CHANGELOG.md +733 -0
  2. package/README.md +143 -1
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/argument-values.js +38 -0
  5. package/dist/cli/command-inputs.js +376 -0
  6. package/dist/cli/command-metadata.js +197 -0
  7. package/dist/cli/command-option-descriptions.js +191 -0
  8. package/dist/cli/completion-cli.js +70 -0
  9. package/dist/cli/completion.js +57 -0
  10. package/dist/cli/error-output.js +52 -0
  11. package/dist/cli/executor-onboarding.js +76 -0
  12. package/dist/cli/flows.js +29 -0
  13. package/dist/cli/harnesses.js +84 -0
  14. package/dist/cli/help.js +328 -18
  15. package/dist/cli/hook-ingress.js +78 -0
  16. package/dist/cli/input-preparation.js +165 -0
  17. package/dist/cli/memory-flow-admission.js +44 -0
  18. package/dist/cli/memory-init-invocation.js +78 -0
  19. package/dist/cli/memory-init.js +240 -0
  20. package/dist/cli/memory-storage-admission.js +53 -0
  21. package/dist/cli/models.js +39 -0
  22. package/dist/cli/native-hook-ingress.js +80 -0
  23. package/dist/cli/operator-profiles.js +61 -0
  24. package/dist/cli/output-options.js +35 -0
  25. package/dist/cli/response-file.js +71 -0
  26. package/dist/cli/run-cli.js +1689 -545
  27. package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
  28. package/dist/cli/runtime-maintenance-ingress.js +52 -0
  29. package/dist/cli/runtime-process.js +153 -0
  30. package/dist/cli/setup.js +149 -0
  31. package/dist/cli.js +71 -2
  32. package/dist/domain/stage-catalog.js +1 -1
  33. package/dist/domain/stage-handler-identities.js +20 -0
  34. package/dist/harness-runtime/DD-ZCODE.md +112 -0
  35. package/dist/harness-runtime/OPENCODE-LICENSE.txt +24 -0
  36. package/dist/harness-runtime/bin/dd-agy.mjs +56 -0
  37. package/dist/harness-runtime/bin/dd-codex.mjs +27 -0
  38. package/dist/harness-runtime/bin/dd-droid.mjs +34 -0
  39. package/dist/harness-runtime/bin/dd-grok.mjs +40 -0
  40. package/dist/harness-runtime/bin/dd-opencode.mjs +22 -0
  41. package/dist/harness-runtime/bin/dd-zcode.mjs +87 -0
  42. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  43. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  44. package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
  45. package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
  46. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  47. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  48. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  49. package/dist/harness-runtime/lib/daemon-operations.mjs +371 -0
  50. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
  51. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +724 -0
  52. package/dist/harness-runtime/lib/dd-agy.mjs +70 -0
  53. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  54. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +196 -0
  55. package/dist/harness-runtime/lib/dd-codex.d.mts +1 -0
  56. package/dist/harness-runtime/lib/dd-codex.mjs +817 -0
  57. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +129 -0
  58. package/dist/harness-runtime/lib/dd-droid.mjs +584 -0
  59. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +498 -0
  60. package/dist/harness-runtime/lib/dd-grok.mjs +177 -0
  61. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +507 -0
  62. package/dist/harness-runtime/lib/dd-opencode.mjs +354 -0
  63. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +750 -0
  64. package/dist/harness-runtime/lib/dd-zcode.mjs +1145 -0
  65. package/dist/harness-runtime/lib/delegation-instructions.d.mts +14 -0
  66. package/dist/harness-runtime/lib/delegation-instructions.mjs +157 -0
  67. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  68. package/dist/harness-runtime/lib/driver-recovery.mjs +139 -0
  69. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  70. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  71. package/dist/harness-runtime/lib/lease-renewal.mjs +37 -0
  72. package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
  73. package/dist/harness-runtime/lib/lifecycle-command.mjs +273 -0
  74. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
  75. package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
  76. package/dist/harness-runtime/lib/managed-daemon.d.mts +13 -0
  77. package/dist/harness-runtime/lib/managed-daemon.mjs +654 -0
  78. package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
  79. package/dist/harness-runtime/lib/model-observations.mjs +101 -0
  80. package/dist/harness-runtime/lib/native-children.d.mts +16 -0
  81. package/dist/harness-runtime/lib/native-children.mjs +113 -0
  82. package/dist/harness-runtime/lib/native-hook-command.d.mts +7 -0
  83. package/dist/harness-runtime/lib/native-hook-command.mjs +251 -0
  84. package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
  85. package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
  86. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  87. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  88. package/dist/harness-runtime/lib/observation-clock.mjs +92 -0
  89. package/dist/harness-runtime/lib/opencode-plugin.mjs +85 -0
  90. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  91. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  92. package/dist/harness-runtime/lib/operation-errors.mjs +116 -0
  93. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  94. package/dist/harness-runtime/lib/operation-progress.mjs +114 -0
  95. package/dist/harness-runtime/lib/process-json.d.mts +5 -0
  96. package/dist/harness-runtime/lib/process-json.mjs +201 -0
  97. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  98. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +13 -0
  99. package/dist/harness-runtime/lib/provider-overload-policy.mjs +138 -0
  100. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  101. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  102. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  103. package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
  104. package/dist/harness-runtime/lib/setup-progress.mjs +51 -0
  105. package/dist/harness-runtime/lib/tool-observations.d.mts +21 -0
  106. package/dist/harness-runtime/lib/tool-observations.mjs +228 -0
  107. package/dist/harness-runtime/lib/zcode-usage.mjs +55 -0
  108. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-1y4a697q.js +81 -0
  109. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-2ny4vxct.js +92 -0
  110. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-6mptkzbc.js +0 -0
  111. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-7x67cf1q.js +52 -0
  112. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9rqn6x4v.js +16 -0
  113. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9vdz0zwn.js +1661 -0
  114. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ecs5ftka.js +1466 -0
  115. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-frbwqjmf.js +145 -0
  116. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gne7h7mq.js +50 -0
  117. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gx9wnkzr.js +105 -0
  118. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-m2eywc8w.js +0 -0
  119. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-nwcpakzx.js +12 -0
  120. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-q4g3afnw.js +623 -0
  121. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-qav6mfdz.js +0 -0
  122. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-sxt88q69.js +12 -0
  123. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-t1xa31vm.js +39 -0
  124. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ve4aws36.js +103 -0
  125. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vmytr4q8.js +4 -0
  126. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vt9jbch8.js +36 -0
  127. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vyas3vgt.js +0 -0
  128. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-wjyeq25y.js +30 -0
  129. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-xgnj2w4v.js +20 -0
  130. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-y2etg5bs.js +9 -0
  131. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ydhz1zrh.js +238 -0
  132. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-z6qdwcsf.js +62 -0
  133. package/dist/harness-runtime/vendor/opencode-client/contract.d.ts +1 -0
  134. package/dist/harness-runtime/vendor/opencode-client/contract.js +12 -0
  135. package/dist/harness-runtime/vendor/opencode-client/effect/api/api.d.ts +2806 -0
  136. package/dist/harness-runtime/vendor/opencode-client/effect/api/api.js +0 -0
  137. package/dist/harness-runtime/vendor/opencode-client/effect/api.d.ts +4 -0
  138. package/dist/harness-runtime/vendor/opencode-client/effect/api.js +0 -0
  139. package/dist/harness-runtime/vendor/opencode-client/effect/client.d.ts +2530 -0
  140. package/dist/harness-runtime/vendor/opencode-client/effect/client.js +15 -0
  141. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.d.ts +7 -0
  142. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.js +6 -0
  143. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.d.ts +2529 -0
  144. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.js +8 -0
  145. package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.d.ts +2 -0
  146. package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.js +12 -0
  147. package/dist/harness-runtime/vendor/opencode-client/effect/index.d.ts +35 -0
  148. package/dist/harness-runtime/vendor/opencode-client/effect/index.js +75 -0
  149. package/dist/harness-runtime/vendor/opencode-client/effect/rpc.d.ts +20 -0
  150. package/dist/harness-runtime/vendor/opencode-client/effect/rpc.js +11 -0
  151. package/dist/harness-runtime/vendor/opencode-client/effect/service.d.ts +90 -0
  152. package/dist/harness-runtime/vendor/opencode-client/effect/service.js +187 -0
  153. package/dist/harness-runtime/vendor/opencode-client/package.json +84 -0
  154. package/dist/harness-runtime/vendor/opencode-client/promise/api.d.ts +21 -0
  155. package/dist/harness-runtime/vendor/opencode-client/promise/api.js +0 -0
  156. package/dist/harness-runtime/vendor/opencode-client/promise/client.d.ts +252 -0
  157. package/dist/harness-runtime/vendor/opencode-client/promise/client.js +14 -0
  158. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.d.ts +8 -0
  159. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.js +6 -0
  160. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.d.ts +259 -0
  161. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.js +7 -0
  162. package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.d.ts +3 -0
  163. package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.js +79 -0
  164. package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.d.ts +8932 -0
  165. package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.js +70 -0
  166. package/dist/harness-runtime/vendor/opencode-client/promise/index.d.ts +6 -0
  167. package/dist/harness-runtime/vendor/opencode-client/promise/index.js +83 -0
  168. package/dist/harness-runtime/vendor/opencode-client/promise/rpc.d.ts +32 -0
  169. package/dist/harness-runtime/vendor/opencode-client/promise/rpc.js +7 -0
  170. package/dist/harness-runtime/vendor/opencode-client/promise/service.d.ts +24 -0
  171. package/dist/harness-runtime/vendor/opencode-client/promise/service.js +187 -0
  172. package/dist/harness-runtime/vendor/opencode-client/pty-handoff.d.ts +12 -0
  173. package/dist/harness-runtime/vendor/opencode-client/pty-handoff.js +14 -0
  174. package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.d.ts +20 -0
  175. package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.js +14 -0
  176. package/dist/harness-runtime/vendor/opencode-client/service-contender.d.ts +37 -0
  177. package/dist/harness-runtime/vendor/opencode-client/service-contender.js +10 -0
  178. package/dist/harness-runtime/vendor/opencode-client/service-probe.d.ts +70 -0
  179. package/dist/harness-runtime/vendor/opencode-client/service-probe.js +16 -0
  180. package/dist/harness-runtime/vendor/opencode-client/service-timing.d.ts +12 -0
  181. package/dist/harness-runtime/vendor/opencode-client/service-timing.js +10 -0
  182. package/dist/harness-runtime/vendor/opencode-client/service-version.d.ts +2 -0
  183. package/dist/harness-runtime/vendor/opencode-client/service-version.js +6 -0
  184. package/dist/harness-runtime/vendor/opencode-client/service.d.ts +52 -0
  185. package/dist/harness-runtime/vendor/opencode-client/service.js +1 -0
  186. package/dist/harness-runtime/vendor/opencode-client/shared-events.d.ts +11 -0
  187. package/dist/harness-runtime/vendor/opencode-client/shared-events.js +8 -0
  188. package/dist/harness-runtime/vendor/opencode-client/solid/connection.d.ts +37 -0
  189. package/dist/harness-runtime/vendor/opencode-client/solid/connection.js +10 -0
  190. package/dist/harness-runtime/vendor/opencode-client/solid/data.d.ts +211 -0
  191. package/dist/harness-runtime/vendor/opencode-client/solid/data.js +18 -0
  192. package/dist/harness-runtime/vendor/opencode-client/solid/index.d.ts +3 -0
  193. package/dist/harness-runtime/vendor/opencode-client/solid/index.js +32 -0
  194. package/dist/harness-runtime/vendor/opencode-client/solid/pty.d.ts +22 -0
  195. package/dist/harness-runtime/vendor/opencode-client/solid/pty.js +8 -0
  196. package/dist/harness-runtime/vendor/opencode-plugin.mjs +3 -0
  197. package/dist/protocol/local-files.js +18 -9
  198. package/dist/runtime/context.js +8 -4
  199. package/dist/schemas/agent-profile.schema.json +1 -1
  200. package/dist/schemas/code-review-result.schema.json +1 -1
  201. package/dist/schemas/code-verification.schema.json +7 -5
  202. package/dist/schemas/code-work-batch.schema.json +2 -2
  203. package/dist/schemas/discovery-result.schema.json +184 -0
  204. package/dist/schemas/execution-bundle.schema.json +205 -0
  205. package/dist/schemas/execution-settings.schema.json +162 -0
  206. package/dist/schemas/flow-descriptor.schema.json +54 -0
  207. package/dist/schemas/flow-run.schema.json +28 -0
  208. package/dist/schemas/harness-catalog.schema.json +80 -0
  209. package/dist/schemas/mb-init-compose-result.schema.json +457 -0
  210. package/dist/schemas/mb-init-discover-result.schema.json +500 -0
  211. package/dist/schemas/mb-init-finish-result.schema.json +422 -0
  212. package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
  213. package/dist/schemas/mb-init-review-result.schema.json +396 -0
  214. package/dist/schemas/mb-init-verify-result.schema.json +391 -0
  215. package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
  216. package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
  217. package/dist/schemas/memory-composition-work-result.schema.json +38 -0
  218. package/dist/schemas/memory-repair-work-result.schema.json +49 -0
  219. package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
  220. package/dist/schemas/memory-review-work-result.schema.json +43 -0
  221. package/dist/schemas/model-catalog.schema.json +153 -0
  222. package/dist/schemas/plan-review-decision.schema.json +3 -3
  223. package/dist/schemas/plan-review-result.schema.json +2 -2
  224. package/dist/schemas/profile-settings.schema.json +122 -0
  225. package/dist/schemas/release-manifest.schema.json +291 -0
  226. package/dist/schemas/run-control-receipt.schema.json +118 -0
  227. package/dist/schemas/run-control-request.schema.json +36 -0
  228. package/dist/schemas/run-engine-binding.schema.json +1 -1
  229. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  230. package/dist/services/branch-context.js +3 -3
  231. package/dist/services/canon.js +11 -8
  232. package/dist/services/check-runner-evidence.js +168 -0
  233. package/dist/services/cleanup.js +133 -40
  234. package/dist/services/cli-operation-classifier.js +25 -62
  235. package/dist/services/cli-upgrade-allowlist.js +27 -0
  236. package/dist/services/code-checks.js +1156 -107
  237. package/dist/services/codex-hook-delivery.js +32 -0
  238. package/dist/services/command-context.js +80 -0
  239. package/dist/services/compatibility-preflight.js +13 -3
  240. package/dist/services/config.js +11 -8
  241. package/dist/services/continuation-outcome.js +20 -0
  242. package/dist/services/controller-child-capacity.js +13 -0
  243. package/dist/services/controller-child-observer.js +115 -0
  244. package/dist/services/controller-execution.js +88 -0
  245. package/dist/services/controller-fanout.js +202 -0
  246. package/dist/services/dashboard.js +143 -58
  247. package/dist/services/descriptor-coordinator-session.js +35 -0
  248. package/dist/services/descriptor-root-work.js +53 -0
  249. package/dist/services/descriptor-run-settlement.js +105 -0
  250. package/dist/services/descriptor-run-start.js +449 -0
  251. package/dist/services/descriptor-stage-coordinator.js +197 -0
  252. package/dist/services/descriptor-stage-settlement.js +222 -0
  253. package/dist/services/descriptor-stage-start.js +139 -0
  254. package/dist/services/discovery/agy-discovery.js +2 -0
  255. package/dist/services/discovery/codex-discovery.js +2 -0
  256. package/dist/services/discovery/droid-discovery.js +2 -0
  257. package/dist/services/discovery/grok-discovery.js +2 -0
  258. package/dist/services/discovery/opencode-discovery.js +2 -0
  259. package/dist/services/discovery/static.js +31 -0
  260. package/dist/services/discovery/zcode-discovery.js +2 -0
  261. package/dist/services/engines.js +294 -153
  262. package/dist/services/eval-snapshots.js +1215 -116
  263. package/dist/services/execution-bundles.js +263 -0
  264. package/dist/services/execution-policy.js +185 -0
  265. package/dist/services/executor-onboarding.js +56 -0
  266. package/dist/services/external-work-launch.js +308 -0
  267. package/dist/services/flow-catalog.js +21 -0
  268. package/dist/services/flow-descriptors.js +82 -0
  269. package/dist/services/frozen-engine.js +250 -0
  270. package/dist/services/generic-execution.js +164 -0
  271. package/dist/services/harness-adapter.js +355 -36
  272. package/dist/services/harness-config.js +2 -0
  273. package/dist/services/harness-discovery.js +29 -0
  274. package/dist/services/harness-doctor.js +75 -0
  275. package/dist/services/harness-probe-effects.js +51 -0
  276. package/dist/services/harness-version-probe.js +100 -0
  277. package/dist/services/hooks.js +771 -325
  278. package/dist/services/hybrid-contracts.js +483 -0
  279. package/dist/services/ids.js +29 -1
  280. package/dist/services/installation-lock.js +95 -0
  281. package/dist/services/installed-flow-source.js +183 -0
  282. package/dist/services/lanes.js +78 -65
  283. package/dist/services/lifecycle-command.js +1 -288
  284. package/dist/services/lifecycle-contract.js +1 -0
  285. package/dist/services/lifecycle-invocations.js +2007 -0
  286. package/dist/services/managed-daemon-binding.js +38 -0
  287. package/dist/services/managed-lease-monitor.js +133 -0
  288. package/dist/services/managed-processes.js +475 -55
  289. package/dist/services/memory-accepted-composition.js +77 -0
  290. package/dist/services/memory-baseline-preservation.js +65 -0
  291. package/dist/services/memory-completed-correction-wave.js +320 -0
  292. package/dist/services/memory-compose-handler.js +258 -0
  293. package/dist/services/memory-composition-acceptance.js +199 -0
  294. package/dist/services/memory-composition-graph.js +134 -0
  295. package/dist/services/memory-composition-registration.js +146 -0
  296. package/dist/services/memory-composition-work-custody.js +151 -0
  297. package/dist/services/memory-composition-work-results.js +96 -0
  298. package/dist/services/memory-corrected-review-acceptance.js +129 -0
  299. package/dist/services/memory-corrected-review-batch.js +30 -0
  300. package/dist/services/memory-corrected-review-findings.js +106 -0
  301. package/dist/services/memory-corrected-review-inputs.js +210 -0
  302. package/dist/services/memory-corrected-review-projection.js +78 -0
  303. package/dist/services/memory-corrected-review-receipt.js +170 -0
  304. package/dist/services/memory-corrected-review-registration.js +191 -0
  305. package/dist/services/memory-correction-check-receipt.js +219 -0
  306. package/dist/services/memory-correction-input-owner.js +6 -0
  307. package/dist/services/memory-discover-handler.js +104 -0
  308. package/dist/services/memory-finish-evidence.js +435 -0
  309. package/dist/services/memory-finish-handler.js +215 -0
  310. package/dist/services/memory-finish-integration.js +280 -0
  311. package/dist/services/memory-finish-readiness.js +122 -0
  312. package/dist/services/memory-git-integration.js +151 -0
  313. package/dist/services/memory-historical-corrected-review.js +206 -0
  314. package/dist/services/memory-historical-original-review.js +130 -0
  315. package/dist/services/memory-historical-repair-registration.js +99 -0
  316. package/dist/services/memory-historical-second-review.js +128 -0
  317. package/dist/services/memory-init-bootstrap-readiness.js +210 -0
  318. package/dist/services/memory-init-contexts.js +89 -0
  319. package/dist/services/memory-init-controller-start.js +84 -0
  320. package/dist/services/memory-init-engine-release.js +223 -0
  321. package/dist/services/memory-init-git-policy.js +136 -0
  322. package/dist/services/memory-init-lint-capability.js +185 -0
  323. package/dist/services/memory-init-native-capacity.js +296 -0
  324. package/dist/services/memory-init-preflight.js +330 -0
  325. package/dist/services/memory-init-project-bootstrap.js +599 -0
  326. package/dist/services/memory-init-request-candidate.js +157 -0
  327. package/dist/services/memory-init-request-input.js +66 -0
  328. package/dist/services/memory-init-resource-storage.js +125 -0
  329. package/dist/services/memory-init-run-allocation.js +81 -0
  330. package/dist/services/memory-init-run-bundle.js +98 -0
  331. package/dist/services/memory-init-start.js +181 -0
  332. package/dist/services/memory-init-startup-custody.js +80 -0
  333. package/dist/services/memory-init-startup-input.js +240 -0
  334. package/dist/services/memory-init-storage.js +81 -0
  335. package/dist/services/memory-init-stores.js +359 -0
  336. package/dist/services/memory-init-worktrunk-capability.js +178 -0
  337. package/dist/services/memory-layer-publication.js +611 -0
  338. package/dist/services/memory-layer.js +156 -0
  339. package/dist/services/memory-lint-config.js +85 -0
  340. package/dist/services/memory-native-review-copy.js +165 -0
  341. package/dist/services/memory-normal-flow-entry.js +75 -0
  342. package/dist/services/memory-ordinary-defaults.js +112 -0
  343. package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
  344. package/dist/services/memory-preflight-handler.js +259 -0
  345. package/dist/services/memory-repair-current-view.js +299 -0
  346. package/dist/services/memory-repair-document-delta.js +451 -0
  347. package/dist/services/memory-repair-history.js +491 -0
  348. package/dist/services/memory-repair-physical-binding.js +312 -0
  349. package/dist/services/memory-repair-publication-files.js +152 -0
  350. package/dist/services/memory-repair-publication-intent.js +27 -0
  351. package/dist/services/memory-repair-publication-plan.js +160 -0
  352. package/dist/services/memory-repair-publication-source.js +225 -0
  353. package/dist/services/memory-repair-publication.js +794 -0
  354. package/dist/services/memory-repair-registration.js +239 -0
  355. package/dist/services/memory-repair-work-baseline.js +413 -0
  356. package/dist/services/memory-repair-work-completion.js +97 -0
  357. package/dist/services/memory-repair-work-custody.js +73 -0
  358. package/dist/services/memory-repair-work-results.js +89 -0
  359. package/dist/services/memory-repair-workspace.js +252 -0
  360. package/dist/services/memory-repair-writer-copy.js +735 -0
  361. package/dist/services/memory-result-binding.js +173 -0
  362. package/dist/services/memory-retained-review-authority.js +129 -0
  363. package/dist/services/memory-retained-review-results.js +70 -0
  364. package/dist/services/memory-retained-second-baseline.js +102 -0
  365. package/dist/services/memory-retained-second-repairs.js +156 -0
  366. package/dist/services/memory-review-acceptance.js +214 -0
  367. package/dist/services/memory-review-correction-plan.js +171 -0
  368. package/dist/services/memory-review-handler.js +585 -0
  369. package/dist/services/memory-review-inputs.js +200 -0
  370. package/dist/services/memory-review-receipt.js +180 -0
  371. package/dist/services/memory-review-registration.js +108 -0
  372. package/dist/services/memory-review-work-custody.js +124 -0
  373. package/dist/services/memory-review-work-input.js +72 -0
  374. package/dist/services/memory-review-work-results.js +73 -0
  375. package/dist/services/memory-second-corrected-review-inputs.js +165 -0
  376. package/dist/services/memory-second-correction-plan.js +76 -0
  377. package/dist/services/memory-second-repair-baseline.js +91 -0
  378. package/dist/services/memory-second-repair-launch.js +614 -0
  379. package/dist/services/memory-second-repair-registration.js +88 -0
  380. package/dist/services/memory-second-repair-source.js +187 -0
  381. package/dist/services/memory-second-review-acceptance.js +115 -0
  382. package/dist/services/memory-source-observations.js +117 -0
  383. package/dist/services/memory-stage-dispatch.js +28 -0
  384. package/dist/services/memory-stage-results.js +199 -0
  385. package/dist/services/memory-verification-acceptance.js +135 -0
  386. package/dist/services/memory-verification-checks.js +252 -0
  387. package/dist/services/memory-verification-receipt.js +159 -0
  388. package/dist/services/memory-verify-handler.js +135 -0
  389. package/dist/services/memory-work-result-preparation.js +63 -0
  390. package/dist/services/merge-queue.js +228 -149
  391. package/dist/services/merge-server.js +161 -35
  392. package/dist/services/migrations.js +37 -16
  393. package/dist/services/model-catalog.js +109 -0
  394. package/dist/services/model-discovery.js +46 -0
  395. package/dist/services/native-daemon-history.js +49 -0
  396. package/dist/services/native-session-control.js +39 -0
  397. package/dist/services/operator-profiles.js +236 -0
  398. package/dist/services/plan-runtime.js +26 -11
  399. package/dist/services/plans.js +28 -25
  400. package/dist/services/projects.js +37 -17
  401. package/dist/services/prompts.js +28 -17
  402. package/dist/services/protocols.js +163 -87
  403. package/dist/services/provider-capacity.js +385 -0
  404. package/dist/services/recovery-observation-budget.js +121 -0
  405. package/dist/services/recovery-snapshot-database.js +560 -0
  406. package/dist/services/release-archive-producer.js +52 -0
  407. package/dist/services/release-archive.js +143 -0
  408. package/dist/services/release-cache.js +142 -0
  409. package/dist/services/release-compatibility.js +18 -0
  410. package/dist/services/release-download.js +67 -0
  411. package/dist/services/release-envelope.js +104 -0
  412. package/dist/services/release-manifest.js +45 -0
  413. package/dist/services/release-payload.js +114 -0
  414. package/dist/services/release-producer.js +63 -0
  415. package/dist/services/release-provenance.js +160 -0
  416. package/dist/services/release-registry.js +66 -0
  417. package/dist/services/release-source.js +194 -0
  418. package/dist/services/repair-intents.js +42 -0
  419. package/dist/services/review-copy.js +165 -0
  420. package/dist/services/review-deferral.js +23 -0
  421. package/dist/services/run-control-receipt.js +56 -0
  422. package/dist/services/run-control-worker.js +411 -0
  423. package/dist/services/run-control.js +1056 -0
  424. package/dist/services/run-controller-adapter.js +399 -0
  425. package/dist/services/run-controller-capture.js +186 -0
  426. package/dist/services/run-controller-process.js +208 -0
  427. package/dist/services/run-controller-recovery.js +289 -0
  428. package/dist/services/run-controller-state.js +97 -0
  429. package/dist/services/run-controller.js +1205 -0
  430. package/dist/services/run-engine-bindings.js +1 -1
  431. package/dist/services/run-fork.js +147 -0
  432. package/dist/services/run-health.js +149 -0
  433. package/dist/services/run-observations.js +152 -0
  434. package/dist/services/run-projection.js +22 -64
  435. package/dist/services/run-recovery-guard.js +77 -0
  436. package/dist/services/run-recovery-runtime.js +112 -0
  437. package/dist/services/run-recovery.js +489 -0
  438. package/dist/services/run-start-custody.js +177 -0
  439. package/dist/services/runs.js +1019 -247
  440. package/dist/services/runtime-budget.js +430 -0
  441. package/dist/services/runtime-command.js +43 -0
  442. package/dist/services/runtime-scope-capture.js +52 -0
  443. package/dist/services/runtime-scope-control.js +468 -0
  444. package/dist/services/runtime-scope-resume.js +599 -0
  445. package/dist/services/runtime-scope-stop.js +210 -0
  446. package/dist/services/runtime-scope-worker.js +287 -0
  447. package/dist/services/runtime-service.js +96 -0
  448. package/dist/services/schema-validation.js +10 -8
  449. package/dist/services/scope-writer-cleanup.js +27 -0
  450. package/dist/services/sessions.js +35 -69
  451. package/dist/services/setup.js +377 -0
  452. package/dist/services/stage-blocker.js +17 -7
  453. package/dist/services/stage-context.js +193 -19
  454. package/dist/services/stage-handlers.js +74 -0
  455. package/dist/services/stage-lifecycle.js +100 -108
  456. package/dist/services/stage-pause.js +89 -75
  457. package/dist/services/stage-report-renderer.js +234 -6
  458. package/dist/services/stage-work-graph.js +52 -0
  459. package/dist/services/tooling.js +32 -23
  460. package/dist/services/usage.js +49 -101
  461. package/dist/services/verification-matrix.js +374 -0
  462. package/dist/services/vnext-code-review.js +223 -88
  463. package/dist/services/vnext-code.js +327 -226
  464. package/dist/services/vnext-contracts.js +10 -1
  465. package/dist/services/vnext-execution-profile.js +80 -5
  466. package/dist/services/vnext-fanout.js +311 -19
  467. package/dist/services/vnext-merge.js +332 -111
  468. package/dist/services/vnext-plan-review.js +279 -127
  469. package/dist/services/vnext-plan.js +229 -104
  470. package/dist/services/vnext-protocolize.js +411 -86
  471. package/dist/services/vnext-specify.js +119 -81
  472. package/dist/services/vnext-workspace-policy.js +7 -2
  473. package/dist/services/work-registry.js +1627 -199
  474. package/dist/services/workspace-bootstrap.js +52 -15
  475. package/dist/services/workspace-files.js +85 -0
  476. package/dist/services/worktrees.js +98 -44
  477. package/dist/shared/entity-references.js +8 -0
  478. package/dist/shared/errors.js +17 -0
  479. package/dist/storage/agent-output.js +26 -0
  480. package/dist/storage/database.js +891 -39
  481. package/dist/storage/foreign-key-transaction.js +39 -0
  482. package/dist/storage/immutable-input.js +45 -0
  483. package/dist/storage/import-guard.js +102 -0
  484. package/dist/storage/native-cleanup.js +52 -0
  485. package/dist/storage/paths.js +17 -4
  486. package/dist/storage/projection-lock.js +73 -0
  487. package/dist/storage/work-references.js +23 -0
  488. package/dist/storage/writer-contract.js +89 -0
  489. package/dist/storage/writer-migration.js +85 -0
  490. package/package.json +43 -13
  491. package/tools/create-canon-release.mjs +33 -0
  492. package/tools/produce-hybrid-release.mjs +41 -0
  493. package/tools/repair-paused-run-status.mjs +84 -0
@@ -1,25 +1,97 @@
1
1
  import crypto from "node:crypto";
2
+ import { candidateFailureFiles, checkRepairDisposition, readRunnerEvidence, readRunnerProgress, runnerContractsValid, runnerEvidencePass, runnerObservationDurations } from "./check-runner-evidence.js";
3
+ export { assertSourceRepairEligible, checkRepairDisposition } from "./check-runner-evidence.js";
4
+ import { isGitlessWorkspace, ownedWorkspaceHookPath, projectServicePathFilter, workspaceGitFiles } from "./workspace-files.js";
2
5
  import { spawn, spawnSync } from "node:child_process";
6
+ import { ObservationClock, validateObservationDuration } from "../harness-runtime/lib/observation-clock.mjs";
3
7
  import fs from "node:fs";
4
8
  import path from "node:path";
5
- import { AppError } from "../shared/errors.js";
6
- import { confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessStatus, managedOwnerIsAlive, processTreeIsAlive, registerManagedProcess, reservePorts } from "./managed-processes.js";
9
+ import { isDeepStrictEqual } from "node:util";
10
+ import { parse } from "shell-quote";
11
+ import { assertRunMutationAllowed } from "./run-recovery.js";
12
+ import { AppError, errorRecord } from "../shared/errors.js";
13
+ import { stopProcessGroup } from "../harness-runtime/lib/managed-daemon.mjs";
14
+ import { createRenewalState, maintenanceRetryable, renewalConfirmed, renewalDelay, renewalFailure, renewalRemaining, RENEWAL_POLICY } from "../harness-runtime/lib/lease-renewal.mjs";
15
+ import { confirmManagedProcess, finishManagedProcess, renewManagedProcess, managedProcessStatus, managedProcessRuntimeHome, managedOwnerIsAlive, processTreeIsAlive, registerManagedProcess, reservePorts } from "./managed-processes.js";
16
+ import { admitRuntimeCheck, runtimeBudgetForRun } from "./runtime-budget.js";
17
+ import { getResourceDatabase } from "../storage/database.js";
18
+ import { readBoundedReleaseFile } from "./release-source.js";
7
19
  const profileRelativePath = path.join(".memory-bank", "spec", "engineering", "code-check-profile.json");
8
20
  // Receipt reservation precedes child spawn so an interrupted CLI leaves an
9
21
  // auditable attempt. A second caller must not mistake that small handoff window
10
22
  // for a dead check and overwrite its outcome.
11
23
  const receiptStartingGraceMs = 30_000;
12
- export function codeExecutionEnvironment(workspaceRoot) { const suffix = crypto.createHash("sha256").update(path.resolve(workspaceRoot)).digest("hex").slice(0, 12); return { ...process.env, DD_FLOW_LOCAL_DATABASE_SUFFIX: suffix }; }
24
+ /** The check writer proves an empty tree before marking its registration terminal. */
25
+ export function terminalCheckSettled(context, record) {
26
+ if (record.kind !== "check" || !record.check_id || record.owner_id !== `check:${record.check_id}` || !record.project_id || !record.run_id
27
+ || !record.pid || !record.pid_started_at || !["stopped", "failed"].includes(record.state) || !record.finished_at)
28
+ return null;
29
+ let metadata;
30
+ try {
31
+ metadata = JSON.parse(record.metadata_json);
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ if (metadata.role !== "check" || managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
37
+ return null;
38
+ let resource;
39
+ try {
40
+ resource = getResourceDatabase(context.env.DD_FLOW_RESOURCE_HOME ?? context.ddFlowHome, "read_existing");
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ try {
46
+ const current = resource.get("SELECT * FROM managed_processes WHERE id = ?", [record.id]);
47
+ if (!current || current.lease_token !== record.lease_token || current.owner_id !== record.owner_id || current.pid !== record.pid
48
+ || current.pid_started_at !== record.pid_started_at || current.metadata_json !== record.metadata_json || current.state !== record.state
49
+ || current.project_id !== record.project_id || current.run_id !== record.run_id || current.work_id !== record.work_id
50
+ || current.check_id !== record.check_id || current.finished_at !== record.finished_at)
51
+ return null;
52
+ const receipt = context.db.get("SELECT status, finished_at, work_id FROM check_receipts WHERE id = ? AND project_id = ? AND run_id = ?", [record.check_id, record.project_id, record.run_id]);
53
+ return receipt && receipt.work_id === record.work_id && ["passed", "failed", "aborted"].includes(receipt.status) && receipt.finished_at
54
+ ? { status: receipt.status, finished_at: receipt.finished_at } : null;
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ finally {
60
+ resource.close?.();
61
+ }
62
+ }
63
+ export function codeExecutionEnvironment(workspaceRoot, checkEnvironment) {
64
+ if (checkEnvironment !== undefined && (!checkEnvironment || typeof checkEnvironment !== "object" || Array.isArray(checkEnvironment)
65
+ || Object.keys(checkEnvironment).length !== 1 || typeof checkEnvironment.CODEX_HOME !== "string"
66
+ || checkEnvironment.CODEX_HOME.length > 4096 || checkEnvironment.CODEX_HOME.includes("\0") || !path.isAbsolute(checkEnvironment.CODEX_HOME)))
67
+ throw new AppError("invalid_check_environment", "Owned check environment must name one absolute Codex home", 2, { effect: "no_effect" });
68
+ const suffix = crypto.createHash("sha256").update(path.resolve(workspaceRoot)).digest("hex").slice(0, 12);
69
+ // The managed CLI's Node must win over a provider app's PATH: a Rosetta
70
+ // provider can otherwise run x64 checks against arm64-installed modules.
71
+ const environment = { ...process.env, PATH: `${path.dirname(process.execPath)}${path.delimiter}${process.env.PATH ?? ""}`, DD_FLOW_LOCAL_DATABASE_SUFFIX: suffix };
72
+ delete environment.NODE_PATH;
73
+ for (const key of ["DD_FLOW_CHECK_RUNNER_EVIDENCE_PATH", "DD_FLOW_CHECK_RUNNER_PROGRESS_PATH", "DD_FLOW_CHECK_INVOCATION_ID", "DD_FLOW_CHECK_ID", "DD_FLOW_CHECK_INPUT_HASH", "DD_FLOW_CHECK_VERIFICATION_EPOCH", "DD_FLOW_CHECK_COMMAND"])
74
+ delete environment[key];
75
+ if (checkEnvironment)
76
+ environment.CODEX_HOME = checkEnvironment.CODEX_HOME;
77
+ return environment;
78
+ }
79
+ export function workspaceOperationId(workspaceRoot) { return `workspace:${crypto.createHash("sha256").update(path.resolve(workspaceRoot)).digest("hex")}`; }
13
80
  export function readCodeCheckProfile(workspaceRoot) {
14
81
  const file = path.join(workspaceRoot, profileRelativePath);
15
82
  if (!fs.existsSync(file))
16
83
  return { profile: null, hash: null, file };
17
84
  const bytes = fs.readFileSync(file);
18
85
  const value = JSON.parse(bytes.toString("utf8"));
19
- if (value.schema_id !== "dd-flow/code-check-profile@6")
20
- throw new AppError("invalid_code_check_profile", "CODE check profile has an unsupported schema", 2, { file, schema_id: value.schema_id ?? null, required: "dd-flow/code-check-profile@6" });
21
- if (!isStringRecord(value.aliases) || !isStringRecord(value.require_alias_for) || !isGateRecord(value.mandatory_by_gate) || !isPortAliasRecord(value.ports_by_alias))
22
- throw new AppError("invalid_code_check_profile", "CODE check profile aliases, require_alias_for, mandatory_by_gate or ports_by_alias are invalid", 2, { file });
86
+ if (!["dd-flow/code-check-profile@6", "dd-flow/code-check-profile@7"].includes(value.schema_id ?? ""))
87
+ throw new AppError("invalid_code_check_profile", "CODE check profile has an unsupported schema", 2, { file, schema_id: value.schema_id ?? null, supported: ["dd-flow/code-check-profile@6", "dd-flow/code-check-profile@7"] });
88
+ if (!runnerContractsValid(value.runner_evidence_by_alias) || value.schema_id === "dd-flow/code-check-profile@6" && value.runner_evidence_by_alias !== undefined)
89
+ throw new AppError("invalid_code_check_profile", "Runner evidence requires valid profile@7 relative artifact contracts", 2, { file });
90
+ for (const alias of Object.keys(value.runner_evidence_by_alias ?? {}))
91
+ if (!value.aliases?.[alias])
92
+ throw new AppError("invalid_code_check_profile", "Runner evidence references an unknown alias", 2, { file, alias });
93
+ if (!isStringRecord(value.aliases) || !isStringRecord(value.require_alias_for) || !isGateRecord(value.mandatory_by_gate) || !isPortAliasRecord(value.ports_by_alias) || !isInputAliasRecord(workspaceRoot, value.inputs_by_alias))
94
+ throw new AppError("invalid_code_check_profile", "CODE check profile aliases, require_alias_for, mandatory_by_gate, ports_by_alias or inputs_by_alias are invalid", 2, { file });
23
95
  for (const [gate, aliases] of Object.entries(value.mandatory_by_gate))
24
96
  for (const alias of aliases ?? [])
25
97
  if (!value.aliases[alias])
@@ -27,19 +99,76 @@ export function readCodeCheckProfile(workspaceRoot) {
27
99
  for (const alias of Object.keys(value.ports_by_alias ?? {}))
28
100
  if (!value.aliases[alias])
29
101
  throw new AppError("invalid_code_check_profile", "A port declaration references an unknown alias", 2, { file, alias });
102
+ for (const alias of Object.keys(value.inputs_by_alias ?? {}))
103
+ if (!value.aliases[alias])
104
+ throw new AppError("invalid_code_check_profile", "An input declaration references an unknown alias", 2, { file, alias });
30
105
  return { profile: value, hash: crypto.createHash("sha256").update(bytes).digest("hex"), file };
31
106
  }
107
+ /**
108
+ * Planned aliases are qualification-owned extensions: their provider Work is
109
+ * expected to materialize them after PLAN. Freeze the project policy and
110
+ * existing aliases, while still validating every planned alias definition at
111
+ * execution time. Unknown edits and changes to existing policy remain part
112
+ * of the hash and therefore fail closed.
113
+ */
114
+ export function codeCheckProfileHash(workspaceRoot, checks = []) {
115
+ const { profile } = readCodeCheckProfile(workspaceRoot);
116
+ if (!profile)
117
+ return null;
118
+ const planned = new Set(checks.filter(check => check.availability === "planned" && check.command.startsWith("@check/")).map(check => check.command));
119
+ const serialize = (value) => profile.schema_id === "dd-flow/code-check-profile@7"
120
+ ? JSON.stringify(value, (_key, item) => item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).sort(([left], [right]) => left.localeCompare(right))) : item)
121
+ : JSON.stringify(value);
122
+ if (!planned.size)
123
+ return crypto.createHash("sha256").update(serialize(profile)).digest("hex");
124
+ const canonical = {
125
+ ...profile,
126
+ aliases: Object.fromEntries(Object.entries(profile.aliases).filter(([alias]) => !planned.has(alias))),
127
+ ports_by_alias: Object.fromEntries(Object.entries(profile.ports_by_alias ?? {}).filter(([alias]) => !planned.has(alias))),
128
+ inputs_by_alias: Object.fromEntries(Object.entries(profile.inputs_by_alias ?? {}).filter(([alias]) => !planned.has(alias))),
129
+ ...(profile.schema_id === "dd-flow/code-check-profile@7" ? { runner_evidence_by_alias: Object.fromEntries(Object.entries(profile.runner_evidence_by_alias ?? {}).filter(([alias]) => !planned.has(alias))) } : {})
130
+ };
131
+ return crypto.createHash("sha256").update(serialize(canonical)).digest("hex");
132
+ }
133
+ /** The integration target starts at its baseline profile; the reviewed source
134
+ * may add aliases, but must not change existing check policy or bindings. */
135
+ export function mergeCheckProfileHash(sourceWorkspace, targetWorkspace, checks) {
136
+ const source = readCodeCheckProfile(sourceWorkspace).profile;
137
+ const target = readCodeCheckProfile(targetWorkspace).profile;
138
+ if (!source && !target)
139
+ return null;
140
+ if (!source || !target)
141
+ throw new AppError("check_profile_drift", "Source and integration check profiles differ", 1);
142
+ const retained = structuredClone(source);
143
+ for (const field of ["aliases", "ports_by_alias", "inputs_by_alias", "runner_evidence_by_alias"]) {
144
+ const old = target[field];
145
+ if (!old) {
146
+ delete retained[field];
147
+ continue;
148
+ }
149
+ const current = source[field] ?? {};
150
+ retained[field] = Object.fromEntries(Object.keys(old).filter(key => key in current).map(key => [key, current[key]]));
151
+ }
152
+ const existingAliases = new Set(Object.keys(target.aliases));
153
+ const changedExistingMetadata = ["ports_by_alias", "inputs_by_alias", "runner_evidence_by_alias"].some(field => Object.keys(source[field] ?? {}).some(alias => existingAliases.has(alias) && !(alias in (target[field] ?? {}))));
154
+ if (changedExistingMetadata || !isDeepStrictEqual(retained, target)) {
155
+ throw new AppError("check_profile_drift", "Source check profile changes existing integration policy", 1, { source_profile_hash: codeCheckProfileHash(sourceWorkspace, checks), target_profile_hash: codeCheckProfileHash(targetWorkspace, checks) });
156
+ }
157
+ return codeCheckProfileHash(sourceWorkspace, checks);
158
+ }
32
159
  export function validateCodeCheckCommands(workspaceRoot, commands) {
33
160
  const { profile, file } = readCodeCheckProfile(workspaceRoot);
34
- if (!profile)
35
- return [...commands];
36
- return commands.map((command) => { if (command.startsWith("@check/")) {
37
- if (!profile.aliases[command]?.trim())
38
- throw new AppError("unknown_code_check_alias", "CODE check alias is not defined by the project profile", 2, { alias: command, file });
161
+ return commands.map((command) => {
162
+ if (command.startsWith("@check/")) {
163
+ if (!profile?.aliases[command]?.trim())
164
+ throw new AppError("unknown_code_check_alias", "CODE check alias is not defined by the project profile", 2, { alias: command, file });
165
+ return command;
166
+ }
167
+ for (const [prefix, alias] of Object.entries(profile?.require_alias_for ?? {}))
168
+ if (command === prefix || command.startsWith(`${prefix} `))
169
+ throw new AppError("raw_code_check_forbidden", "CODE check must use the project alias instead of a raw guarded command", 2, { command, required_alias: alias, file });
39
170
  return command;
40
- } for (const [prefix, alias] of Object.entries(profile.require_alias_for))
41
- if (command === prefix || command.startsWith(`${prefix} `))
42
- throw new AppError("raw_code_check_forbidden", "CODE check must use the project alias instead of a raw guarded command", 2, { command, required_alias: alias, file }); return command; });
171
+ });
43
172
  }
44
173
  export function resolveCodeCheckCommands(workspaceRoot, runId, commands) {
45
174
  const validated = validateCodeCheckCommands(workspaceRoot, commands);
@@ -50,7 +179,11 @@ export function resolveCodeCheckCommands(workspaceRoot, runId, commands) {
50
179
  }
51
180
  export function projectPolicyCheckDeclarations(workspaceRoot, gate) {
52
181
  const { profile } = readCodeCheckProfile(workspaceRoot);
53
- return (profile?.mandatory_by_gate[gate] ?? []).map((alias) => { const ref = `POLICY/${gate}/${alias.slice(7).replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "").toLowerCase()}`; return { id: ref, canonical_ref: ref, source: "project_policy", command: alias, purpose: `Mandatory project ${gate} gate.`, run_at: gate, availability: "available", ...(profile?.ports_by_alias?.[alias] ? { ports: profile.ports_by_alias[alias] } : {}) }; });
182
+ return projectPolicyCheckDeclarationsFromProfile(profile, gate);
183
+ }
184
+ /** The caller may already have independently admitted exact profile bytes. */
185
+ export function projectPolicyCheckDeclarationsFromProfile(profile, gate) {
186
+ return (profile?.mandatory_by_gate[gate] ?? []).map((alias) => { const ref = `POLICY/${gate}/${alias.slice(7).replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "").toLowerCase()}`; return { id: ref, canonical_ref: ref, source: "project_policy", command: alias, purpose: `Mandatory project ${gate} gate.`, run_at: gate, availability: "available", ...(profile?.ports_by_alias?.[alias] ? { ports: profile.ports_by_alias[alias] } : {}), ...(profile?.inputs_by_alias?.[alias] ? { inputs: profile.inputs_by_alias[alias] } : {}) }; });
54
187
  }
55
188
  export function effectiveCheckDeclarations(workspaceRoot, declared, gates) { return gates.flatMap((gate) => [...declared.filter((check) => check.run_at === gate), ...projectPolicyCheckDeclarations(workspaceRoot, gate)]); }
56
189
  export function aggregateCheckDeclarations(workspaceRoot) { return projectPolicyCheckDeclarations(workspaceRoot, "code"); }
@@ -60,12 +193,21 @@ export function validateCheckPlacement(workspaceRoot, checks) { const { profile
60
193
  if (check.run_at === "work" && aggregateCommands.has(check.command))
61
194
  throw new AppError("aggregate_check_requires_code_gate", "A project aggregate check must run at CODE or readiness, not inside one scoped Work", 2, { check_id: check.id, command: check.command, run_at: check.run_at }); }
62
195
  export function validateCheckDeclaration(workspaceRoot, check) {
63
- if (check.run_at === "external")
64
- return;
196
+ if ((check.required_artifacts ?? []).some(file => path.posix.normalize(file.replaceAll("\\", "/")).split("/")[0] === "engine-outcome.json"))
197
+ throw new AppError("invalid_code_check_profile", "engine-outcome.json is reserved for engine execution evidence, not a candidate artifact", 2, { check_id: check.id });
198
+ if (!["work", "code", "readiness", "merge", "release"].includes(check.run_at))
199
+ throw new AppError("unsupported_check_gate", "Check requires an autonomous executable gate; replan the incompatible declaration", 2, { check_id: check.id, run_at: check.run_at });
200
+ if (check.runner_evidence !== undefined && !runnerContractsValid({ "@check/declaration": check.runner_evidence }))
201
+ throw new AppError("invalid_code_check_profile", "Check runner evidence contract is invalid", 2, { check_id: check.id });
65
202
  const { profile } = readCodeCheckProfile(workspaceRoot);
203
+ const resolvedCommand = profile?.aliases[check.command] ?? check.command;
204
+ validateTerminatingCheckCommand(resolvedCommand, check.id);
66
205
  if (check.availability === "planned" && check.command.startsWith("@check/") && !profile?.aliases[check.command])
67
206
  throw new AppError("planned_check_not_materialized", "A planned check alias was not materialized by its provider Work", 2, { check_id: check.id, command: check.command, provider: check.provided_by ?? null });
68
207
  validateCodeCheckCommands(workspaceRoot, [check.command]);
208
+ if (check.availability === "available" && !check.command.startsWith("@check/"))
209
+ assertDirectCheckExecutable(workspaceRoot, check.command, check.id);
210
+ validateCheckInputs(workspaceRoot, check.inputs ?? []);
69
211
  if (check.command.startsWith("@check/") && check.definition && profile?.aliases[check.command] !== check.definition)
70
212
  throw new AppError("check_definition_drift", "The accepted check alias definition differs from the current project profile", 2, { check_id: check.id, alias: check.command, expected: check.definition, actual: profile?.aliases[check.command] ?? null });
71
213
  if (check.availability !== "planned")
@@ -77,36 +219,379 @@ export function validateCheckDeclaration(workspaceRoot, check) {
77
219
  if (!profile?.aliases[check.command])
78
220
  throw new AppError("planned_check_not_materialized", "A planned check alias was not materialized by its provider Work", 2, { check_id: check.id, command: check.command, provider: check.provided_by ?? null });
79
221
  }
222
+ export function validateTerminatingCheckCommand(command, checkId) {
223
+ if (isBareServiceScript(command))
224
+ throw new AppError("non_terminating_check", "A check must terminate; run a finite test instead of a bare dev/start/serve/watch service script", 2, { check_id: checkId, command });
225
+ }
226
+ function isBareServiceScript(command) {
227
+ const words = parse(command.trim());
228
+ if (words.some((word) => typeof word !== "string"))
229
+ return false;
230
+ let args = words;
231
+ while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(args[0] ?? ""))
232
+ args = args.slice(1);
233
+ const manager = path.basename(args.shift() ?? "");
234
+ if (!["pnpm", "npm", "yarn", "bun"].includes(manager))
235
+ return false;
236
+ while (args.length && ["--filter", "-F", "--dir", "-C", "--cwd"].includes(args[0]))
237
+ args.splice(0, 2);
238
+ if (args[0] === "run")
239
+ args.shift();
240
+ return ["dev", "start", "serve", "watch"].includes(args[0] ?? "");
241
+ }
242
+ /** Reject a plan-time typo or a fictitious "manual" command before it becomes
243
+ * a late shell exit. Compound shell programs remain valid: only a simple,
244
+ * directly named executable is preflighted here. */
245
+ function assertDirectCheckExecutable(workspaceRoot, command, checkId) {
246
+ const trimmed = command.trim();
247
+ // A shell operator or expansion makes the launch path dynamic. Do not
248
+ // guess there; this preflight is deliberately limited to a direct program
249
+ // with ordinary arguments and optional literal environment assignments.
250
+ if (/[|&;<>()`$\n\r~]/.test(trimmed))
251
+ return;
252
+ const parsed = parse(trimmed);
253
+ if (parsed.some(word => typeof word !== "string"))
254
+ return;
255
+ const words = parsed;
256
+ let index = 0;
257
+ const env = codeExecutionEnvironment(workspaceRoot);
258
+ while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[index] ?? "")) {
259
+ const assignment = words[index++];
260
+ const equals = assignment.indexOf("=");
261
+ env[assignment.slice(0, equals)] = assignment.slice(equals + 1);
262
+ }
263
+ const program = words[index];
264
+ if (!program)
265
+ return;
266
+ const probe = spawnSync("/bin/sh", ["-c", "command -v -- \"$1\" >/dev/null", "dd-flow-check", program], { cwd: workspaceRoot, env, encoding: "utf8", timeout: 5000 });
267
+ if (probe.error || probe.signal)
268
+ throw new AppError("check_command_probe_unavailable", "Executable probe could not be observed; availability is unknown", 2, { check_id: checkId, executable: program, cause: probe.error?.message, signal: probe.signal });
269
+ if (probe.status !== 0)
270
+ throw new AppError("check_command_unavailable", "Available CODE check names an executable that is unavailable in this environment", 2, { check_id: checkId, command, executable: program });
271
+ }
272
+ // An owning handler supplies this identity; it is a reuse boundary, not an
273
+ // authority proof. Omitting it preserves the historical SDLC input hash.
274
+ function validateCheckEvidenceContext(value) {
275
+ if (value !== undefined && (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9:._-]{0,255}$/.test(value)))
276
+ throw new AppError("check_evidence_context_invalid", "Check evidence context must be a bounded opaque identifier", 2, { effect: "no_effect" });
277
+ }
278
+ const evidenceDigest = (value) => crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
279
+ /** Prepares current physical evidence outside a writer and retains an exact
280
+ * SQL-only fence. Declarations must come from the owning handler/policy, never
281
+ * from the result whose acceptance is being decided. This does not supply
282
+ * semantic acceptance or choose a memory verification policy.
283
+ */
284
+ export class PreparedCheckEvidence {
285
+ #input;
286
+ #snapshot;
287
+ #generation;
288
+ #references;
289
+ #artifacts;
290
+ #completions = [];
291
+ #passed;
292
+ constructor(input, snapshot, generation, references, artifacts, passed) {
293
+ this.#input = structuredClone({ projectId: input.projectId, runId: input.runId, runHome: input.runHome, workspaceRoot: input.workspaceRoot, scope: input.scope, checks: input.checks,
294
+ ...(input.mode === undefined ? {} : { mode: input.mode }), ...(input.workId === undefined ? {} : { workId: input.workId }), ...(input.expectedProfileHash === undefined ? {} : { expectedProfileHash: input.expectedProfileHash }), ...(input.evidenceContext === undefined ? {} : { evidenceContext: input.evidenceContext }), ...(input.checkEnvironment === undefined ? {} : { checkEnvironment: input.checkEnvironment }) });
295
+ this.#snapshot = snapshot;
296
+ this.#generation = generation;
297
+ this.#references = references;
298
+ this.#artifacts = artifacts;
299
+ this.#passed = passed;
300
+ Object.freeze(this);
301
+ }
302
+ static prepare(context, input) {
303
+ validateCheckEvidenceContext(input.evidenceContext);
304
+ const fail = () => { throw new AppError("check_evidence_stale", "Current declared checks lack owned passed evidence", 2, { effect: "no_effect" }); };
305
+ if (!input.checks.length || (input.scope === "work") !== Boolean(input.workId) || input.mode !== undefined && input.mode !== "diagnostic")
306
+ fail();
307
+ const generation = assertRunMutationAllowed(context, input.projectId, input.runId);
308
+ const snapshot = checkEvidenceSqlSnapshot(context, input);
309
+ const selected = JSON.parse(snapshot);
310
+ if (!selected.run || fs.realpathSync.native(selected.run.workspace_root) !== fs.realpathSync.native(input.workspaceRoot)
311
+ || fs.realpathSync.native(selected.run.run_root ?? selected.run.run_dir) !== fs.realpathSync.native(input.runHome))
312
+ fail();
313
+ const rows = selected.receipts;
314
+ const root = fs.realpathSync.native(input.runHome);
315
+ for (const row of rows) {
316
+ const relative = path.relative(path.resolve(input.runHome), row.receipt_path);
317
+ if (!path.isAbsolute(row.receipt_path) || relative.startsWith(`..${path.sep}`) || relative === ".." || path.isAbsolute(relative))
318
+ fail();
319
+ if (fs.existsSync(row.receipt_path)) {
320
+ const physical = path.relative(root, fs.realpathSync.native(row.receipt_path));
321
+ if (physical.startsWith(`..${path.sep}`) || physical === ".." || path.isAbsolute(physical))
322
+ fail();
323
+ }
324
+ }
325
+ for (const check of input.checks)
326
+ validateCheckDeclaration(input.workspaceRoot, check);
327
+ const { profile } = readCodeCheckProfile(input.workspaceRoot), profileHash = codeCheckProfileHash(input.workspaceRoot, input.checks);
328
+ if (input.expectedProfileHash !== undefined && profileHash !== input.expectedProfileHash)
329
+ fail();
330
+ const checks = input.checks.map(check => {
331
+ const runner = profile?.runner_evidence_by_alias?.[check.command] ?? check.runner_evidence;
332
+ return { ...check, ports: check.ports ?? profile?.ports_by_alias?.[check.command] ?? [], inputs: check.inputs ?? profile?.inputs_by_alias?.[check.command] ?? [],
333
+ ...(runner ? { runner_evidence: runner, required_artifacts: [...new Set([...(check.required_artifacts ?? []), runner.path])] } : {}) };
334
+ });
335
+ const commands = resolveCodeCheckCommands(input.workspaceRoot, input.runId, checks.map(check => check.command));
336
+ assertConsistentDeclarations(checks, commands);
337
+ const state = workspaceState(input.workspaceRoot, [input.runHome]);
338
+ const receipts = rows.map(projectCheckReceiptRow);
339
+ const artifacts = [], completions = [];
340
+ let passed = true;
341
+ const references = deduplicate(checks, commands).map(group => {
342
+ const inputHash = checkInputHash(input.workspaceRoot, group, profileHash, workspaceInputFingerprint(input.workspaceRoot, state.files, group.inputs, [input.runHome]), input.evidenceContext, input.checkEnvironment);
343
+ const receipt = receipts.filter(item => item.input_hash === inputHash && item.profile_hash === profileHash && item.scope === input.scope && item.work_id === (input.workId ?? null)).at(-1);
344
+ if (!receipt || !checkReceiptEvidenceValidForMode(bindReceipt(receipt, group), input.mode))
345
+ fail();
346
+ passed = passed && receipt.status === "passed";
347
+ for (const artifact of receipt.artifacts)
348
+ artifacts.push({ root: "run", path: path.relative(input.runHome, path.join(path.dirname(receipt.receipt_path), "artifacts", artifact.path)).split(path.sep).join("/"), sha256: artifact.sha256 });
349
+ const bytes = fs.readFileSync(receipt.receipt_path);
350
+ completions.push({ root: "run", path: path.relative(input.runHome, receipt.completion_path).split(path.sep).join("/"), sha256: crypto.createHash("sha256").update(fs.readFileSync(receipt.completion_path)).digest("hex") });
351
+ return { id: receipt.id, path: receipt.receipt_path, sha256: crypto.createHash("sha256").update(bytes).digest("hex") };
352
+ });
353
+ const prepared = new PreparedCheckEvidence(input, snapshot, generation, references, artifacts, passed);
354
+ prepared.#completions = completions;
355
+ prepared.assertCustody(context);
356
+ return prepared;
357
+ }
358
+ references() { return structuredClone(this.#references); }
359
+ artifactReferences() { return structuredClone(this.#artifacts); }
360
+ sourceReferences() { return [...this.artifactReferences(), ...structuredClone(this.#completions), ...this.resultReferences().map(reference => ({ root: "run", ...reference }))]; }
361
+ qualityPassed() { return this.#passed; }
362
+ resultReferences() {
363
+ return this.#references.map(reference => ({ path: path.relative(this.#input.runHome, reference.path).split(path.sep).join("/"), sha256: reference.sha256 }));
364
+ }
365
+ /** For an owning acceptance writer to retain independently of agent bytes.
366
+ * The row digest pins historical execution, not later workspace policy.
367
+ */
368
+ retainedReferences(context) {
369
+ return context.db.readSnapshot(() => {
370
+ this.assertCustody(context);
371
+ return this.#references.map(reference => ({ id: reference.id, path: path.relative(this.#input.runHome, reference.path).split(path.sep).join("/"), sha256: reference.sha256,
372
+ sql_sha256: evidenceDigest(context.db.get("SELECT * FROM check_receipts WHERE project_id=? AND run_id=? AND id=?", [this.#input.projectId, this.#input.runId, reference.id])) }));
373
+ });
374
+ }
375
+ assertOwner(scope) {
376
+ if (scope.projectId !== this.#input.projectId || scope.runId !== this.#input.runId || scope.scope !== undefined && scope.scope !== this.#input.scope)
377
+ throw new AppError("check_receipt_changed", "Prepared checks belong to another RUN or scope", 2, { effect: "no_effect" });
378
+ }
379
+ assertCustody(context) {
380
+ assertRunMutationAllowed(context, this.#input.projectId, this.#input.runId, this.#generation);
381
+ if (checkEvidenceSqlSnapshot(context, this.#input) !== this.#snapshot)
382
+ throw new AppError("check_receipt_changed", "Prepared check SQL custody changed", 2, { effect: "no_effect" });
383
+ }
384
+ revalidate(context) {
385
+ const current = PreparedCheckEvidence.prepare(context, this.#input);
386
+ if (!isDeepStrictEqual(current.sourceReferences(), this.sourceReferences()) || !isDeepStrictEqual(current.#references, this.#references) || current.#snapshot !== this.#snapshot)
387
+ throw new AppError("check_evidence_stale", "Prepared check evidence changed", 2, { effect: "no_effect" });
388
+ this.assertCustody(context);
389
+ }
390
+ }
391
+ /** Historical aggregate execution proof. Selectors must come from an owning
392
+ * committed acceptance receipt, not a proposed result. This reader does not
393
+ * select current declarations, inspect the current workspace or execute checks.
394
+ * Relocation of SQL path fields still requires explicit receipt projection.
395
+ */
396
+ export class PreparedRetainedCheckEvidence {
397
+ #input;
398
+ #snapshot;
399
+ #generation;
400
+ constructor(input, snapshot, generation) {
401
+ this.#input = structuredClone(input);
402
+ this.#snapshot = snapshot;
403
+ this.#generation = generation;
404
+ Object.freeze(this);
405
+ }
406
+ static prepare(context, input) {
407
+ const fail = () => { throw new AppError("retained_check_evidence_invalid", "Accepted checks lack exact historical execution proof", 2, { effect: "no_effect" }); };
408
+ if (!Array.isArray(input.references) || !input.references.length || input.references.length > 4096
409
+ || new Set(input.references.map(reference => reference?.id)).size !== input.references.length
410
+ || input.references.some(reference => !reference || typeof reference.id !== "string" || !reference.id
411
+ || typeof reference.path !== "string" || !/^[a-f0-9]{64}$/.test(reference.sha256) || !/^[a-f0-9]{64}$/.test(reference.sql_sha256)))
412
+ fail();
413
+ const generation = assertRunMutationAllowed(context, input.projectId, input.runId), snapshot = retainedCheckSqlSnapshot(context, input);
414
+ const selected = JSON.parse(snapshot);
415
+ if (!selected.run || selected.receipts.some(row => !row))
416
+ fail();
417
+ const runRoot = selected.run.run_root ?? selected.run.run_dir;
418
+ if (!path.isAbsolute(runRoot) || fs.realpathSync.native(runRoot) !== path.resolve(runRoot))
419
+ fail();
420
+ for (let index = 0; index < input.references.length; index++) {
421
+ const reference = input.references[index], row = selected.receipts[index];
422
+ if (row.scope !== "aggregate" || row.work_id !== null || evidenceDigest(row) !== reference.sql_sha256
423
+ || row.receipt_path !== path.join(runRoot, reference.path))
424
+ fail();
425
+ // Admit contained regular receipt/completion files before any helper
426
+ // reads them; escaping paths and symlink parents are rejected.
427
+ const bytes = readRetainedCheckFile(runRoot, reference.path);
428
+ readRetainedCheckFile(runRoot, path.posix.join(path.posix.dirname(reference.path), "completion.json"));
429
+ if (crypto.createHash("sha256").update(bytes).digest("hex") !== reference.sha256 || !checkReceiptEvidenceValidForMode(projectCheckReceiptRow(row), input.mode))
430
+ fail();
431
+ }
432
+ const prepared = new PreparedRetainedCheckEvidence(input, snapshot, generation);
433
+ prepared.assertCustody(context);
434
+ return prepared;
435
+ }
436
+ assertCustody(context) {
437
+ assertRunMutationAllowed(context, this.#input.projectId, this.#input.runId, this.#generation);
438
+ if (retainedCheckSqlSnapshot(context, this.#input) !== this.#snapshot)
439
+ throw new AppError("retained_check_evidence_invalid", "Accepted check SQL custody changed", 2, { effect: "no_effect" });
440
+ }
441
+ revalidate(context) {
442
+ const current = PreparedRetainedCheckEvidence.prepare(context, this.#input);
443
+ if (current.#snapshot !== this.#snapshot)
444
+ throw new AppError("retained_check_evidence_invalid", "Accepted check proof changed", 2, { effect: "no_effect" });
445
+ this.assertCustody(context);
446
+ }
447
+ }
448
+ function readRetainedCheckFile(root, relative) {
449
+ const fail = () => { throw new AppError("retained_check_evidence_invalid", "Accepted check file is not a contained regular file", 2, { effect: "no_effect" }); };
450
+ if (!relative || path.posix.isAbsolute(relative) || path.win32.isAbsolute(relative) || relative.includes("\\") || relative.includes("\0")
451
+ || relative.split("/").some(segment => !segment || segment === "." || segment === ".."))
452
+ fail();
453
+ try {
454
+ let parent = root;
455
+ if (!fs.lstatSync(parent).isDirectory())
456
+ fail();
457
+ for (const segment of relative.split("/").slice(0, -1)) {
458
+ parent = path.join(parent, segment);
459
+ if (!fs.lstatSync(parent).isDirectory())
460
+ fail();
461
+ }
462
+ return readBoundedReleaseFile(path.join(root, relative), 16 * 1024 * 1024);
463
+ }
464
+ catch {
465
+ return fail();
466
+ }
467
+ }
468
+ function retainedCheckSqlSnapshot(context, input) {
469
+ return context.db.readSnapshot(() => JSON.stringify({
470
+ run: context.db.get("SELECT project_id,id,workspace_root,run_root,run_dir FROM runs WHERE project_id=? AND id=?", [input.projectId, input.runId]) ?? null,
471
+ receipts: input.references.map(reference => context.db.get("SELECT * FROM check_receipts WHERE project_id=? AND run_id=? AND id=?", [input.projectId, input.runId, reference.id]) ?? null)
472
+ }));
473
+ }
474
+ function checkEvidenceSqlSnapshot(context, input) {
475
+ return context.db.readSnapshot(() => JSON.stringify({
476
+ run: context.db.get("SELECT project_id,id,workspace_root,run_root,run_dir FROM runs WHERE project_id=? AND id=?", [input.projectId, input.runId]),
477
+ receipts: context.db.all("SELECT * FROM check_receipts WHERE project_id=? AND run_id=? ORDER BY reservation_sequence,rowid", [input.projectId, input.runId])
478
+ }));
479
+ }
80
480
  export async function runCodeChecks(context, input) {
81
- reconcileUnfinishedChecks(context, input);
481
+ validateCheckEvidenceContext(input.evidenceContext);
482
+ const generation = assertRunMutationAllowed(context, input.projectId, input.runId);
483
+ let gate;
484
+ for (;;) {
485
+ assertRunMutationAllowed(context, input.projectId, input.runId, generation);
486
+ reconcileUnfinishedChecks(context, input);
487
+ reconcileWorkspaceOwnership(context, input.workspaceRoot);
488
+ try {
489
+ gate = acquireWorkspaceGate(context, input);
490
+ break;
491
+ }
492
+ catch (error) {
493
+ const sameRunGate = managedProcessStatus(context).find(item => item.kind === "workspace-gate" && item.operation_id === workspaceOperationId(input.workspaceRoot) && ["starting", "running", "stopping"].includes(item.state) && item.project_id === input.projectId && item.run_id === input.runId);
494
+ if (input.scope !== "work" || !input.workId || !(error instanceof AppError) || !["check_in_progress", "managed_operation_in_progress"].includes(error.code) || !sameRunGate)
495
+ throw error;
496
+ await new Promise(resolve => setTimeout(resolve, 500));
497
+ }
498
+ }
499
+ try {
500
+ return await runCodeChecksOwned(context, input, gate.id, generation);
501
+ }
502
+ finally {
503
+ finishManagedProcess(context, { id: gate.id, leaseToken: gate.lease_token, state: "stopped", reason: "workspace_gate_settled" });
504
+ }
505
+ }
506
+ async function runCodeChecksOwned(context, input, workspaceGateId, generation) {
507
+ assertRunMutationAllowed(context, input.projectId, input.runId, generation);
82
508
  for (const check of input.checks)
83
509
  validateCheckDeclaration(input.workspaceRoot, check);
84
- const { hash: profileHash, profile } = readCodeCheckProfile(input.workspaceRoot);
85
- const checks = input.checks.map((check) => { const ports = profile?.ports_by_alias?.[check.command]; return check.ports || !ports ? check : { ...check, ports }; });
510
+ const { profile } = readCodeCheckProfile(input.workspaceRoot);
511
+ const profileHash = codeCheckProfileHash(input.workspaceRoot, input.checks);
512
+ if (input.expectedProfileHash !== undefined && input.expectedProfileHash !== profileHash)
513
+ throw new AppError("check_profile_drift", "The frozen check profile differs from the workspace profile", 1, { expected_profile_hash: input.expectedProfileHash, actual_profile_hash: profileHash });
514
+ const checks = input.checks.map(check => {
515
+ const ports = profile?.ports_by_alias?.[check.command], inputs = profile?.inputs_by_alias?.[check.command];
516
+ const runner = profile?.runner_evidence_by_alias?.[check.command] ?? check.runner_evidence;
517
+ if (runner && !runnerContractsValid({ "@check/declaration": runner }))
518
+ throw new AppError("invalid_code_check_profile", "Check runner evidence contract is invalid", 2, { check_id: check.id });
519
+ return { ...check, ...(check.ports || !ports ? {} : { ports }), ...(check.inputs || !inputs ? {} : { inputs }), ...(runner ? { runner_evidence: runner, required_artifacts: [...new Set([...(check.required_artifacts ?? []), runner.path])] } : {}) };
520
+ });
86
521
  const resolved = resolveCodeCheckCommands(input.workspaceRoot, input.runId, checks.map((check) => check.command));
87
- const groups = deduplicate(checks, resolved);
88
- const receipts = [];
522
+ assertConsistentDeclarations(checks, resolved);
523
+ const groups = deduplicate(checks, resolved), receipts = [];
524
+ const retryIds = [...new Set([...(input.retryReceiptId ? [input.retryReceiptId] : []), ...(input.retryReceiptIds ?? [])])];
525
+ const retries = new Map();
526
+ for (const receiptId of retryIds) {
527
+ const predecessor = validateFailedCheckRetry(context, { projectId: input.projectId, runId: input.runId, receiptId, declarations: checks, reason: input.retryReason });
528
+ const snapshot = workspaceState(input.workspaceRoot, [input.runHome]);
529
+ const group = groups.find(candidate => candidate.declarations.some(check => check.run_at === predecessor.gate)
530
+ && (checkInputHash(input.workspaceRoot, { ...candidate, gate: predecessor.gate }, profileHash, workspaceInputFingerprint(input.workspaceRoot, snapshot.files, candidate.inputs, [input.runHome]), input.evidenceContext, input.checkEnvironment) === predecessor.input_hash
531
+ || sourceRepairContractMatches(predecessor, candidate, profileHash)));
532
+ if (!group || retries.has(group))
533
+ throw new AppError("retry_check_invalid", "Retry must match exactly one current execution contract", 2, { check_receipt_id: receiptId });
534
+ // Preserve the identity of a pre-deduplication failed execution when an
535
+ // upgraded RUN retries it; the new receipt still proves every grouped ref.
536
+ group.gate = predecessor.gate;
537
+ retries.set(group, receiptId);
538
+ }
89
539
  for (let index = 0; index < groups.length; index += 1) {
90
540
  const group = groups[index];
91
541
  const before = workspaceState(input.workspaceRoot, [input.runHome]);
92
542
  const startedAt = context.now();
93
- const inputHash = crypto.createHash("sha256").update(JSON.stringify({ gate: group.gate, command: group.command, environment: String(codeExecutionEnvironment(input.workspaceRoot).DD_FLOW_LOCAL_DATABASE_SUFFIX), required_artifacts: group.requiredArtifacts, ports: [...group.ports].sort() })).digest("hex");
543
+ const inputFingerprint = workspaceInputFingerprint(input.workspaceRoot, before.files, group.inputs, [input.runHome]);
544
+ const inputHash = checkInputHash(input.workspaceRoot, group, profileHash, inputFingerprint, input.evidenceContext, input.checkEnvironment);
94
545
  const epoch = crypto.createHash("sha256").update(`${group.gate}\0${before.fingerprint}`).digest("hex");
95
- const reserved = reserveReceipt(context, { ...input, group, before, profileHash, inputHash, epoch, startedAt });
546
+ const reservationInput = { ...input, retryReceiptIds: retries.has(group) ? [retries.get(group)] : [], generation, group, before, profileHash, inputHash, epoch, startedAt };
547
+ delete reservationInput.retryReceiptId;
548
+ const decision = reserveOrReuseReceipt(context, reservationInput);
549
+ if (decision.receipt) {
550
+ receipts.push(bindReceipt(decision.receipt, group));
551
+ input.progress?.(`check ${index + 1}/${groups.length} ${decision.receipt.status === "passed" ? "reused" : "requires repair"}: ${group.command}`);
552
+ continue;
553
+ }
554
+ const reserved = decision.reserved;
96
555
  const { id, evidenceDir, stdoutPath, stderrPath, receiptPath, completionPath } = reserved;
97
556
  let resources = {};
98
557
  let result = { exitCode: null, error: "check did not start", aborted: true };
99
558
  try {
100
559
  fs.mkdirSync(evidenceDir, { recursive: true });
101
- writeReceipt(receiptPath, receiptFor({ ...reserved, resources, result, status: "running", after: before, mutationPaths: [], artifacts: [] }));
102
- const managed = registerManagedProcess(context, { kind: "check", ownerPid: process.pid, ownerId: `check:${id}`, projectId: input.projectId, runId: input.runId, workId: input.workId ?? null, checkId: id, stdoutPath, stderrPath, metadata: { command: group.command } });
560
+ const startingReceipt = receiptFor({ ...reserved, resources, result, status: "running", after: before, mutationPaths: [], artifacts: [] });
561
+ if (group.runnerEvidence)
562
+ startingReceipt.runner_evidence_required = group.runnerEvidence;
563
+ writeReceipt(receiptPath, startingReceipt);
564
+ const budget = runtimeBudgetForRun(context, input.projectId, input.runId);
565
+ if (group.runnerEvidence) {
566
+ // Reject configuration before acquiring a check lease/ports, so there
567
+ // is no unconfirmed managed child to strand on a validation error.
568
+ runnerObservationDurations(process.env);
569
+ }
570
+ const managed = registerManagedProcess(context, { kind: "check", ownerPid: process.pid, ownerId: `check:${id}`, operationId: workspaceOperationId(input.workspaceRoot), projectId: input.projectId, runId: input.runId, workId: input.workId ?? null, checkId: id, stdoutPath, stderrPath, metadata: { command: group.command, role: "check", workspace_root: input.workspaceRoot, workspace_gate_id: workspaceGateId, budget } });
103
571
  const allocation = await reservePorts(context, { ownerId: managed.owner_id, processId: managed.id, names: group.ports });
104
572
  resources = allocation.ports;
105
- input.progress?.(`check ${index + 1}/${groups.length} started: ${group.command}`);
573
+ try {
574
+ input.progress?.(`check ${index + 1}/${groups.length} preparing: ${group.command}`);
575
+ assertRunMutationAllowed(context, input.projectId, input.runId, generation);
576
+ }
577
+ catch (error) {
578
+ allocation.release();
579
+ finishManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, state: "failed", reason: "dispatch_fenced_before_spawn" });
580
+ throw error;
581
+ }
106
582
  const stdout = fs.openSync(stdoutPath, "a");
107
583
  const stderr = fs.openSync(stderrPath, "a");
584
+ let confirmedExpiry = null;
108
585
  try {
109
- result = await runCheck(group.command, input.workspaceRoot, { ...codeExecutionEnvironment(input.workspaceRoot), DD_FLOW_EVIDENCE_DIR: evidenceDir, DD_FLOW_CHECK_COMPLETION_FILE: completionPath, ...portEnvironment(resources) }, stdout, stderr, (elapsed) => input.progress?.(`check ${index + 1}/${groups.length} still running (${elapsed}s): ${group.command}`), (pid) => confirmManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, pid, processGroupId: process.platform === "win32" ? null : pid }), () => heartbeatManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token }));
586
+ const runnerEnvironment = group.runnerEvidence ? { DD_FLOW_CHECK_RUNNER_EVIDENCE_PATH: path.join(evidenceDir, group.runnerEvidence.path), DD_FLOW_CHECK_RUNNER_PROGRESS_PATH: path.join(evidenceDir, "runner-progress.jsonl"), DD_FLOW_CHECK_INVOCATION_ID: id, DD_FLOW_CHECK_ID: id, DD_FLOW_CHECK_INPUT_HASH: inputHash, DD_FLOW_CHECK_VERIFICATION_EPOCH: epoch } : {};
587
+ if (group.runnerEvidence)
588
+ fs.mkdirSync(path.dirname(path.join(evidenceDir, group.runnerEvidence.path)), { recursive: true });
589
+ result = await runCheck(group.command, input.workspaceRoot, { ...codeExecutionEnvironment(input.workspaceRoot, input.checkEnvironment), DD_FLOW_EVIDENCE_DIR: evidenceDir, DD_FLOW_CHECK_COMPLETION_FILE: completionPath, ...runnerEnvironment, ...portEnvironment(resources) }, stdout, stderr, (elapsed) => input.progress?.(`check ${index + 1}/${groups.length} still running (${elapsed}s): ${group.command}`), (pid) => {
590
+ confirmedExpiry = confirmManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, pid, processGroupId: process.platform === "win32" ? null : pid }).lease_expires_at;
591
+ assertRunMutationAllowed(context, input.projectId, input.runId, generation);
592
+ if (budget)
593
+ admitRuntimeCheck(context, managed.id, managed.lease_token);
594
+ }, attemptMs => renewManagedProcess({ ...context, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: Date.now() + attemptMs }, { id: managed.id, leaseToken: managed.lease_token }), { leaseExpiresAt: () => confirmedExpiry });
110
595
  }
111
596
  finally {
112
597
  fs.closeSync(stdout);
@@ -116,7 +601,8 @@ export async function runCodeChecks(context, input) {
116
601
  if (processTreeIsAlive(current))
117
602
  throw new AppError("check_in_progress", "The check shell exited but its process tree is still live; retain this receipt and its resources until settlement", 2, { process_id: managed.id, check_receipt_id: id });
118
603
  allocation.release();
119
- finishManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, state: result.aborted || result.exitCode !== 0 || Boolean(result.error) ? "failed" : "stopped", reason: result.error });
604
+ if (!result.cleanupUnconfirmed)
605
+ finishManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, state: result.aborted || result.exitCode !== 0 || Boolean(result.error) ? "failed" : "stopped", reason: result.error });
120
606
  }
121
607
  catch (error) {
122
608
  if (error instanceof AppError && error.code === "check_in_progress")
@@ -125,7 +611,7 @@ export async function runCodeChecks(context, input) {
125
611
  }
126
612
  const after = workspaceState(input.workspaceRoot, [input.runHome]);
127
613
  const mutationPaths = changedPaths(before.files, after.files);
128
- const artifacts = collectRequiredArtifacts(evidenceDir, group.requiredArtifacts);
614
+ let artifacts = collectCheckArtifacts(evidenceDir, group.requiredArtifacts);
129
615
  if (fs.existsSync(stderrPath)) {
130
616
  if (!artifacts.complete)
131
617
  fs.appendFileSync(stderrPath, `\nmissing required evidence artifacts: ${artifacts.missing.join(", ")}\n`);
@@ -134,13 +620,40 @@ export async function runCodeChecks(context, input) {
134
620
  if (mutationPaths.length)
135
621
  fs.appendFileSync(stderrPath, `\ncheck mutated project workspace: ${mutationPaths.join(", ")}\n`);
136
622
  }
137
- const status = result.aborted ? "aborted" : result.exitCode === 0 && !result.error && artifacts.complete && mutationPaths.length === 0 ? "passed" : "failed";
623
+ const runnerEvidence = group.runnerEvidence ? readRunnerEvidence(evidenceDir, group.runnerEvidence, { invocation_id: id, check_id: id, command: group.command, input_hash: inputHash, verification_epoch: epoch }) : undefined;
624
+ const physicalFailures = executionFailures({ exitCode: result.exitCode, launcherError: result.error ?? undefined, cleanupUnconfirmed: result.cleanupUnconfirmed, missing: artifacts.missing, mutations: mutationPaths, invalidRunner: Boolean(group.runnerEvidence && !runnerEvidence) });
625
+ const candidateFiles = candidateFailureFiles(runnerEvidence, input.workspaceRoot);
626
+ if (group.runnerEvidence)
627
+ retainExecutionOutcome(evidenceDir, { id, command: group.command, input_hash: inputHash, verification_epoch: epoch }, input.workspaceRoot, physicalFailures, candidateFiles);
628
+ const requiredArtifacts = [...new Set([...group.requiredArtifacts, ...(group.runnerEvidence ? ["engine-outcome.json"] : [])])];
629
+ artifacts = collectCheckArtifacts(evidenceDir, requiredArtifacts);
630
+ const status = result.aborted ? "aborted" : result.exitCode === 0 && !result.error && artifacts.complete && mutationPaths.length === 0 && (!group.runnerEvidence || runnerEvidencePass(runnerEvidence)) ? "passed" : "failed";
138
631
  const receipt = receiptFor({ ...reserved, resources, result, status, after, mutationPaths, artifacts: artifacts.items });
632
+ receipt.required_artifacts = requiredArtifacts;
633
+ receipt.candidate_root = input.workspaceRoot;
634
+ receipt.candidate_files = candidateFiles;
635
+ receipt.execution_failures = physicalFailures;
636
+ if (group.runnerEvidence) {
637
+ receipt.runner_evidence_required = group.runnerEvidence;
638
+ receipt.runner_evidence = runnerEvidence ?? null;
639
+ }
139
640
  context.db.run("UPDATE check_receipts SET status = ?, exit_code = ?, after_fingerprint = ?, mutation_paths_json = ?, artifacts_json = ?, finished_at = ? WHERE id = ? AND project_id = ? AND status = 'running'", [receipt.status, receipt.exit_code, receipt.after_fingerprint, JSON.stringify(receipt.mutation_paths), JSON.stringify(receipt.artifacts), receipt.finished_at, id, input.projectId]);
140
641
  writeReceipt(receiptPath, receipt);
642
+ if (status === "failed")
643
+ receipt.diagnostic_evidence_valid = checkReceiptEvidenceValidForMode(receipt, "diagnostic");
141
644
  receipts.push(receipt);
142
645
  input.progress?.(`check ${index + 1}/${groups.length} ${status}: ${group.command}`);
143
646
  }
647
+ if (receipts.every(receipt => receipt.status === "passed")) {
648
+ const current = workspaceState(input.workspaceRoot, [input.runHome]), currentProfileHash = codeCheckProfileHash(input.workspaceRoot, input.checks);
649
+ for (const receipt of receipts) {
650
+ const group = groups.find(candidate => candidate.declarations.some(check => receipt.check_refs.includes(checkRef(check))));
651
+ const currentHash = group ? checkInputHash(input.workspaceRoot, group, currentProfileHash, workspaceInputFingerprint(input.workspaceRoot, current.files, group.inputs, [input.runHome]), input.evidenceContext, input.checkEnvironment) : null;
652
+ if (!group || !checkReceiptEvidenceValid(receipt) || receipt.input_hash !== currentHash)
653
+ throw new AppError("check_evidence_stale", "Check evidence changed before the gate could be accepted", 1, { check_receipt_id: receipt.id });
654
+ }
655
+ assertRunMutationAllowed(context, input.projectId, input.runId, generation);
656
+ }
144
657
  return receipts;
145
658
  }
146
659
  /**
@@ -149,62 +662,143 @@ export async function runCodeChecks(context, input) {
149
662
  * a dead/terminal process becomes an explicit aborted receipt that can retry.
150
663
  */
151
664
  export function reconcileUnfinishedChecks(context, input) {
152
- const where = input.workId ? "project_id = ? AND run_id = ? AND work_id = ? AND status = 'running'" : "project_id = ? AND run_id = ? AND status = 'running'";
665
+ let where = input.workId ? "project_id = ? AND run_id = ? AND work_id = ? AND status = 'running'" : "project_id = ? AND run_id = ? AND status = 'running'";
153
666
  const params = input.workId ? [input.projectId, input.runId, input.workId] : [input.projectId, input.runId];
667
+ if (input.receiptId) {
668
+ where += " AND id = ?";
669
+ params.push(input.receiptId);
670
+ }
154
671
  const pending = context.db.all(`SELECT id, receipt_path, started_at FROM check_receipts WHERE ${where}`, params);
155
672
  if (pending.length === 0)
156
673
  return [];
157
674
  const processes = managedProcessStatus(context);
158
675
  const reconciled = [];
159
676
  for (const pendingReceipt of pending) {
160
- const process = processes.filter((item) => item.check_id === pendingReceipt.id).at(-1);
677
+ const process = processes.filter((item) => item.check_id === pendingReceipt.id && item.project_id === input.projectId && item.run_id === input.runId).at(-1);
161
678
  const receipt = readReceipt(pendingReceipt.receipt_path);
162
679
  const completion = receipt ? readCompletion(receipt.completion_path || path.join(path.dirname(pendingReceipt.receipt_path), "completion.json")) : null;
163
680
  // A stale lease after host sleep is not permission to duplicate a living
164
681
  // check. Its durable completion marker or explicit reconciliation decides
165
682
  // the outcome before another invocation may start.
166
- if (process && ["starting", "running", "stopping"].includes(process.state) && (processTreeIsAlive(process) || (process.state === "starting" && (managedOwnerIsAlive(process) !== false || Date.parse(process.lease_expires_at) > Date.parse(context.now()))))) {
683
+ // A confirmed PID plus its birth time is sufficient to prove that this
684
+ // exact check writer is gone. Do not retain a dead receipt merely because
685
+ // older registrations lack the optional parent-owner identity.
686
+ const processProvenDead = Boolean(process?.pid && process.pid_started_at && !processTreeIsAlive(process));
687
+ if (process && !processProvenDead && (processTreeIsAlive(process) || (["starting", "running", "stopping", "orphaned"].includes(process.state) && managedOwnerIsAlive(process) !== false))) {
167
688
  throw new AppError("check_in_progress", "A previous check invocation is still running", 1, { check_receipt_id: pendingReceipt.id, process_id: process.id, pid: process.pid });
168
689
  }
169
690
  if (!process && !completion && Date.now() - Date.parse(pendingReceipt.started_at) < receiptStartingGraceMs) {
170
691
  throw new AppError("check_in_progress", "A previous check invocation is still initializing", 1, { check_receipt_id: pendingReceipt.id, initializing: true });
171
692
  }
172
693
  const after = workspaceState(input.workspaceRoot, input.runHome ? [input.runHome] : []);
173
- const artifacts = receipt ? collectRequiredArtifacts(path.join(path.dirname(receipt.completion_path || pendingReceipt.receipt_path), "artifacts"), receipt.required_artifacts ?? []) : { complete: false, missing: [], items: [] };
694
+ let artifacts = receipt ? collectCheckArtifacts(path.join(path.dirname(receipt.completion_path || pendingReceipt.receipt_path), "artifacts"), receipt.required_artifacts ?? []) : { complete: false, missing: [], items: [] };
174
695
  const workspaceUnchanged = receipt?.before_fingerprint === after.fingerprint;
175
- const status = completion === null ? "aborted" : completion.exit_code === 0 && workspaceUnchanged && artifacts.complete ? "passed" : "failed";
696
+ const runnerEvidence = receipt?.runner_evidence_required ? readRunnerEvidence(path.join(path.dirname(pendingReceipt.receipt_path), "artifacts"), receipt.runner_evidence_required, { invocation_id: receipt.id, check_id: receipt.id, command: receipt.command, input_hash: receipt.input_hash, verification_epoch: receipt.verification_epoch }) : undefined;
176
697
  const exitCode = completion?.exit_code ?? null;
177
698
  const finishedAt = completion?.finished_at ?? context.now();
699
+ const physicalFailures = executionFailures({ exitCode, missing: artifacts.missing.filter(file => file !== "engine-outcome.json"), mutations: workspaceUnchanged ? [] : ["<workspace changed while detached check ran>"], invalidRunner: Boolean(receipt?.runner_evidence_required && !runnerEvidence) });
700
+ let candidateFiles = candidateFailureFiles(runnerEvidence, input.workspaceRoot);
701
+ if (receipt?.runner_evidence_required) {
702
+ const evidenceDir = path.join(path.dirname(pendingReceipt.receipt_path), "artifacts");
703
+ if (fs.existsSync(path.join(evidenceDir, "engine-outcome.json"))) {
704
+ // A completed live writer's bound proof is already authoritative. Never erase its blockers during reconciliation.
705
+ const retained = context.db.get("SELECT artifacts_json FROM check_receipts WHERE id = ?", [pendingReceipt.id]);
706
+ const proven = resolvedReceiptArtifacts(pendingReceipt.receipt_path, retained ? JSON.parse(retained.artifacts_json) : [], receipt);
707
+ if (proven.candidate_root) {
708
+ physicalFailures.push(...(proven.execution_failures ?? []));
709
+ candidateFiles = proven.candidate_files ?? candidateFiles;
710
+ }
711
+ else {
712
+ physicalFailures.push({ code: "engine_outcome_unbound", message: "Existing execution outcome is not bound to retained inventory; prior launcher/cleanup outcome is unknown" });
713
+ fs.renameSync(path.join(evidenceDir, "engine-outcome.json"), path.join(evidenceDir, `engine-outcome-unconfirmed-${crypto.randomUUID()}.json`));
714
+ }
715
+ }
716
+ retainExecutionOutcome(evidenceDir, receipt, input.workspaceRoot, physicalFailures, candidateFiles);
717
+ receipt.required_artifacts = [...new Set([...(receipt.required_artifacts ?? []), "engine-outcome.json"])];
718
+ artifacts = collectCheckArtifacts(evidenceDir, receipt.required_artifacts);
719
+ }
720
+ const status = completion === null ? "aborted" : completion.exit_code === 0 && workspaceUnchanged && artifacts.complete && physicalFailures.length === 0 && (!receipt?.runner_evidence_required || runnerEvidencePass(runnerEvidence)) ? "passed" : "failed";
178
721
  context.db.run("UPDATE check_receipts SET status = ?, exit_code = ?, after_fingerprint = ?, mutation_paths_json = ?, artifacts_json = ?, finished_at = ? WHERE id = ? AND project_id = ? AND status = 'running'", [status, exitCode, after.fingerprint, JSON.stringify(workspaceUnchanged ? [] : ["<workspace changed while detached check ran>"]), JSON.stringify(artifacts.items), finishedAt, pendingReceipt.id, input.projectId]);
179
722
  if (process)
180
723
  finishManagedProcess(context, { id: process.id, leaseToken: process.lease_token, state: status === "passed" ? "stopped" : "failed", reason: status === "passed" ? "reconciled_completion" : completion === null ? "completion_marker_missing" : "reconciled_check_failed" });
181
724
  if (receipt) {
182
- const repaired = { ...receipt, completion_path: receipt.completion_path || path.join(path.dirname(pendingReceipt.receipt_path), "completion.json"), required_artifacts: receipt.required_artifacts ?? [], status, exit_code: exitCode, after_fingerprint: after.fingerprint, mutation_paths: workspaceUnchanged ? [] : ["<workspace changed while detached check ran>"], artifacts: artifacts.items, finished_at: finishedAt };
725
+ const repaired = { ...receipt, ...(receipt.runner_evidence_required ? { runner_evidence: runnerEvidence ?? null } : {}), completion_path: receipt.completion_path || path.join(path.dirname(pendingReceipt.receipt_path), "completion.json"), required_artifacts: receipt.required_artifacts ?? [], ...(status === "aborted" ? { abort_reason: "completion_marker_missing" } : {}), status, exit_code: exitCode, after_fingerprint: after.fingerprint, mutation_paths: workspaceUnchanged ? [] : ["<workspace changed while detached check ran>"], artifacts: artifacts.items, finished_at: finishedAt };
726
+ repaired.candidate_root = input.workspaceRoot;
727
+ repaired.candidate_files = candidateFiles;
728
+ repaired.execution_failures = physicalFailures;
183
729
  writeReceipt(pendingReceipt.receipt_path, repaired);
730
+ if (status === "failed")
731
+ repaired.diagnostic_evidence_valid = checkReceiptEvidenceValidForMode(repaired, "diagnostic");
184
732
  reconciled.push(repaired);
185
733
  }
186
734
  }
187
735
  return reconciled;
188
736
  }
189
- function reserveReceipt(context, input) {
190
- context.db.exec("BEGIN IMMEDIATE");
191
- try {
192
- const rows = context.db.all("SELECT id FROM check_receipts WHERE work_id IS ? AND run_id = ?", [input.workId ?? null, input.runId]);
193
- const ordinal = rows.reduce((max, row) => Math.max(max, Number(/\/RCP-(\d+)$/.exec(row.id)?.[1] ?? 0)), 0) + 1;
194
- const localId = `RCP-${String(ordinal).padStart(3, "0")}`;
195
- const id = `${input.workId ?? input.runId}/${localId}`;
737
+ function reserveOrReuseReceipt(context, input) {
738
+ // Receipt files and artifact hashes are prepared before reserving SQLite.
739
+ // The compact SQL snapshot below fences races with another check settlement.
740
+ const snapshotSql = "SELECT id, status, exit_code, input_hash, profile_hash, command, gate, retry_of_receipt_id, receipt_path FROM check_receipts WHERE project_id = ? AND run_id = ? ORDER BY reservation_sequence, rowid";
741
+ const snapshot = JSON.stringify(context.db.all(snapshotSql, [input.projectId, input.runId]));
742
+ const receipts = checkReceipts(context, { projectId: input.projectId, runId: input.runId });
743
+ if (JSON.stringify(context.db.all(snapshotSql, [input.projectId, input.runId])) !== snapshot)
744
+ throw new AppError("check_receipt_changed", "Check receipts changed during evidence preparation", 1);
745
+ const prior = receipts.filter((receipt) => receipt.input_hash === input.inputHash && receipt.profile_hash === input.profileHash).at(-1) ?? null;
746
+ const reusablePrior = prior?.status === "passed" && checkReceiptEvidenceValid(prior);
747
+ const retryIds = new Set([...(input.retryReceiptId ? [input.retryReceiptId] : []), ...(input.retryReceiptIds ?? [])]);
748
+ const successor = retryIds.size === 1 ? receipts.find((receipt) => receipt.retry_of_receipt_id === [...retryIds][0] && receipt.input_hash === input.inputHash && receipt.profile_hash === input.profileHash) : null;
749
+ const reusableSuccessor = successor?.status === "passed" && checkReceiptEvidenceValid(successor);
750
+ const retryPredecessor = receipts.find(receipt => retryIds.has(receipt.id));
751
+ const sourceCorrection = Boolean(retryPredecessor && sourceRepairContractMatches(retryPredecessor, input.group, input.profileHash));
752
+ const latestContract = receipts.filter(receipt => receipt.command === input.group.command && receipt.profile_hash === input.profileHash && receipt.gate === input.group.gate && receipt.scope === input.scope && receipt.work_id === (input.workId ?? null)).at(-1);
753
+ return context.db.writeTransaction(() => {
754
+ assertRunMutationAllowed(context, input.projectId, input.runId, input.generation);
755
+ if (JSON.stringify(context.db.all(snapshotSql, [input.projectId, input.runId])) !== snapshot)
756
+ throw new AppError("check_receipt_changed", "Check receipts changed before reservation", 1);
757
+ if (prior?.status === "running")
758
+ throw new AppError("check_in_progress", "A matching check invocation is still running", 1, { check_receipt_id: prior.id });
759
+ if (retryIds.size > 1)
760
+ throw new AppError("retry_check_invalid", "A check execution accepts exactly one retry predecessor", 2);
761
+ const retryId = [...retryIds][0];
762
+ if (retryId) {
763
+ if (!input.retryReason?.trim())
764
+ throw new AppError("retry_check_invalid", "A check retry requires the restored-condition reason", 2, { check_receipt_id: retryId });
765
+ const predecessor = receipts.find(receipt => receipt.id === retryId);
766
+ if (!predecessor || !["failed", "aborted"].includes(predecessor.status) || predecessor.input_hash !== input.inputHash && !sourceCorrection || predecessor.profile_hash !== input.profileHash || predecessor.command !== input.group.command || predecessor.gate !== input.group.gate)
767
+ throw new AppError("retry_check_invalid", "Retry predecessor does not match this exact check contract", 2, { check_receipt_id: retryId });
768
+ if ((sourceCorrection ? latestContract?.id : prior?.id) !== (successor?.id ?? predecessor.id))
769
+ throw new AppError("retry_check_invalid", "This retry was superseded by a newer check outcome; inspect the latest receipt", 2, { check_receipt_id: retryId, latest_check_receipt_id: prior?.id ?? null, latest_status: prior?.status ?? null });
770
+ if (successor?.status === "running")
771
+ throw new AppError("check_in_progress", "The requested retry is already running", 1, { check_receipt_id: successor.id, retry_of_receipt_id: retryId });
772
+ if (successor) {
773
+ if (reusableSuccessor)
774
+ return { receipt: successor };
775
+ throw new AppError("retry_check_invalid", "The requested retry already has a terminal outcome; choose its failed receipt for another retry", 2, { check_receipt_id: successor.id, retry_of_receipt_id: retryId, status: successor.status });
776
+ }
777
+ }
778
+ // An unchanged failed contract needs a concrete restored-condition retry,
779
+ // not repeated coordinator finish until it happens to become green.
780
+ if (prior && ["failed", "aborted"].includes(prior.status) && !retryId)
781
+ return { receipt: prior };
782
+ if (prior?.status === "passed" && !retryIds.has(prior.id) && input.group.reusable && reusablePrior)
783
+ return { receipt: prior };
784
+ let ordinal = (context.db.get("SELECT COALESCE(MAX(reservation_sequence), 0) + 1 AS sequence FROM check_receipts WHERE project_id = ? AND run_id = ?", [input.projectId, input.runId])?.sequence ?? 1);
785
+ let localId = `RCP-${String(ordinal).padStart(3, "0")}`, id = `${input.workId ?? input.runId}/${localId}`;
786
+ while (context.db.get("SELECT 1 FROM check_receipts WHERE id = ?", [id])) {
787
+ ordinal += 1;
788
+ localId = `RCP-${String(ordinal).padStart(3, "0")}`;
789
+ id = `${input.workId ?? input.runId}/${localId}`;
790
+ }
196
791
  const directory = path.join(input.runHome, input.artifactDir ?? "05-code", "checks", localId);
197
- const reserved = { id, localId, directory, evidenceDir: path.join(directory, "artifacts"), stdoutPath: path.join(directory, "stdout.log"), stderrPath: path.join(directory, "stderr.log"), receiptPath: path.join(directory, "receipt.json"), completionPath: path.join(directory, "completion.json"), group: input.group, before: input.before, profileHash: input.profileHash, inputHash: input.inputHash, epoch: input.epoch, startedAt: input.startedAt, scope: input.scope, ...(input.workId ? { workId: input.workId } : {}), projectId: input.projectId, runId: input.runId };
198
- context.db.run("INSERT INTO check_receipts (id, project_id, run_id, work_id, scope, declaration_id, check_refs_json, gate, command, input_hash, verification_epoch, status, exit_code, stdout_path, stderr_path, receipt_path, workspace_fingerprint, before_fingerprint, after_fingerprint, profile_hash, mutation_paths_json, artifacts_json, started_at, finished_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', NULL, ?, ?, ?, ?, ?, ?, ?, '[]', '[]', ?, ?)", [id, input.projectId, input.runId, input.workId ?? null, input.scope, input.group.declarations[0].id, JSON.stringify(input.group.declarations.map(checkRef)), input.group.gate, input.group.command, input.inputHash, input.epoch, reserved.stdoutPath, reserved.stderrPath, reserved.receiptPath, input.before.fingerprint, input.before.fingerprint, input.before.fingerprint, input.profileHash, input.startedAt, input.startedAt]);
199
- context.db.exec("COMMIT");
200
- return reserved;
201
- }
202
- catch (error) {
203
- context.db.exec("ROLLBACK");
204
- throw error;
205
- }
792
+ const reserved = { id, localId, directory, evidenceDir: path.join(directory, "artifacts"), stdoutPath: path.join(directory, "stdout.log"), stderrPath: path.join(directory, "stderr.log"), receiptPath: path.join(directory, "receipt.json"), completionPath: path.join(directory, "completion.json"), group: input.group, before: input.before, profileHash: input.profileHash, inputHash: input.inputHash, epoch: input.epoch, startedAt: input.startedAt, scope: input.scope, ...(retryId ? { retryOf: retryId, retryReason: input.retryReason } : {}), ...(input.workId ? { workId: input.workId } : {}), projectId: input.projectId, runId: input.runId };
793
+ context.db.run("INSERT INTO check_receipts (id, project_id, run_id, work_id, scope, declaration_id, check_refs_json, gate, command, input_hash, verification_epoch, reservation_sequence, retry_of_receipt_id, retry_reason, status, exit_code, stdout_path, stderr_path, receipt_path, workspace_fingerprint, before_fingerprint, after_fingerprint, profile_hash, mutation_paths_json, artifacts_json, started_at, finished_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', NULL, ?, ?, ?, ?, ?, ?, ?, '[]', '[]', ?, ?)", [id, input.projectId, input.runId, input.workId ?? null, input.scope, input.group.declarations[0].id, JSON.stringify(input.group.declarations.map(checkRef).sort()), input.group.gate, input.group.command, input.inputHash, input.epoch, ordinal, reserved.retryOf ?? null, reserved.retryReason ?? null, reserved.stdoutPath, reserved.stderrPath, reserved.receiptPath, input.before.fingerprint, input.before.fingerprint, input.before.fingerprint, input.profileHash, input.startedAt, input.startedAt]);
794
+ return { reserved };
795
+ }, "checks.reserveOrReuseReceipt");
796
+ }
797
+ function sourceRepairContractMatches(receipt, group, profileHash) {
798
+ return checkRepairDisposition(receipt) === "source_repair" && group.command === receipt.command && profileHash === receipt.profile_hash && isDeepStrictEqual(group.inputs, receipt.inputs)
799
+ && isDeepStrictEqual([...group.requiredArtifacts].sort(), receipt.required_artifacts.filter(file => file !== "engine-outcome.json").sort()) && isDeepStrictEqual([...group.ports].sort(), Object.keys(receipt.resources.ports).sort());
206
800
  }
207
- function receiptFor(input) { return { id: input.id, local_id: input.localId, declaration_id: input.group.declarations[0].id, check_refs: input.group.declarations.map(checkRef), gate: input.group.gate, scope: input.scope, work_id: input.workId ?? null, command: input.group.command, input_hash: input.inputHash, verification_epoch: input.epoch, status: input.status, exit_code: input.status === "running" ? null : input.result.exitCode, stdout_path: input.stdoutPath, stderr_path: input.stderrPath, receipt_path: input.receiptPath, completion_path: input.completionPath, required_artifacts: input.group.requiredArtifacts, workspace_fingerprint: input.before.fingerprint, before_fingerprint: input.before.fingerprint, after_fingerprint: input.after.fingerprint, profile_hash: input.profileHash, mutation_paths: input.mutationPaths, artifacts: input.artifacts, resources: { ports: input.resources }, started_at: input.startedAt, finished_at: input.status === "running" ? input.startedAt : new Date().toISOString() }; }
801
+ function receiptFor(input) { return { id: input.id, local_id: input.localId, declaration_id: input.group.declarations[0].id, check_refs: input.group.declarations.map(checkRef), gate: input.group.gate, scope: input.scope, work_id: input.workId ?? null, command: input.group.command, input_hash: input.inputHash, verification_epoch: input.epoch, retry_of_receipt_id: input.retryOf ?? null, retry_reason: input.retryReason ?? null, status: input.status, ...(input.status === "aborted" ? { abort_reason: input.result.error } : {}), exit_code: input.status === "running" ? null : input.result.exitCode, stdout_path: input.stdoutPath, stderr_path: input.stderrPath, receipt_path: input.receiptPath, completion_path: input.completionPath, required_artifacts: input.group.requiredArtifacts, inputs: input.group.inputs, workspace_fingerprint: input.before.fingerprint, before_fingerprint: input.before.fingerprint, after_fingerprint: input.after.fingerprint, profile_hash: input.profileHash, mutation_paths: input.mutationPaths, artifacts: input.artifacts, resources: { ports: input.resources }, started_at: input.startedAt, finished_at: input.status === "running" ? input.startedAt : new Date().toISOString() }; }
208
802
  function writeReceipt(file, receipt) { const temporary = `${file}.tmp-${process.pid}-${crypto.randomUUID()}`; fs.writeFileSync(temporary, `${JSON.stringify(receipt, null, 2)}\n`); fs.renameSync(temporary, file); }
209
803
  function readReceipt(file) { try {
210
804
  return JSON.parse(fs.readFileSync(file, "utf8"));
@@ -220,30 +814,365 @@ function readCompletion(file) { try {
220
814
  catch {
221
815
  return null;
222
816
  } }
223
- export function unchangedFinalGateFailures(context, input) { const wanted = new Set(input.declarations.flatMap((check) => [check.id, checkRef(check)])); const current = workspaceFingerprint(input.workspaceRoot); const latest = new Map(); for (const receipt of checkReceipts(context, { projectId: input.projectId, runId: input.runId }).filter((item) => item.scope === "aggregate" && item.check_refs.some((ref) => wanted.has(ref))))
224
- for (const ref of receipt.check_refs)
225
- latest.set(ref, receipt); return [...new Set(latest.values())].filter((receipt) => receipt.status === "failed" && receipt.before_fingerprint === current); }
817
+ /** Validate an explicit retry; the caller still executes a fresh real gate and retains a new receipt. */
818
+ export function validateFailedCheckRetry(context, input) {
819
+ if (!input.reason?.trim())
820
+ throw new AppError("retry_check_invalid", "--retry-check requires --reason describing the restored environment", 2);
821
+ const receipt = checkReceipts(context, { projectId: input.projectId, runId: input.runId }).find((item) => item.id === input.receiptId);
822
+ if (!receipt || !["failed", "aborted"].includes(receipt.status))
823
+ throw new AppError("retry_check_invalid", "--retry-check must name a failed or aborted receipt in this RUN", 2, { check_receipt_id: input.receiptId });
824
+ const declared = new Set(input.declarations.flatMap((check) => [check.id, checkRef(check)]));
825
+ if (!receipt.check_refs.some((ref) => declared.has(ref)))
826
+ throw new AppError("retry_check_invalid", "--retry-check does not belong to this stage gate", 2, { check_receipt_id: input.receiptId, check_refs: receipt.check_refs });
827
+ return receipt;
828
+ }
226
829
  export function checkReceipts(context, input) {
227
830
  const where = input.workId ? "project_id = ? AND run_id = ? AND work_id = ?" : "project_id = ? AND run_id = ?";
228
831
  const params = input.workId ? [input.projectId, input.runId, input.workId] : [input.projectId, input.runId];
229
- return context.db.all(`SELECT id, declaration_id, check_refs_json, gate, scope, work_id, command, input_hash, verification_epoch, status, exit_code, stdout_path, stderr_path, receipt_path, workspace_fingerprint, before_fingerprint, after_fingerprint, profile_hash, mutation_paths_json, artifacts_json, started_at, finished_at FROM check_receipts WHERE ${where} ORDER BY started_at, id`, params).map((row) => ({ ...row, local_id: row.id.slice(row.id.lastIndexOf("/") + 1), check_refs: parseArray(row.check_refs_json, [row.declaration_id]), mutation_paths: parseArray(row.mutation_paths_json, []), artifacts: JSON.parse(row.artifacts_json), resources: readReceiptResources(row.receipt_path) }));
832
+ return context.db.all(`SELECT id, declaration_id, check_refs_json, gate, scope, work_id, command, input_hash, verification_epoch, retry_of_receipt_id, retry_reason, status, exit_code, stdout_path, stderr_path, receipt_path, workspace_fingerprint, before_fingerprint, after_fingerprint, profile_hash, mutation_paths_json, artifacts_json, started_at, finished_at FROM check_receipts WHERE ${where} ORDER BY reservation_sequence, rowid`, params).map(projectCheckReceiptRow);
833
+ }
834
+ function projectCheckReceiptRow(row) {
835
+ const receipt = { ...row, local_id: row.id.slice(row.id.lastIndexOf("/") + 1), check_refs: parseArray(row.check_refs_json, [row.declaration_id]), mutation_paths: parseArray(row.mutation_paths_json, []), resources: readReceiptResources(row.receipt_path), ...resolvedReceiptArtifacts(row.receipt_path, JSON.parse(row.artifacts_json), row) };
836
+ if (receipt.status === "failed")
837
+ receipt.diagnostic_evidence_valid = checkReceiptEvidenceValidForMode(receipt, "diagnostic");
838
+ return receipt;
839
+ }
840
+ /** File bytes remain immutable; only the reader projects their current location. */
841
+ function resolvedReceiptArtifacts(receiptPath, artifacts, binding) {
842
+ let original = {};
843
+ try {
844
+ const value = JSON.parse(fs.readFileSync(receiptPath, "utf8"));
845
+ if (value && typeof value === "object" && !Array.isArray(value))
846
+ original = value;
847
+ }
848
+ catch { /* Historical missing receipts remain visibly incomplete. */ }
849
+ const currentRoot = path.dirname(receiptPath);
850
+ const oldRoot = typeof original.receipt_path === "string" ? path.dirname(original.receipt_path) : currentRoot;
851
+ const relocate = (file) => {
852
+ const relative = path.relative(oldRoot, file);
853
+ return !path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`) ? path.join(currentRoot, relative) : file;
854
+ };
855
+ let evidence = null;
856
+ if (original.runner_evidence_required && original.id && original.command && original.input_hash && original.verification_epoch) {
857
+ const root = path.join(currentRoot, "artifacts");
858
+ const retained = collectCheckArtifacts(root, [original.runner_evidence_required.path]);
859
+ if (retained.complete && retained.items.every(item => artifacts.some(artifact => relocate(artifact.path) === item.path && artifact.sha256 === item.sha256)))
860
+ evidence = readRunnerEvidence(root, original.runner_evidence_required, { invocation_id: binding.id, check_id: binding.id, command: binding.command, input_hash: binding.input_hash, verification_epoch: binding.verification_epoch });
861
+ }
862
+ let outcome = {};
863
+ const outcomePath = path.join(currentRoot, "artifacts", "engine-outcome.json");
864
+ const retainedOutcome = collectCheckArtifacts(path.join(currentRoot, "artifacts"), ["engine-outcome.json"]);
865
+ if (retainedOutcome.complete && retainedOutcome.items.every(item => artifacts.some(artifact => relocate(artifact.path) === item.path && artifact.sha256 === item.sha256))) {
866
+ try {
867
+ const value = JSON.parse(fs.readFileSync(outcomePath, "utf8"));
868
+ if (value.schema_id === "dd-flow/check-execution-outcome@1" && value.id === binding.id && value.command === binding.command && value.input_hash === binding.input_hash && value.verification_epoch === binding.verification_epoch && typeof value.candidate_root === "string" && path.isAbsolute(value.candidate_root) && (value.candidate_files === undefined || Array.isArray(value.candidate_files) && value.candidate_files.length <= 64 && value.candidate_files.every((file) => typeof file === "string" && path.isAbsolute(file) && Buffer.byteLength(file) <= 4096)) && Array.isArray(value.execution_failures) && value.execution_failures.every((failure) => failure && typeof failure === "object" && typeof failure.code === "string" && typeof failure.message === "string"))
869
+ outcome = value;
870
+ }
871
+ catch { /* Missing physical proof cannot grant attribution. */ }
872
+ }
873
+ return { completion_path: path.join(currentRoot, "completion.json"), required_artifacts: original.required_artifacts ?? [], inputs: original.inputs ?? [], artifacts: artifacts.map((artifact) => ({ ...artifact, path: relocate(artifact.path) })), ...(original.runner_evidence_required ? { runner_evidence_required: original.runner_evidence_required, runner_evidence: evidence } : {}), execution_failures: outcome.execution_failures ?? [], ...(outcome.candidate_root ? { candidate_root: outcome.candidate_root } : {}), ...(outcome.candidate_files ? { candidate_files: outcome.candidate_files } : {}) };
874
+ }
875
+ function executionFailures(input) {
876
+ const failures = [];
877
+ if (input.exitCode === null)
878
+ failures.push({ code: "completion_unknown", message: "Check completion is unknown" });
879
+ if (input.launcherError)
880
+ failures.push({ code: "launcher_error", message: input.launcherError });
881
+ if (input.cleanupUnconfirmed)
882
+ failures.push({ code: "cleanup_unconfirmed", message: "Check process cleanup is not confirmed" });
883
+ if (input.missing.length)
884
+ failures.push({ code: "missing_required_artifact", message: input.missing.join(", ") });
885
+ if (input.mutations.length)
886
+ failures.push({ code: "check_input_mutated", message: input.mutations.join(", ") });
887
+ if (input.invalidRunner)
888
+ failures.push({ code: "invalid_runner_evidence", message: "Required runner evidence is unavailable or invalid" });
889
+ return failures;
890
+ }
891
+ function retainExecutionOutcome(evidenceDir, binding, candidateRoot, failures, candidateFiles) {
892
+ const temporary = path.join(evidenceDir, `.engine-outcome-${crypto.randomUUID()}.tmp`);
893
+ if (!fs.lstatSync(evidenceDir).isDirectory() || fs.lstatSync(evidenceDir).isSymbolicLink())
894
+ throw new AppError("check_evidence_stale", "Check evidence directory was replaced", 1);
895
+ fs.writeFileSync(temporary, JSON.stringify({ schema_id: "dd-flow/check-execution-outcome@1", id: binding.id, command: binding.command, input_hash: binding.input_hash, verification_epoch: binding.verification_epoch, candidate_root: candidateRoot, candidate_files: candidateFiles, execution_failures: failures }), { flag: "wx", mode: 0o600 });
896
+ fs.renameSync(temporary, path.join(evidenceDir, "engine-outcome.json"));
230
897
  }
231
898
  export function workspaceFingerprint(root) { return workspaceState(root).fingerprint; }
232
- export function workspaceChangedPaths(root) { const status = spawnSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: root, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }); if (status.status !== 0)
233
- return null; return status.stdout.split("\0").filter(Boolean).map((entry) => entry.slice(3)).filter(Boolean).sort(); }
234
- function deduplicate(checks, commands) { const groups = new Map(); checks.forEach((check, index) => { const artifacts = [...(check.required_artifacts ?? [])].sort(); const ports = [...(check.ports ?? [])].sort(); const key = JSON.stringify([check.run_at, commands[index], artifacts, ports]); const current = groups.get(key); if (current)
235
- current.declarations.push(check);
236
- else
237
- groups.set(key, { gate: check.run_at, command: commands[index], declarations: [check], requiredArtifacts: artifacts, ports }); }); return [...groups.values()]; }
238
- function workspaceState(root, ignoredRoots = []) { const git = spawnSync("git", ["ls-files", "-co", "--exclude-standard", "-z"], { cwd: root, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }); const ignored = ignoredRoots.map((entry) => path.resolve(entry)); const listing = (git.status === 0 ? git.stdout.split("\0").filter(Boolean).sort() : listFiles(root)).filter((relative) => { const file = path.resolve(root, relative); return !ignored.some((ignoredRoot) => file === ignoredRoot || file.startsWith(`${ignoredRoot}${path.sep}`)); }); const files = new Map(); const hash = crypto.createHash("sha256"); for (const relative of listing) {
239
- const file = path.join(root, relative);
240
- const content = fs.existsSync(file) && fs.statSync(file).isFile() ? fs.readFileSync(file) : Buffer.from("<missing>");
241
- const digest = crypto.createHash("sha256").update(content).digest("hex");
242
- files.set(relative, digest);
243
- hash.update(relative).update("\0").update(digest).update("\0");
244
- } return { fingerprint: hash.digest("hex"), files }; }
899
+ /** One NUL-aware source of truth for paths differing from HEAD. Rename/copy
900
+ * records carry a second path; retain both because the old side is a deletion. */
901
+ export function workspaceChangedPaths(root) {
902
+ const status = spawnSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: root, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
903
+ // A workspace can be intentionally gitless. Keep the same conservative
904
+ // file inventory used for fingerprints instead of making result validation
905
+ // fail before the work contract can report its actual error.
906
+ if (isGitlessWorkspace(root, status))
907
+ return listFiles(root);
908
+ if (status.status !== 0 || status.error)
909
+ throw new AppError("workspace_status_failed", "Could not determine changed workspace paths", 1, { workspace_root: root, stderr: status.stderr || null, cause: status.error?.message ?? null });
910
+ const records = status.stdout.split("\0");
911
+ const paths = [];
912
+ for (let index = 0; index < records.length; index += 1) {
913
+ const record = records[index];
914
+ if (!record)
915
+ continue;
916
+ if (record.length < 4 || record[2] !== " ")
917
+ throw new AppError("workspace_status_invalid", "Git returned an invalid porcelain-v1 record", 1, { workspace_root: root, record });
918
+ paths.push(normalizeWorkspacePath(root, record.slice(3)));
919
+ if (record[0] === "R" || record[0] === "C" || record[1] === "R" || record[1] === "C") {
920
+ const prior = records[++index];
921
+ if (!prior)
922
+ throw new AppError("workspace_status_invalid", "Git rename/copy record is missing its second path", 1, { workspace_root: root, record });
923
+ paths.push(normalizeWorkspacePath(root, prior));
924
+ }
925
+ }
926
+ const ownedHook = paths.includes(".agents/hooks.json") ? ownedWorkspaceHookPath(root) : null;
927
+ return [...new Set(paths)].filter(file => file !== ownedHook).sort();
928
+ }
929
+ /** Ignored machine state cannot be reported as a product change. Tracked paths
930
+ * remain eligible regardless of their directory name. */
931
+ export function ignoredWorkspacePaths(root, paths) {
932
+ const normalized = [...new Set(paths.map((item) => normalizeWorkspacePath(root, item)))];
933
+ if (!normalized.length)
934
+ return [];
935
+ const status = spawnSync("git", ["check-ignore", "-z", "--stdin"], { cwd: root, encoding: "utf8", input: `${normalized.join("\0")}\0`, maxBuffer: 64 * 1024 * 1024 });
936
+ // The canonical owned hook has one meaning across Git status, check cache
937
+ // and MERGE, without changing a sibling worktree's shared Git excludes.
938
+ const gitless = isGitlessWorkspace(root, status);
939
+ if (!gitless && (![0, 1].includes(status.status ?? -1) || status.error))
940
+ throw new AppError("workspace_ignore_check_failed", "Could not classify reported workspace paths", 1, { workspace_root: root, stderr: status.stderr || null, cause: status.error?.message ?? null });
941
+ const ownedHook = normalized.includes(".agents/hooks.json") ? ownedWorkspaceHookPath(root, gitless ? [] : undefined) : null;
942
+ if (gitless)
943
+ return ownedHook ? [ownedHook] : [];
944
+ return [...new Set([...status.stdout.split("\0").filter(Boolean).map((item) => normalizeWorkspacePath(root, item)), ...(ownedHook ? [ownedHook] : [])])].sort();
945
+ }
946
+ export function normalizeWorkspacePath(root, value) {
947
+ if (typeof value !== "string" || !value || value.includes("\0") || path.isAbsolute(value))
948
+ throw new AppError("workspace_path_invalid", "Workspace path must be a non-empty relative path", 2, { path: value });
949
+ const resolvedRoot = path.resolve(root);
950
+ const resolved = path.resolve(resolvedRoot, value);
951
+ const relative = path.relative(resolvedRoot, resolved);
952
+ if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
953
+ throw new AppError("workspace_path_invalid", "Workspace path escapes its root", 2, { path: value, workspace_root: root });
954
+ return relative.split(path.sep).join("/");
955
+ }
956
+ function deduplicate(checks, commands) {
957
+ const groups = new Map();
958
+ checks.forEach((check, index) => {
959
+ const artifacts = [...(check.required_artifacts ?? [])].sort(), ports = [...(check.ports ?? [])].sort(), inputs = normalizedInputs(check.inputs ?? []);
960
+ const reusable = check.reuse === "deterministic";
961
+ // Stage labels identify obligations, not different processes in this gate.
962
+ // Cross-invocation reuse still requires the explicit deterministic contract.
963
+ const environmentInputs = normalizedEnvironmentInputs(check.environment_inputs);
964
+ const key = JSON.stringify([commands[index], artifacts, ports, inputs, reusable, environmentInputs, check.runner_evidence ?? null]);
965
+ const current = groups.get(key);
966
+ if (current)
967
+ current.declarations.push(check);
968
+ else
969
+ groups.set(key, { gate: check.run_at, command: commands[index], declarations: [check], requiredArtifacts: artifacts, ports, inputs, reusable, ...(environmentInputs === undefined ? {} : { environmentInputs }), ...(check.runner_evidence ? { runnerEvidence: check.runner_evidence } : {}) });
970
+ });
971
+ return [...groups.values()].map(group => ({ ...group, gate: [...group.declarations].map(check => check.run_at).sort()[0], declarations: [...group.declarations].sort((left, right) => checkRef(left).localeCompare(checkRef(right))) }));
972
+ }
973
+ function workspaceState(root, ignoredRoots = []) {
974
+ const git = workspaceGitFiles(root, ["-co", "--exclude-standard"]);
975
+ const ignored = ignoredRoots.map(entry => path.resolve(entry));
976
+ const listing = [...new Set(git ?? listFiles(root))].sort();
977
+ const ownedHook = listing.includes(".agents/hooks.json") ? ownedWorkspaceHookPath(root, git ? undefined : []) : null;
978
+ const files = new Map(), hash = crypto.createHash("sha256");
979
+ for (const relative of listing) {
980
+ if (relative === ownedHook)
981
+ continue;
982
+ const file = path.resolve(root, relative);
983
+ if (ignored.some(ignoredRoot => file === ignoredRoot || file.startsWith(`${ignoredRoot}${path.sep}`)))
984
+ continue;
985
+ // Deleted tracked paths are absent content, both before and after commit.
986
+ // Do not mask other IO errors as an unchanged workspace.
987
+ let stat;
988
+ try {
989
+ stat = fs.lstatSync(file);
990
+ }
991
+ catch (error) {
992
+ if (error.code === "ENOENT")
993
+ continue;
994
+ throw error;
995
+ }
996
+ const content = stat.isSymbolicLink() ? Buffer.from(`symlink\0${fs.readlinkSync(file)}`) : stat.isFile() ? fs.readFileSync(file) : Buffer.from("<directory>");
997
+ const digest = crypto.createHash("sha256").update(content).digest("hex");
998
+ files.set(relative, digest);
999
+ hash.update(relative).update("\0").update(digest).update("\0");
1000
+ }
1001
+ return { fingerprint: hash.digest("hex"), files };
1002
+ }
1003
+ function workspaceInputFingerprint(root, files, inputs, ignoredRoots) {
1004
+ const selected = new Map(inputs.length === 0 || inputs.includes(".") ? files : []);
1005
+ for (const [file, digest] of explicitInputFiles(root, inputs.filter(input => input !== "."), ignoredRoots))
1006
+ selected.set(file, digest);
1007
+ const hash = crypto.createHash("sha256");
1008
+ for (const [file, digest] of [...selected].sort(([left], [right]) => left.localeCompare(right)))
1009
+ hash.update(file).update("\0").update(digest).update("\0");
1010
+ return hash.digest("hex");
1011
+ }
245
1012
  function changedPaths(before, after) { return [...new Set([...before.keys(), ...after.keys()])].filter((key) => before.get(key) !== after.get(key)).sort(); }
246
1013
  function checkRef(check) { return check.canonical_ref ?? check.id; }
1014
+ function assertConsistentDeclarations(checks, commands) {
1015
+ const seen = new Map();
1016
+ for (const [index, check] of checks.entries()) {
1017
+ const contract = JSON.stringify([check.run_at, commands[index], [...(check.required_artifacts ?? [])].sort(), [...(check.ports ?? [])].sort(), normalizedInputs(check.inputs ?? []), check.reuse ?? null, normalizedEnvironmentInputs(check.environment_inputs), check.runner_evidence ?? null]);
1018
+ const ref = checkRef(check), prior = seen.get(ref);
1019
+ if (prior && prior !== contract)
1020
+ throw new AppError("check_declaration_conflict", "The same check reference has conflicting execution definitions", 2, { check_ref: ref });
1021
+ seen.set(ref, contract);
1022
+ }
1023
+ }
1024
+ function normalizedEnvironmentInputs(value) {
1025
+ if (value === undefined)
1026
+ return undefined;
1027
+ if (!Array.isArray(value) || !value.length || value.length > 64 || value.some(name => typeof name !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*\*?$/.test(name)))
1028
+ throw new AppError("invalid_check_environment_inputs", "Internal check environment inputs must be bounded names or prefixes", 2, { effect: "no_effect" });
1029
+ return [...new Set(value)].sort();
1030
+ }
1031
+ function environmentFingerprint(workspaceRoot, inputs, checkEnvironment) {
1032
+ const environment = codeExecutionEnvironment(workspaceRoot, checkEnvironment);
1033
+ // Invocation authority is checked by its own owners, not an application check
1034
+ // input. Preserve arbitrary application variables (including DD_FLOW_PORT_*)
1035
+ // and tool selection; omit only known process/control bookkeeping.
1036
+ const bookkeeping = new Set(["PWD", "OLDPWD", "SHLVL", "_", "DD_FLOW_DAEMON_ID",
1037
+ "DD_FLOW_CURRENT_INVOCATION", "DD_FLOW_INVOCATION_READONLY", "DD_FLOW_INVOCATION_SCOPE", "DD_FLOW_CODEX_STATE_DIR",
1038
+ "DD_FLOW_RUNTIME_OWNER", "DD_FLOW_CONTROLLER_TOKEN", "DD_FLOW_CONTROL_TOKEN", "DD_FLOW_SCOPE_CONTROL_TOKEN",
1039
+ "DD_FLOW_BIN", "DD_FLOW_ENGINE_HOME", "DD_FLOW_RUNTIME_BUDGET", "DD_FLOW_WORK_ID", "DD_FLOW_SESSION_ID", "CODEX_THREAD_ID", "DD_EVAL_OPERATION_ID",
1040
+ "CODEX_PERMISSION_PROFILE", "CODEX_SESSION_ID", "CODEX_VERSION"]);
1041
+ return crypto.createHash("sha256").update(JSON.stringify(Object.entries(environment)
1042
+ .filter(([name, value]) => value !== undefined && !bookkeeping.has(name)
1043
+ && (inputs === undefined || inputs.some(input => input.endsWith("*") ? name.startsWith(input.slice(0, -1)) : name === input)))
1044
+ .sort(([left], [right]) => left.localeCompare(right)))).digest("hex");
1045
+ }
1046
+ function acquireWorkspaceGate(context, input) {
1047
+ const operationId = workspaceOperationId(input.workspaceRoot);
1048
+ const active = managedProcessStatus(context).find(item => item.operation_id === operationId && ["starting", "running", "stopping"].includes(item.state));
1049
+ if (active)
1050
+ throw new AppError("check_in_progress", "The workspace is owned by another bootstrap or check; observe it before starting a new check", 1, { process_id: active.id, operation_id: operationId });
1051
+ return registerManagedProcess(context, { kind: "workspace-gate", ownerPid: process.pid, ownerId: `workspace-gate:${crypto.randomUUID()}`, operationId, uniqueActiveOperation: true, projectId: input.projectId, runId: input.runId, metadata: { workspace_root: input.workspaceRoot, role: "check_gate" } });
1052
+ }
1053
+ export function reconcileWorkspaceOwnership(context, workspaceRoot) {
1054
+ const operationId = workspaceOperationId(workspaceRoot);
1055
+ const active = managedProcessStatus(context).filter(item => item.operation_id === operationId && ["starting", "running", "stopping", "orphaned"].includes(item.state));
1056
+ if (active.some(item => processTreeIsAlive(item) || managedOwnerIsAlive(item) !== false))
1057
+ return;
1058
+ // Registry IDs are host-wide, whereas receipts belong to one runtime DB.
1059
+ // An unknown/foreign home must not be settled against this client's rows.
1060
+ if (active.some(item => managedProcessRuntimeHome(item, context) !== context.ddFlowHome))
1061
+ return;
1062
+ for (const child of active.filter(item => item.kind !== "workspace-gate")) {
1063
+ if (child.kind !== "check" || !child.project_id || !child.run_id || !child.check_id)
1064
+ return;
1065
+ const run = context.db.get("SELECT run_root FROM runs WHERE project_id = ? AND id = ?", [child.project_id, child.run_id]);
1066
+ if (!run?.run_root || !context.db.get("SELECT 1 FROM check_receipts WHERE id = ? AND project_id = ? AND run_id = ?", [child.check_id, child.project_id, child.run_id]))
1067
+ return;
1068
+ reconcileUnfinishedChecks(context, { projectId: child.project_id, runId: child.run_id, workspaceRoot, runHome: run.run_root, receiptId: child.check_id });
1069
+ // A crash may occur after receipt settlement but before registry settlement.
1070
+ const current = managedProcessStatus(context).find(item => item.id === child.id);
1071
+ if (!current || current.lease_token !== child.lease_token)
1072
+ return;
1073
+ if (!["stopped", "failed"].includes(current.state) && !finishManagedProcess(context, { id: child.id, leaseToken: child.lease_token, state: "stopped", reason: "workspace_check_owner_reconciled" }))
1074
+ return;
1075
+ }
1076
+ for (const gate of active.filter(item => item.kind === "workspace-gate"))
1077
+ finishManagedProcess(context, { id: gate.id, leaseToken: gate.lease_token, state: "stopped", reason: "workspace_gate_owner_reconciled" });
1078
+ }
1079
+ function normalizedInputs(inputs) { for (const input of inputs)
1080
+ validateCheckInputSyntax(input); return [...new Set(inputs.map((input) => { const normalized = path.posix.normalize(input.replaceAll("\\", "/")).replace(/^\.\/$/, ""); return normalized === "" || normalized === "." ? "." : normalized.replace(/\/$/, ""); }))].sort(); }
1081
+ function validateCheckInputs(workspaceRoot, inputs) { const root = fs.realpathSync.native(workspaceRoot); for (const input of normalizedInputs(inputs)) {
1082
+ if (input === ".git" || input.startsWith(".git/"))
1083
+ throw new AppError("invalid_check_input", "Check inputs cannot include Git internals", 2, { input });
1084
+ const candidate = path.resolve(workspaceRoot, input);
1085
+ if (candidate !== workspaceRoot && !candidate.startsWith(`${workspaceRoot}${path.sep}`))
1086
+ throw new AppError("invalid_check_input", "Check inputs must stay within the workspace", 2, { input });
1087
+ if (fs.existsSync(candidate))
1088
+ validateInputTree(root, candidate, input, new Set());
1089
+ else
1090
+ validateMissingInputParent(root, candidate, input);
1091
+ } }
1092
+ function validateCheckInputSyntax(input) { if (typeof input !== "string" || !input.trim() || path.isAbsolute(input) || input.split(/[\\/]/).includes(".."))
1093
+ throw new AppError("invalid_check_input", "Check inputs must be non-empty relative workspace paths", 2, { input }); }
1094
+ function validateMissingInputParent(root, candidate, input) { let parent = candidate; while (!fs.existsSync(parent))
1095
+ parent = path.dirname(parent); const resolved = fs.realpathSync.native(parent); if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`))
1096
+ throw new AppError("invalid_check_input", "Check input parent symlink escapes the workspace", 2, { input }); }
1097
+ function validateInputTree(root, candidate, input, seen) {
1098
+ const resolved = fs.realpathSync.native(candidate);
1099
+ if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`))
1100
+ throw new AppError("invalid_check_input", "Check input symlink escapes the workspace", 2, { input });
1101
+ if (seen.has(resolved))
1102
+ throw new AppError("invalid_check_input", "Check input contains a symbolic-link cycle", 2, { input });
1103
+ seen.add(resolved);
1104
+ const stat = fs.statSync(candidate);
1105
+ if (stat.isDirectory())
1106
+ for (const entry of fs.readdirSync(candidate))
1107
+ validateInputTree(root, path.join(candidate, entry), input, seen);
1108
+ seen.delete(resolved);
1109
+ }
1110
+ function explicitInputFiles(root, inputs, ignoredRoots) {
1111
+ const rootReal = fs.realpathSync.native(root), ignored = ignoredRoots.map(entry => path.resolve(entry)), result = new Map();
1112
+ const visit = (file, relative, seen) => {
1113
+ if (ignored.some(entry => file === entry || file.startsWith(`${entry}${path.sep}`)))
1114
+ return;
1115
+ if (!fs.existsSync(file)) {
1116
+ result.set(relative, "<missing>");
1117
+ return;
1118
+ }
1119
+ const resolved = fs.realpathSync.native(file);
1120
+ if (resolved !== rootReal && !resolved.startsWith(`${rootReal}${path.sep}`))
1121
+ throw new AppError("invalid_check_input", "Check input symlink escapes the workspace", 2, { input: relative });
1122
+ if (seen.has(resolved))
1123
+ throw new AppError("invalid_check_input", "Check input contains a symbolic-link cycle", 2, { input: relative });
1124
+ const stat = fs.statSync(file);
1125
+ if (stat.isDirectory()) {
1126
+ seen.add(resolved);
1127
+ for (const entry of fs.readdirSync(file).sort())
1128
+ visit(path.join(file, entry), path.posix.join(relative, entry), seen);
1129
+ seen.delete(resolved);
1130
+ }
1131
+ else if (stat.isFile())
1132
+ result.set(relative, crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"));
1133
+ };
1134
+ for (const input of inputs)
1135
+ visit(path.resolve(root, input), input, new Set());
1136
+ return [...result.entries()];
1137
+ }
1138
+ function checkInputHash(workspaceRoot, group, profileHash, inputFingerprint, evidenceContext, checkEnvironment) { return crypto.createHash("sha256").update(JSON.stringify({ workspace_root: path.resolve(workspaceRoot), gate: group.reusable ? null : group.gate, command: group.command, required_artifacts: group.requiredArtifacts, ports: group.ports, inputs: group.inputs, ...(group.runnerEvidence ? { runner_evidence: group.runnerEvidence } : {}), profile_hash: profileHash, environment: environmentFingerprint(workspaceRoot, group.environmentInputs, checkEnvironment), input_fingerprint: inputFingerprint, ...(group.environmentInputs === undefined ? {} : { environment_inputs: group.environmentInputs }), ...(evidenceContext === undefined ? {} : { evidence_context: evidenceContext }) })).digest("hex"); }
1139
+ function bindReceipt(receipt, group) { return { ...receipt, gate: group.gate, declaration_id: group.declarations[0].id, check_refs: group.declarations.map(checkRef), required_artifacts: [...new Set([...group.requiredArtifacts, ...(receipt.required_artifacts.includes("engine-outcome.json") ? ["engine-outcome.json"] : [])])], inputs: group.inputs }; }
1140
+ export function checkReceiptEvidenceValid(receipt) { return checkReceiptEvidenceValidForMode(receipt); }
1141
+ function checkReceiptEvidenceValidForMode(receipt, mode) {
1142
+ try {
1143
+ if (!(receipt.status === "passed" && receipt.exit_code === 0 || mode === "diagnostic" && receipt.status === "failed" && typeof receipt.exit_code === "number" && receipt.exit_code !== 0) || receipt.before_fingerprint !== receipt.after_fingerprint
1144
+ || receipt.mutation_paths.length !== 0 || receipt.completion_path !== path.join(path.dirname(receipt.receipt_path), "completion.json")
1145
+ || readCompletion(receipt.completion_path)?.exit_code !== receipt.exit_code)
1146
+ return false;
1147
+ const stored = readReceipt(receipt.receipt_path);
1148
+ if (!stored || typeof stored !== "object" || Array.isArray(stored)
1149
+ || typeof stored.receipt_path !== "string" || !path.isAbsolute(stored.receipt_path)
1150
+ || path.basename(stored.receipt_path) !== "receipt.json"
1151
+ || stored.completion_path !== path.join(path.dirname(stored.receipt_path), "completion.json"))
1152
+ return false;
1153
+ // Gate/ref labels may be rebound for explicit deterministic reuse. Physical
1154
+ // execution identity and settled SQL evidence must never be rebound from a
1155
+ // mutable receipt file, including after recovery relocates the RUN home.
1156
+ const fields = ["id", "scope", "work_id", "command", "input_hash", "verification_epoch", "status", "exit_code",
1157
+ "workspace_fingerprint", "before_fingerprint", "after_fingerprint", "profile_hash", "started_at", "finished_at"];
1158
+ if (fields.some(field => stored[field] !== receipt[field]) || !Array.isArray(stored.artifacts)
1159
+ || !Array.isArray(stored.mutation_paths) || stored.mutation_paths.length !== 0
1160
+ || JSON.stringify(stored.required_artifacts) !== JSON.stringify(receipt.required_artifacts)
1161
+ || JSON.stringify(stored.inputs) !== JSON.stringify(receipt.inputs))
1162
+ return false;
1163
+ const retained = resolvedReceiptArtifacts(receipt.receipt_path, stored.artifacts, receipt).artifacts;
1164
+ if (JSON.stringify(retained) !== JSON.stringify(receipt.artifacts))
1165
+ return false;
1166
+ const root = path.join(path.dirname(receipt.receipt_path), "artifacts");
1167
+ if (stored.runner_evidence_required && mode !== "diagnostic" && !runnerEvidencePass(readRunnerEvidence(root, stored.runner_evidence_required, { invocation_id: receipt.id, check_id: receipt.id, command: receipt.command, input_hash: receipt.input_hash, verification_epoch: receipt.verification_epoch })))
1168
+ return false;
1169
+ const artifacts = collectCheckArtifacts(root, receipt.required_artifacts);
1170
+ return artifacts.complete && JSON.stringify(artifacts.items) === JSON.stringify(retained);
1171
+ }
1172
+ catch {
1173
+ return false;
1174
+ }
1175
+ }
247
1176
  function parseArray(value, fallback) { try {
248
1177
  const parsed = JSON.parse(value);
249
1178
  return Array.isArray(parsed) && parsed.every((item) => typeof item === "string") ? parsed : fallback;
@@ -252,8 +1181,14 @@ catch {
252
1181
  return fallback;
253
1182
  } }
254
1183
  function isStringRecord(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((item) => typeof item === "string" && item.trim().length > 0); }
255
- function isGateRecord(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value) && Object.entries(value).every(([gate, aliases]) => ["work", "code", "readiness", "merge", "release", "external"].includes(gate) && Array.isArray(aliases) && aliases.every((item) => typeof item === "string" && item.startsWith("@check/"))); }
1184
+ function isGateRecord(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value) && Object.entries(value).every(([gate, aliases]) => ["work", "code", "readiness", "merge", "release"].includes(gate) && Array.isArray(aliases) && aliases.every((item) => typeof item === "string" && item.startsWith("@check/"))); }
256
1185
  function isPortAliasRecord(value) { return value === undefined || (Boolean(value) && typeof value === "object" && !Array.isArray(value) && Object.entries(value).every(([alias, ports]) => alias.startsWith("@check/") && Array.isArray(ports) && ports.length > 0 && new Set(ports).size === ports.length && ports.every((port) => typeof port === "string" && /^[A-Za-z][A-Za-z0-9_]*$/.test(port)))); }
1186
+ function isInputAliasRecord(workspaceRoot, value) { try {
1187
+ return value === undefined || (Boolean(value) && typeof value === "object" && !Array.isArray(value) && Object.entries(value).every(([alias, inputs]) => alias.startsWith("@check/") && Array.isArray(inputs) && (validateCheckInputs(workspaceRoot, inputs), true)));
1188
+ }
1189
+ catch {
1190
+ return false;
1191
+ } }
257
1192
  function portEnvironment(ports) { return Object.fromEntries(Object.entries(ports).map(([name, value]) => [`DD_FLOW_PORT_${name.toUpperCase()}`, String(value)])); }
258
1193
  function readReceiptResources(file) { try {
259
1194
  const value = JSON.parse(fs.readFileSync(file, "utf8"));
@@ -262,26 +1197,63 @@ function readReceiptResources(file) { try {
262
1197
  catch {
263
1198
  return { ports: {} };
264
1199
  } }
265
- function collectRequiredArtifacts(root, required) { const missing = []; const items = []; for (const relative of required) {
266
- if (path.isAbsolute(relative) || relative.split(/[\\/]/).includes(".."))
267
- throw new AppError("invalid_evidence_path", "Required evidence paths must be relative to DD_FLOW_EVIDENCE_DIR", 2, { path: relative });
268
- const file = path.join(root, relative);
269
- if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
270
- missing.push(relative);
271
- continue;
272
- }
273
- items.push({ path: relative, sha256: crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex") });
274
- } return { complete: missing.length === 0, missing, items }; }
275
- function listFiles(root, current = root) { return fs.readdirSync(current, { withFileTypes: true }).flatMap((entry) => { if ([".git", "node_modules"].includes(entry.name))
276
- return []; const absolute = path.join(current, entry.name); return entry.isDirectory() ? listFiles(root, absolute) : [path.relative(root, absolute)]; }).sort(); }
277
- export function runCheck(command, cwd, environment, stdout, stderr, heartbeat, onSpawn, renewLease) {
1200
+ function collectCheckArtifacts(root, required) {
1201
+ for (const relative of required) {
1202
+ if (path.isAbsolute(relative) || relative.split(/[\\/]/).includes(".."))
1203
+ throw new AppError("invalid_evidence_path", "Required evidence paths must be relative to DD_FLOW_EVIDENCE_DIR", 2, { path: relative });
1204
+ }
1205
+ // Required paths are minimum obligations; retain all explicit check evidence.
1206
+ // Never follow symlinks, including a replaced evidence root.
1207
+ const items = [];
1208
+ const visit = (directory) => {
1209
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
1210
+ const file = path.join(directory, entry.name);
1211
+ if (entry.isDirectory())
1212
+ visit(file);
1213
+ else if (entry.isFile())
1214
+ items.push({ path: path.relative(root, file), sha256: crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex") });
1215
+ }
1216
+ };
1217
+ if (fs.existsSync(root) && fs.lstatSync(root).isDirectory())
1218
+ visit(root);
1219
+ items.sort((a, b) => a.path.localeCompare(b.path));
1220
+ const retained = new Set(items.map((item) => item.path));
1221
+ const missing = required.filter((relative) => !retained.has(path.normalize(relative)));
1222
+ return { complete: missing.length === 0, missing, items };
1223
+ }
1224
+ function listFiles(root) {
1225
+ const allowed = projectServicePathFilter(root, [], []);
1226
+ const ownedHook = ownedWorkspaceHookPath(root, []);
1227
+ const visit = (current) => fs.readdirSync(current, { withFileTypes: true }).flatMap(entry => {
1228
+ const absolute = path.join(current, entry.name);
1229
+ if ([".git", "node_modules"].includes(entry.name) || !allowed(absolute))
1230
+ return [];
1231
+ return entry.isDirectory() ? visit(absolute) : [path.relative(root, absolute)];
1232
+ });
1233
+ return visit(root).filter(file => file.split(path.sep).join("/") !== ownedHook).sort();
1234
+ }
1235
+ export function runCheck(command, cwd, environment, stdout, stderr, heartbeat, onSpawn, renewLease, observation = {}) {
1236
+ const inactivityTimeoutMs = validateObservationDuration(observation.inactivityTimeoutMs ?? 30 * 60_000);
1237
+ const { startupMs: runnerStartupMs, executionMs: runnerExecutionMs } = runnerObservationDurations(environment.DD_FLOW_CHECK_RUNNER_PROGRESS_PATH ? environment : {});
278
1238
  return new Promise((resolve) => {
279
1239
  const started = Date.now();
280
- const child = spawn("/bin/sh", ["-lc", checkShellScript()], { cwd, env: { ...environment, DD_FLOW_CHECK_COMMAND: command }, stdio: ["ignore", stdout, stderr], detached: process.platform !== "win32" });
1240
+ const child = spawn("/bin/sh", ["-c", checkShellScript()], { cwd, env: { ...environment, DD_FLOW_CHECK_COMMAND: command }, stdio: ["pipe", "pipe", "pipe"], detached: process.platform !== "win32" });
281
1241
  let error = null;
282
1242
  let stopping = false;
1243
+ let closed = false;
283
1244
  let progress;
284
- let escalation;
1245
+ let cleanup;
1246
+ let cleanupUnconfirmed = false;
1247
+ const renewal = createRenewalState();
1248
+ let renewalTimer;
1249
+ const clock = new ObservationClock({ timeoutMs: inactivityTimeoutMs });
1250
+ let outputCursor = 0;
1251
+ let runnerSequence = 0;
1252
+ let runnerPhase = "startup";
1253
+ const runnerProgress = environment.DD_FLOW_CHECK_RUNNER_PROGRESS_PATH;
1254
+ const runnerClock = runnerProgress ? new ObservationClock({ timeoutMs: runnerStartupMs }) : null;
1255
+ const outputs = [fs.createWriteStream("", { fd: stdout, autoClose: false }), fs.createWriteStream("", { fd: stderr, autoClose: false })];
1256
+ const flushed = outputs.map(output => new Promise(resolve => { output.once("finish", resolve); output.once("error", () => resolve()); }));
285
1257
  const stop = (cause) => {
286
1258
  if (stopping)
287
1259
  return;
@@ -291,22 +1263,57 @@ export function runCheck(command, cwd, environment, stdout, stderr, heartbeat, o
291
1263
  clearInterval(progress);
292
1264
  progress = undefined;
293
1265
  }
294
- if (child.pid) {
295
- terminateProcessGroup(child.pid, "SIGTERM");
296
- escalation = setTimeout(() => terminateProcessGroup(child.pid, "SIGKILL"), 5_000);
297
- }
1266
+ if (renewalTimer)
1267
+ clearTimeout(renewalTimer);
1268
+ if (observationTimer)
1269
+ clearInterval(observationTimer);
1270
+ cleanup = stopProcessGroup(child, 5_000).catch(cause => {
1271
+ cleanupUnconfirmed = true;
1272
+ error = `${error}; cleanup: ${JSON.stringify(errorRecord(cause))}`;
1273
+ resolve({ exitCode: null, error, aborted: true, cleanupUnconfirmed });
1274
+ });
298
1275
  };
1276
+ const observationTimer = setInterval(() => {
1277
+ if (stopping || closed || observation.phase?.() === "serving")
1278
+ return;
1279
+ if (runnerClock && runnerProgress) {
1280
+ for (const event of readRunnerProgress(runnerProgress, environment.DD_FLOW_CHECK_INVOCATION_ID ?? "", runnerSequence)) {
1281
+ runnerSequence = event.sequence;
1282
+ runnerPhase = event.phase;
1283
+ runnerClock.sample(runnerSequence, Date.parse(event.at));
1284
+ }
1285
+ // The outer guard includes the runner's bounded cleanup tail.
1286
+ runnerClock.timeoutMs = runnerPhase === "execution" ? runnerExecutionMs : runnerStartupMs;
1287
+ if (runnerClock.sample(runnerSequence))
1288
+ stop(new AppError("operation_observation_lost", "Structured test runner stopped making progress", 1, { phase: runnerPhase, sequence: runnerSequence }));
1289
+ return;
1290
+ }
1291
+ if (clock.sample(outputCursor))
1292
+ stop(new AppError("operation_observation_lost", "Owned command has no confirmed progress within its inactivity window", 1, { phase: observation.phase?.() ?? "finite", inactivity_timeout_ms: inactivityTimeoutMs }));
1293
+ }, Math.min(1000, inactivityTimeoutMs));
299
1294
  // Register handlers before checking pid or calling persistence callbacks.
300
1295
  // Failure is not completion until the spawned process has actually closed.
301
1296
  child.once("error", (value) => { error = value.message; });
302
- child.once("close", (exitCode, signal) => {
1297
+ for (const [index, stream] of [child.stdout, child.stderr].entries()) {
1298
+ outputs[index].once("error", stop);
1299
+ stream.on("data", () => { if (!runnerClock) {
1300
+ outputCursor++;
1301
+ clock.sample(outputCursor);
1302
+ } });
1303
+ stream.pipe(outputs[index]);
1304
+ }
1305
+ child.stdin.on("error", stop);
1306
+ child.once("close", async (exitCode, signal) => {
1307
+ closed = true;
303
1308
  if (progress)
304
1309
  clearInterval(progress);
305
- if (escalation)
306
- clearTimeout(escalation);
307
- if (stopping)
308
- terminateProcessGroup(child.pid, "SIGKILL");
309
- resolve({ exitCode, error: error ?? (signal ? `terminated by ${signal}` : null), aborted: stopping || Boolean(signal) || Boolean(error) });
1310
+ if (renewalTimer)
1311
+ clearTimeout(renewalTimer);
1312
+ if (observationTimer)
1313
+ clearInterval(observationTimer);
1314
+ await cleanup;
1315
+ await Promise.all(flushed);
1316
+ resolve({ exitCode, error: error ?? (signal ? `terminated by ${signal}` : null), aborted: stopping || Boolean(signal) || Boolean(error), ...(cleanupUnconfirmed ? { cleanupUnconfirmed } : {}) });
310
1317
  });
311
1318
  if (!child.pid) {
312
1319
  error = "check process did not provide a PID";
@@ -319,31 +1326,73 @@ export function runCheck(command, cwd, environment, stdout, stderr, heartbeat, o
319
1326
  stop(cause);
320
1327
  return;
321
1328
  }
322
- progress = setInterval(() => {
1329
+ if (observation.leaseExpiresAt) {
1330
+ const expiry = observation.leaseExpiresAt();
1331
+ if (!expiry || !renewalConfirmed(renewal, performance.now(), expiry)) {
1332
+ stop("check confirmation has no current authority");
1333
+ return;
1334
+ }
1335
+ }
1336
+ child.stdin.end("admitted\n");
1337
+ clock.sample(outputCursor);
1338
+ const renew = () => {
1339
+ if (stopping || closed)
1340
+ return;
1341
+ if (renewalTimer) {
1342
+ clearTimeout(renewalTimer);
1343
+ renewalTimer = undefined;
1344
+ }
323
1345
  try {
324
- if (!renewLease()) {
1346
+ renewal.uncertainSince ??= performance.now();
1347
+ const remaining = renewalRemaining(renewal);
1348
+ if (!remaining) {
1349
+ stop("managed process renewal budget exhausted");
1350
+ return;
1351
+ }
1352
+ const receipt = renewLease(Math.min(RENEWAL_POLICY.attemptMs, remaining));
1353
+ if (receipt === false || typeof receipt === "object" && !receipt.owned) {
325
1354
  stop("managed process lease lost");
326
1355
  return;
327
1356
  }
1357
+ if (typeof receipt === "object" && receipt.owned) {
1358
+ const expiry = Date.parse(receipt.lease_expires_at);
1359
+ if (!Number.isFinite(expiry))
1360
+ throw new AppError("managed_lease_receipt_invalid", "Check renewal returned invalid expiry", 1);
1361
+ if (expiry <= Date.now())
1362
+ throw Object.assign(new Error("Check renewal receipt expired before acknowledgement"), { code: "process_maintenance_timeout" });
1363
+ }
1364
+ if (!renewalConfirmed(renewal, performance.now(), typeof receipt === "object" && receipt.owned ? receipt.lease_expires_at : null)) {
1365
+ stop("managed process renewal budget exhausted");
1366
+ return;
1367
+ }
1368
+ }
1369
+ catch (cause) {
1370
+ if (!maintenanceRetryable(cause) || !renewalFailure(renewal)) {
1371
+ stop(cause);
1372
+ return;
1373
+ }
1374
+ renewalTimer = setTimeout(renew, Math.min(renewalDelay(renewal), renewalRemaining(renewal)));
1375
+ return;
1376
+ }
1377
+ // A progress callback error is not a retryable renewal failure.
1378
+ try {
328
1379
  heartbeat(Math.floor((Date.now() - started) / 1000));
329
1380
  }
330
1381
  catch (cause) {
331
1382
  stop(cause);
332
1383
  }
333
- }, 15_000);
1384
+ };
1385
+ progress = setInterval(renew, 15_000);
334
1386
  });
335
1387
  }
336
1388
  function checkShellScript() {
337
1389
  return [
1390
+ // The actual command cannot run before PID ownership and admission persist.
1391
+ 'IFS= read -r admission && [ "$admission" = admitted ] || exit 125',
338
1392
  'completion="${DD_FLOW_CHECK_COMPLETION_FILE:-}"',
339
- '/bin/sh -lc "$DD_FLOW_CHECK_COMMAND"',
1393
+ '/bin/sh -c "$DD_FLOW_CHECK_COMMAND"',
340
1394
  "code=$?",
341
1395
  'if [ -n "$completion" ]; then temporary="$completion.tmp.$$"; printf \'{"exit_code":%s,"finished_at":"%s"}\\n\' "$code" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$temporary" && mv "$temporary" "$completion"; fi',
342
1396
  'exit "$code"'
343
1397
  ].join("\n");
344
1398
  }
345
- function terminateProcessGroup(pid, signal) { if (!pid)
346
- return; try {
347
- process.kill(process.platform === "win32" ? pid : -pid, signal);
348
- }
349
- catch { /* already exited */ } }