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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (670) hide show
  1. package/CHANGELOG.md +965 -0
  2. package/README.md +139 -10
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/argument-values.js +38 -0
  5. package/dist/cli/command-inputs.js +417 -0
  6. package/dist/cli/command-metadata.js +303 -0
  7. package/dist/cli/command-option-descriptions.js +192 -0
  8. package/dist/cli/completion-cli.js +70 -0
  9. package/dist/cli/completion.js +57 -0
  10. package/dist/cli/delivery.js +108 -0
  11. package/dist/cli/diagnostic-invocation.js +62 -0
  12. package/dist/cli/diagnostic.js +131 -0
  13. package/dist/cli/error-output.js +62 -0
  14. package/dist/cli/executor-onboarding.js +76 -0
  15. package/dist/cli/flows.js +29 -0
  16. package/dist/cli/harnesses.js +84 -0
  17. package/dist/cli/help.js +521 -19
  18. package/dist/cli/hook-ingress.js +89 -0
  19. package/dist/cli/input-preparation.js +193 -0
  20. package/dist/cli/interactive-invocation.js +33 -0
  21. package/dist/cli/interactive.js +114 -0
  22. package/dist/cli/memory-fix-invocation.js +52 -0
  23. package/dist/cli/memory-fix.js +100 -0
  24. package/dist/cli/memory-flow-admission.js +54 -0
  25. package/dist/cli/memory-init-invocation.js +85 -0
  26. package/dist/cli/memory-init.js +250 -0
  27. package/dist/cli/memory-storage-admission.js +54 -0
  28. package/dist/cli/memory-storage-qualification.js +105 -0
  29. package/dist/cli/memory-storage-transition.js +123 -0
  30. package/dist/cli/memory-upgrade.js +189 -0
  31. package/dist/cli/models.js +39 -0
  32. package/dist/cli/native-hook-ingress.js +94 -0
  33. package/dist/cli/native-hook-preparation.js +82 -0
  34. package/dist/cli/operator-profiles.js +61 -0
  35. package/dist/cli/output-options.js +35 -0
  36. package/dist/cli/response-file.js +72 -0
  37. package/dist/cli/run-cli.js +2020 -555
  38. package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
  39. package/dist/cli/runtime-maintenance-ingress.js +52 -0
  40. package/dist/cli/runtime-process.js +153 -0
  41. package/dist/cli/setup.js +150 -0
  42. package/dist/cli.js +103 -2
  43. package/dist/domain/delivery-flows.js +8 -0
  44. package/dist/domain/diagnostic-aspects.js +103 -0
  45. package/dist/domain/diagnostic-flows.js +8 -0
  46. package/dist/domain/distill-flow.js +20 -0
  47. package/dist/domain/interactive-flow.js +1 -0
  48. package/dist/domain/memory-fix-flow.js +2 -0
  49. package/dist/domain/review-fix-flow.js +2 -0
  50. package/dist/domain/source-snapshot-flows.js +4 -0
  51. package/dist/domain/stage-catalog.js +1 -1
  52. package/dist/domain/stage-handler-identities.js +28 -0
  53. package/dist/harness-runtime/DD-ZCODE.md +112 -0
  54. package/dist/harness-runtime/OPENCODE-LICENSE.txt +24 -0
  55. package/dist/harness-runtime/bin/dd-agy.mjs +71 -0
  56. package/dist/harness-runtime/bin/dd-codex.mjs +27 -0
  57. package/dist/harness-runtime/bin/dd-droid.mjs +34 -0
  58. package/dist/harness-runtime/bin/dd-grok.mjs +50 -0
  59. package/dist/harness-runtime/bin/dd-opencode.mjs +22 -0
  60. package/dist/harness-runtime/bin/dd-zcode.mjs +95 -0
  61. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  62. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  63. package/dist/harness-runtime/lib/agy-error-boundary.mjs +87 -0
  64. package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
  65. package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
  66. package/dist/harness-runtime/lib/agy-native-inspection.mjs +225 -0
  67. package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
  68. package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
  69. package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +74 -0
  70. package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
  71. package/dist/harness-runtime/lib/daemon-observation.d.mts +3 -0
  72. package/dist/harness-runtime/lib/daemon-observation.mjs +171 -0
  73. package/dist/harness-runtime/lib/daemon-operations.d.mts +2 -0
  74. package/dist/harness-runtime/lib/daemon-operations.mjs +512 -0
  75. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +7 -0
  76. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +1362 -0
  77. package/dist/harness-runtime/lib/dd-agy.mjs +105 -0
  78. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  79. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +271 -0
  80. package/dist/harness-runtime/lib/dd-codex.d.mts +1 -0
  81. package/dist/harness-runtime/lib/dd-codex.mjs +986 -0
  82. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +155 -0
  83. package/dist/harness-runtime/lib/dd-droid.mjs +739 -0
  84. package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
  85. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +667 -0
  86. package/dist/harness-runtime/lib/dd-grok.mjs +182 -0
  87. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +772 -0
  88. package/dist/harness-runtime/lib/dd-opencode.mjs +411 -0
  89. package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
  90. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +845 -0
  91. package/dist/harness-runtime/lib/dd-zcode.mjs +1239 -0
  92. package/dist/harness-runtime/lib/delegation-instructions.d.mts +16 -0
  93. package/dist/harness-runtime/lib/delegation-instructions.mjs +179 -0
  94. package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
  95. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  96. package/dist/harness-runtime/lib/driver-recovery.mjs +121 -0
  97. package/dist/harness-runtime/lib/droid-observation.mjs +70 -0
  98. package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
  99. package/dist/harness-runtime/lib/entity-references.mjs +9 -0
  100. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  101. package/dist/harness-runtime/lib/lease-renewal.mjs +37 -0
  102. package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
  103. package/dist/harness-runtime/lib/lifecycle-command.mjs +273 -0
  104. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +11 -0
  105. package/dist/harness-runtime/lib/lifecycle-contract.mjs +20 -0
  106. package/dist/harness-runtime/lib/managed-daemon.d.mts +16 -0
  107. package/dist/harness-runtime/lib/managed-daemon.mjs +698 -0
  108. package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
  109. package/dist/harness-runtime/lib/model-observations.mjs +113 -0
  110. package/dist/harness-runtime/lib/native-children.d.mts +34 -0
  111. package/dist/harness-runtime/lib/native-children.mjs +265 -0
  112. package/dist/harness-runtime/lib/native-hook-command.d.mts +8 -0
  113. package/dist/harness-runtime/lib/native-hook-command.mjs +306 -0
  114. package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
  115. package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
  116. package/dist/harness-runtime/lib/native-streams.mjs +38 -0
  117. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  118. package/dist/harness-runtime/lib/observation-clock.mjs +95 -0
  119. package/dist/harness-runtime/lib/opencode-plugin.mjs +112 -0
  120. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  121. package/dist/harness-runtime/lib/operation-errors.d.mts +16 -0
  122. package/dist/harness-runtime/lib/operation-errors.mjs +140 -0
  123. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  124. package/dist/harness-runtime/lib/operation-progress.mjs +118 -0
  125. package/dist/harness-runtime/lib/process-json.d.mts +5 -0
  126. package/dist/harness-runtime/lib/process-json.mjs +200 -0
  127. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  128. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +23 -0
  129. package/dist/harness-runtime/lib/provider-overload-policy.mjs +242 -0
  130. package/dist/harness-runtime/lib/runner-events.mjs +202 -0
  131. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  132. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  133. package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
  134. package/dist/harness-runtime/lib/setup-progress.mjs +53 -0
  135. package/dist/harness-runtime/lib/tool-observations.d.mts +21 -0
  136. package/dist/harness-runtime/lib/tool-observations.mjs +238 -0
  137. package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
  138. package/dist/harness-runtime/lib/zcode-usage.mjs +55 -0
  139. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-1y4a697q.js +81 -0
  140. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-2ny4vxct.js +92 -0
  141. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-6mptkzbc.js +0 -0
  142. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-7x67cf1q.js +52 -0
  143. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9rqn6x4v.js +16 -0
  144. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9vdz0zwn.js +1661 -0
  145. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ecs5ftka.js +1466 -0
  146. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-frbwqjmf.js +145 -0
  147. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gne7h7mq.js +50 -0
  148. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gx9wnkzr.js +105 -0
  149. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-m2eywc8w.js +0 -0
  150. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-nwcpakzx.js +12 -0
  151. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-q4g3afnw.js +623 -0
  152. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-qav6mfdz.js +0 -0
  153. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-sxt88q69.js +12 -0
  154. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-t1xa31vm.js +39 -0
  155. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ve4aws36.js +103 -0
  156. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vmytr4q8.js +4 -0
  157. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vt9jbch8.js +36 -0
  158. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vyas3vgt.js +0 -0
  159. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-wjyeq25y.js +30 -0
  160. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-xgnj2w4v.js +20 -0
  161. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-y2etg5bs.js +9 -0
  162. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ydhz1zrh.js +238 -0
  163. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-z6qdwcsf.js +62 -0
  164. package/dist/harness-runtime/vendor/opencode-client/contract.d.ts +1 -0
  165. package/dist/harness-runtime/vendor/opencode-client/contract.js +12 -0
  166. package/dist/harness-runtime/vendor/opencode-client/effect/api/api.d.ts +2806 -0
  167. package/dist/harness-runtime/vendor/opencode-client/effect/api/api.js +0 -0
  168. package/dist/harness-runtime/vendor/opencode-client/effect/api.d.ts +4 -0
  169. package/dist/harness-runtime/vendor/opencode-client/effect/api.js +0 -0
  170. package/dist/harness-runtime/vendor/opencode-client/effect/client.d.ts +2530 -0
  171. package/dist/harness-runtime/vendor/opencode-client/effect/client.js +15 -0
  172. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.d.ts +7 -0
  173. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.js +6 -0
  174. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.d.ts +2529 -0
  175. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.js +8 -0
  176. package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.d.ts +2 -0
  177. package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.js +12 -0
  178. package/dist/harness-runtime/vendor/opencode-client/effect/index.d.ts +35 -0
  179. package/dist/harness-runtime/vendor/opencode-client/effect/index.js +75 -0
  180. package/dist/harness-runtime/vendor/opencode-client/effect/rpc.d.ts +20 -0
  181. package/dist/harness-runtime/vendor/opencode-client/effect/rpc.js +11 -0
  182. package/dist/harness-runtime/vendor/opencode-client/effect/service.d.ts +90 -0
  183. package/dist/harness-runtime/vendor/opencode-client/effect/service.js +187 -0
  184. package/dist/harness-runtime/vendor/opencode-client/package.json +84 -0
  185. package/dist/harness-runtime/vendor/opencode-client/promise/api.d.ts +21 -0
  186. package/dist/harness-runtime/vendor/opencode-client/promise/api.js +0 -0
  187. package/dist/harness-runtime/vendor/opencode-client/promise/client.d.ts +252 -0
  188. package/dist/harness-runtime/vendor/opencode-client/promise/client.js +14 -0
  189. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.d.ts +8 -0
  190. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.js +6 -0
  191. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.d.ts +259 -0
  192. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.js +7 -0
  193. package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.d.ts +3 -0
  194. package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.js +79 -0
  195. package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.d.ts +8932 -0
  196. package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.js +70 -0
  197. package/dist/harness-runtime/vendor/opencode-client/promise/index.d.ts +6 -0
  198. package/dist/harness-runtime/vendor/opencode-client/promise/index.js +83 -0
  199. package/dist/harness-runtime/vendor/opencode-client/promise/rpc.d.ts +32 -0
  200. package/dist/harness-runtime/vendor/opencode-client/promise/rpc.js +7 -0
  201. package/dist/harness-runtime/vendor/opencode-client/promise/service.d.ts +24 -0
  202. package/dist/harness-runtime/vendor/opencode-client/promise/service.js +187 -0
  203. package/dist/harness-runtime/vendor/opencode-client/pty-handoff.d.ts +12 -0
  204. package/dist/harness-runtime/vendor/opencode-client/pty-handoff.js +14 -0
  205. package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.d.ts +20 -0
  206. package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.js +14 -0
  207. package/dist/harness-runtime/vendor/opencode-client/service-contender.d.ts +37 -0
  208. package/dist/harness-runtime/vendor/opencode-client/service-contender.js +10 -0
  209. package/dist/harness-runtime/vendor/opencode-client/service-probe.d.ts +70 -0
  210. package/dist/harness-runtime/vendor/opencode-client/service-probe.js +16 -0
  211. package/dist/harness-runtime/vendor/opencode-client/service-timing.d.ts +12 -0
  212. package/dist/harness-runtime/vendor/opencode-client/service-timing.js +10 -0
  213. package/dist/harness-runtime/vendor/opencode-client/service-version.d.ts +2 -0
  214. package/dist/harness-runtime/vendor/opencode-client/service-version.js +6 -0
  215. package/dist/harness-runtime/vendor/opencode-client/service.d.ts +52 -0
  216. package/dist/harness-runtime/vendor/opencode-client/service.js +1 -0
  217. package/dist/harness-runtime/vendor/opencode-client/shared-events.d.ts +11 -0
  218. package/dist/harness-runtime/vendor/opencode-client/shared-events.js +8 -0
  219. package/dist/harness-runtime/vendor/opencode-client/solid/connection.d.ts +37 -0
  220. package/dist/harness-runtime/vendor/opencode-client/solid/connection.js +10 -0
  221. package/dist/harness-runtime/vendor/opencode-client/solid/data.d.ts +211 -0
  222. package/dist/harness-runtime/vendor/opencode-client/solid/data.js +18 -0
  223. package/dist/harness-runtime/vendor/opencode-client/solid/index.d.ts +3 -0
  224. package/dist/harness-runtime/vendor/opencode-client/solid/index.js +32 -0
  225. package/dist/harness-runtime/vendor/opencode-client/solid/pty.d.ts +22 -0
  226. package/dist/harness-runtime/vendor/opencode-client/solid/pty.js +8 -0
  227. package/dist/harness-runtime/vendor/opencode-plugin.mjs +3 -0
  228. package/dist/protocol/local-files.js +18 -9
  229. package/dist/runtime/context.js +8 -4
  230. package/dist/schemas/agent-profile.schema.json +1 -1
  231. package/dist/schemas/code-review-decision.schema.json +9 -0
  232. package/dist/schemas/code-review-result.schema.json +1 -1
  233. package/dist/schemas/code-verification.schema.json +7 -5
  234. package/dist/schemas/code-work-batch.schema.json +2 -2
  235. package/dist/schemas/code-work-result.schema.json +2 -1
  236. package/dist/schemas/delivery-stage-result.schema.json +286 -0
  237. package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
  238. package/dist/schemas/diagnostic-work-result.schema.json +296 -0
  239. package/dist/schemas/discovery-result.schema.json +184 -0
  240. package/dist/schemas/distill-stage-result.schema.json +667 -0
  241. package/dist/schemas/distill-work-result.schema.json +428 -0
  242. package/dist/schemas/execution-bundle.schema.json +215 -0
  243. package/dist/schemas/execution-settings.schema.json +162 -0
  244. package/dist/schemas/flow-descriptor.schema.json +54 -0
  245. package/dist/schemas/flow-run.schema.json +29 -0
  246. package/dist/schemas/harness-catalog.schema.json +80 -0
  247. package/dist/schemas/interactive-stage-result.schema.json +47 -0
  248. package/dist/schemas/interactive-work-result.schema.json +63 -0
  249. package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
  250. package/dist/schemas/mb-init-compose-result.schema.json +457 -0
  251. package/dist/schemas/mb-init-discover-result.schema.json +500 -0
  252. package/dist/schemas/mb-init-finish-result.schema.json +422 -0
  253. package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
  254. package/dist/schemas/mb-init-review-result.schema.json +396 -0
  255. package/dist/schemas/mb-init-verify-result.schema.json +391 -0
  256. package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
  257. package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
  258. package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
  259. package/dist/schemas/memory-composition-work-result.schema.json +38 -0
  260. package/dist/schemas/memory-repair-work-result.schema.json +56 -0
  261. package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
  262. package/dist/schemas/memory-review-work-result.schema.json +43 -0
  263. package/dist/schemas/model-catalog.schema.json +153 -0
  264. package/dist/schemas/plan-review-decision.schema.json +12 -3
  265. package/dist/schemas/plan-review-result.schema.json +2 -2
  266. package/dist/schemas/profile-settings.schema.json +122 -0
  267. package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
  268. package/dist/schemas/release-impact.schema.json +3 -3
  269. package/dist/schemas/release-manifest.schema.json +291 -0
  270. package/dist/schemas/review-fix-stage-result.schema.json +210 -0
  271. package/dist/schemas/run-control-receipt.schema.json +119 -0
  272. package/dist/schemas/run-control-request.schema.json +36 -0
  273. package/dist/schemas/run-engine-binding.schema.json +1 -1
  274. package/dist/schemas/stage-start-response.schema.json +4 -0
  275. package/dist/schemas/vnext-protocol-plan.schema.json +3 -3
  276. package/dist/services/accepted-intent.js +139 -0
  277. package/dist/services/assigned-lifecycle.js +382 -0
  278. package/dist/services/branch-context.js +3 -3
  279. package/dist/services/canon.js +54 -15
  280. package/dist/services/check-environment.js +21 -0
  281. package/dist/services/check-runner-evidence.js +344 -0
  282. package/dist/services/check-runner-proof.js +373 -0
  283. package/dist/services/cleanup.js +133 -40
  284. package/dist/services/cli-operation-classifier.js +32 -64
  285. package/dist/services/cli-upgrade-allowlist.js +27 -0
  286. package/dist/services/code-checks.js +1426 -121
  287. package/dist/services/codex-capacity.js +367 -0
  288. package/dist/services/codex-hook-delivery.js +41 -0
  289. package/dist/services/command-context.js +80 -0
  290. package/dist/services/compatibility-preflight.js +16 -3
  291. package/dist/services/config.js +11 -8
  292. package/dist/services/continuation-outcome.js +25 -0
  293. package/dist/services/controller-child-capacity.js +71 -0
  294. package/dist/services/controller-child-observer.js +121 -0
  295. package/dist/services/controller-child-repair.js +117 -0
  296. package/dist/services/controller-execution.js +97 -0
  297. package/dist/services/controller-fanout.js +277 -0
  298. package/dist/services/controller-memory-task.js +45 -0
  299. package/dist/services/controller-memory-worker.js +51 -0
  300. package/dist/services/controller-result-authority.js +127 -0
  301. package/dist/services/controller-work-failure.js +198 -0
  302. package/dist/services/dashboard.js +145 -58
  303. package/dist/services/delivery-access.js +123 -0
  304. package/dist/services/delivery-approval.js +68 -0
  305. package/dist/services/delivery-artifact.js +86 -0
  306. package/dist/services/delivery-checks.js +178 -0
  307. package/dist/services/delivery-execution.js +181 -0
  308. package/dist/services/delivery-intents.js +212 -0
  309. package/dist/services/delivery-operation.js +81 -0
  310. package/dist/services/delivery-policy.js +91 -0
  311. package/dist/services/delivery-procedures.js +43 -0
  312. package/dist/services/delivery-request.js +99 -0
  313. package/dist/services/delivery-result.js +144 -0
  314. package/dist/services/delivery-stage-commands.js +26 -0
  315. package/dist/services/delivery-stage-entry.js +56 -0
  316. package/dist/services/delivery-stage-handlers.js +135 -0
  317. package/dist/services/delivery-stage-results.js +94 -0
  318. package/dist/services/descriptor-coordinator-session.js +37 -0
  319. package/dist/services/descriptor-root-work.js +53 -0
  320. package/dist/services/descriptor-run-settlement.js +107 -0
  321. package/dist/services/descriptor-run-start.js +495 -0
  322. package/dist/services/descriptor-stage-coordinator.js +318 -0
  323. package/dist/services/descriptor-stage-settlement.js +268 -0
  324. package/dist/services/descriptor-stage-start.js +175 -0
  325. package/dist/services/diagnostic-evidence.js +73 -0
  326. package/dist/services/diagnostic-repair-intake.js +123 -0
  327. package/dist/services/diagnostic-repair-source-intake.js +102 -0
  328. package/dist/services/diagnostic-report-publication.js +72 -0
  329. package/dist/services/diagnostic-report.js +97 -0
  330. package/dist/services/diagnostic-run-bundle.js +113 -0
  331. package/dist/services/diagnostic-snapshot.js +159 -0
  332. package/dist/services/diagnostic-stage-handlers.js +109 -0
  333. package/dist/services/diagnostic-stage-results.js +158 -0
  334. package/dist/services/diagnostic-start.js +134 -0
  335. package/dist/services/diagnostic-work-batch.js +34 -0
  336. package/dist/services/diagnostic-work-collection.js +97 -0
  337. package/dist/services/diagnostic-work-registration.js +101 -0
  338. package/dist/services/diagnostic-work-results.js +93 -0
  339. package/dist/services/discovery/agy-discovery.js +2 -0
  340. package/dist/services/discovery/codex-discovery.js +2 -0
  341. package/dist/services/discovery/droid-discovery.js +2 -0
  342. package/dist/services/discovery/grok-discovery.js +2 -0
  343. package/dist/services/discovery/opencode-discovery.js +2 -0
  344. package/dist/services/discovery/static.js +31 -0
  345. package/dist/services/discovery/zcode-discovery.js +2 -0
  346. package/dist/services/distill-document-publication.js +280 -0
  347. package/dist/services/distill-promotion.js +197 -0
  348. package/dist/services/distill-publication-custody.js +162 -0
  349. package/dist/services/distill-stage-handlers.js +249 -0
  350. package/dist/services/distill-stage-results.js +105 -0
  351. package/dist/services/distill-user-inputs.js +4 -0
  352. package/dist/services/distill-work-batch.js +42 -0
  353. package/dist/services/distill-work-collection.js +74 -0
  354. package/dist/services/distill-work-registration.js +93 -0
  355. package/dist/services/distill-work-results.js +98 -0
  356. package/dist/services/engines.js +354 -163
  357. package/dist/services/eval-snapshots.js +1326 -150
  358. package/dist/services/execution-bundles.js +275 -0
  359. package/dist/services/execution-policy.js +185 -0
  360. package/dist/services/executor-onboarding.js +56 -0
  361. package/dist/services/external-work-launch.js +316 -0
  362. package/dist/services/flow-catalog.js +54 -0
  363. package/dist/services/flow-descriptors.js +82 -0
  364. package/dist/services/frozen-engine.js +404 -0
  365. package/dist/services/generic-execution.js +165 -0
  366. package/dist/services/harness-adapter.js +342 -35
  367. package/dist/services/harness-config.js +2 -0
  368. package/dist/services/harness-discovery.js +29 -0
  369. package/dist/services/harness-doctor.js +75 -0
  370. package/dist/services/harness-probe-effects.js +51 -0
  371. package/dist/services/harness-version-probe.js +100 -0
  372. package/dist/services/hook-memory-routing.js +188 -0
  373. package/dist/services/hook-recovery-guard.js +14 -0
  374. package/dist/services/hook-session-observation.js +24 -0
  375. package/dist/services/hooks.js +813 -327
  376. package/dist/services/hybrid-contracts.js +479 -0
  377. package/dist/services/hybrid-digest.js +14 -0
  378. package/dist/services/ids.js +29 -1
  379. package/dist/services/installation-lock.js +95 -0
  380. package/dist/services/installed-flow-source.js +261 -0
  381. package/dist/services/interactive-attached-session.js +215 -0
  382. package/dist/services/interactive-baseline-custody.js +54 -0
  383. package/dist/services/interactive-before-versions.js +148 -0
  384. package/dist/services/interactive-bundle-data.js +85 -0
  385. package/dist/services/interactive-checks.js +179 -0
  386. package/dist/services/interactive-execution.js +33 -0
  387. package/dist/services/interactive-finish-integration.js +236 -0
  388. package/dist/services/interactive-git-baseline.js +230 -0
  389. package/dist/services/interactive-git-dispatch.js +278 -0
  390. package/dist/services/interactive-input-delivery.js +82 -0
  391. package/dist/services/interactive-mutation-scope.js +231 -0
  392. package/dist/services/interactive-request.js +287 -0
  393. package/dist/services/interactive-run-bundle.js +109 -0
  394. package/dist/services/interactive-stage-entry.js +41 -0
  395. package/dist/services/interactive-stage-handlers.js +289 -0
  396. package/dist/services/interactive-stage-observation.js +136 -0
  397. package/dist/services/interactive-stage-results.js +111 -0
  398. package/dist/services/interactive-user-inputs.js +142 -0
  399. package/dist/services/interactive-work-batch.js +43 -0
  400. package/dist/services/interactive-work-collection.js +119 -0
  401. package/dist/services/interactive-work-completion.js +180 -0
  402. package/dist/services/interactive-work-context.js +249 -0
  403. package/dist/services/interactive-work-history.js +112 -0
  404. package/dist/services/interactive-work-registration.js +124 -0
  405. package/dist/services/interactive-work-results.js +115 -0
  406. package/dist/services/interactive-work-start.js +155 -0
  407. package/dist/services/lanes.js +78 -65
  408. package/dist/services/lifecycle-command.js +1 -288
  409. package/dist/services/lifecycle-contract.js +1 -0
  410. package/dist/services/lifecycle-invocations.js +2549 -0
  411. package/dist/services/managed-daemon-binding.js +129 -0
  412. package/dist/services/managed-lease-monitor.js +133 -0
  413. package/dist/services/managed-processes.js +449 -69
  414. package/dist/services/memory-accepted-composition.js +78 -0
  415. package/dist/services/memory-baseline-preservation.js +65 -0
  416. package/dist/services/memory-completed-correction-wave.js +328 -0
  417. package/dist/services/memory-compose-handler.js +380 -0
  418. package/dist/services/memory-composition-acceptance.js +375 -0
  419. package/dist/services/memory-composition-graph.js +165 -0
  420. package/dist/services/memory-composition-registration.js +161 -0
  421. package/dist/services/memory-composition-work-custody.js +156 -0
  422. package/dist/services/memory-composition-work-results.js +108 -0
  423. package/dist/services/memory-corrected-review-acceptance.js +129 -0
  424. package/dist/services/memory-corrected-review-batch.js +37 -0
  425. package/dist/services/memory-corrected-review-findings.js +108 -0
  426. package/dist/services/memory-corrected-review-inputs.js +215 -0
  427. package/dist/services/memory-corrected-review-projection.js +78 -0
  428. package/dist/services/memory-corrected-review-receipt.js +178 -0
  429. package/dist/services/memory-corrected-review-registration.js +192 -0
  430. package/dist/services/memory-correction-check-receipt.js +235 -0
  431. package/dist/services/memory-correction-input-owner.js +6 -0
  432. package/dist/services/memory-custody-snapshot.js +36 -0
  433. package/dist/services/memory-discover-handler.js +104 -0
  434. package/dist/services/memory-finish-evidence.js +457 -0
  435. package/dist/services/memory-finish-handler.js +229 -0
  436. package/dist/services/memory-finish-integration.js +491 -0
  437. package/dist/services/memory-finish-readiness.js +169 -0
  438. package/dist/services/memory-fix-finish-integration.js +129 -0
  439. package/dist/services/memory-fix-git-dispatch.js +139 -0
  440. package/dist/services/memory-fix-intake-custody.js +11 -0
  441. package/dist/services/memory-fix-intake-settlement.js +127 -0
  442. package/dist/services/memory-fix-publication.js +196 -0
  443. package/dist/services/memory-fix-repair-admission.js +116 -0
  444. package/dist/services/memory-fix-request.js +141 -0
  445. package/dist/services/memory-fix-run-bundle.js +83 -0
  446. package/dist/services/memory-fix-scope.js +78 -0
  447. package/dist/services/memory-fix-sources.js +85 -0
  448. package/dist/services/memory-fix-stage-entry.js +24 -0
  449. package/dist/services/memory-fix-stage-handlers.js +282 -0
  450. package/dist/services/memory-fix-stage-results.js +106 -0
  451. package/dist/services/memory-fix-workspace.js +127 -0
  452. package/dist/services/memory-flow-user-inputs.js +88 -0
  453. package/dist/services/memory-git-integration.js +220 -0
  454. package/dist/services/memory-historical-corrected-review.js +212 -0
  455. package/dist/services/memory-historical-original-review.js +143 -0
  456. package/dist/services/memory-historical-repair-registration.js +105 -0
  457. package/dist/services/memory-historical-second-review.js +147 -0
  458. package/dist/services/memory-init-bootstrap-readiness.js +210 -0
  459. package/dist/services/memory-init-contexts.js +181 -0
  460. package/dist/services/memory-init-controller-start.js +119 -0
  461. package/dist/services/memory-init-engine-release.js +223 -0
  462. package/dist/services/memory-init-git-policy.js +165 -0
  463. package/dist/services/memory-init-lint-capability.js +203 -0
  464. package/dist/services/memory-init-native-capacity.js +309 -0
  465. package/dist/services/memory-init-preflight.js +334 -0
  466. package/dist/services/memory-init-project-bootstrap.js +599 -0
  467. package/dist/services/memory-init-request-candidate.js +213 -0
  468. package/dist/services/memory-init-request-input.js +82 -0
  469. package/dist/services/memory-init-resource-storage.js +125 -0
  470. package/dist/services/memory-init-run-allocation.js +81 -0
  471. package/dist/services/memory-init-run-bundle.js +135 -0
  472. package/dist/services/memory-init-start.js +181 -0
  473. package/dist/services/memory-init-startup-custody.js +80 -0
  474. package/dist/services/memory-init-startup-input.js +240 -0
  475. package/dist/services/memory-init-storage.js +91 -0
  476. package/dist/services/memory-init-stores.js +359 -0
  477. package/dist/services/memory-init-worktrunk-capability.js +174 -0
  478. package/dist/services/memory-layer-publication.js +611 -0
  479. package/dist/services/memory-layer.js +225 -0
  480. package/dist/services/memory-lint-config.js +194 -0
  481. package/dist/services/memory-native-review-copy.js +127 -0
  482. package/dist/services/memory-normal-flow-entry.js +87 -0
  483. package/dist/services/memory-ordinary-defaults.js +129 -0
  484. package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
  485. package/dist/services/memory-preflight-handler.js +274 -0
  486. package/dist/services/memory-repair-current-view.js +299 -0
  487. package/dist/services/memory-repair-document-delta.js +489 -0
  488. package/dist/services/memory-repair-history.js +496 -0
  489. package/dist/services/memory-repair-physical-binding.js +194 -0
  490. package/dist/services/memory-repair-publication-files.js +152 -0
  491. package/dist/services/memory-repair-publication-intent.js +27 -0
  492. package/dist/services/memory-repair-publication-plan.js +162 -0
  493. package/dist/services/memory-repair-publication-source.js +225 -0
  494. package/dist/services/memory-repair-publication.js +795 -0
  495. package/dist/services/memory-repair-registration.js +240 -0
  496. package/dist/services/memory-repair-work-baseline.js +415 -0
  497. package/dist/services/memory-repair-work-completion.js +117 -0
  498. package/dist/services/memory-repair-work-custody.js +73 -0
  499. package/dist/services/memory-repair-work-results.js +108 -0
  500. package/dist/services/memory-repair-workspace.js +261 -0
  501. package/dist/services/memory-repair-writer-copy.js +736 -0
  502. package/dist/services/memory-result-binding.js +239 -0
  503. package/dist/services/memory-retained-review-authority.js +138 -0
  504. package/dist/services/memory-retained-review-results.js +70 -0
  505. package/dist/services/memory-retained-second-baseline.js +102 -0
  506. package/dist/services/memory-retained-second-repairs.js +162 -0
  507. package/dist/services/memory-review-acceptance.js +214 -0
  508. package/dist/services/memory-review-correction-plan.js +206 -0
  509. package/dist/services/memory-review-handler.js +585 -0
  510. package/dist/services/memory-review-inputs.js +246 -0
  511. package/dist/services/memory-review-limits.js +10 -0
  512. package/dist/services/memory-review-receipt.js +180 -0
  513. package/dist/services/memory-review-registration.js +117 -0
  514. package/dist/services/memory-review-work-custody.js +126 -0
  515. package/dist/services/memory-review-work-input.js +91 -0
  516. package/dist/services/memory-review-work-results.js +95 -0
  517. package/dist/services/memory-second-corrected-review-inputs.js +167 -0
  518. package/dist/services/memory-second-correction-plan.js +77 -0
  519. package/dist/services/memory-second-repair-baseline.js +91 -0
  520. package/dist/services/memory-second-repair-launch.js +614 -0
  521. package/dist/services/memory-second-repair-registration.js +88 -0
  522. package/dist/services/memory-second-repair-source.js +187 -0
  523. package/dist/services/memory-second-review-acceptance.js +115 -0
  524. package/dist/services/memory-source-observations.js +117 -0
  525. package/dist/services/memory-stage-dispatch.js +65 -0
  526. package/dist/services/memory-stage-results.js +290 -0
  527. package/dist/services/memory-tool-probe.js +25 -0
  528. package/dist/services/memory-upgrade-analysis-handler.js +243 -0
  529. package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
  530. package/dist/services/memory-upgrade-evidence.js +100 -0
  531. package/dist/services/memory-upgrade-inventory.js +442 -0
  532. package/dist/services/memory-upgrade-publication-custody.js +73 -0
  533. package/dist/services/memory-upgrade-publication.js +413 -0
  534. package/dist/services/memory-upgrade-selection.js +430 -0
  535. package/dist/services/memory-upgrade-start.js +60 -0
  536. package/dist/services/memory-upgrade-storage-readiness.js +340 -0
  537. package/dist/services/memory-verification-acceptance.js +156 -0
  538. package/dist/services/memory-verification-checks.js +317 -0
  539. package/dist/services/memory-verification-receipt.js +160 -0
  540. package/dist/services/memory-verify-handler.js +137 -0
  541. package/dist/services/memory-work-result-json.js +12 -0
  542. package/dist/services/memory-work-result-preparation.js +64 -0
  543. package/dist/services/merge-queue.js +228 -149
  544. package/dist/services/merge-server.js +163 -35
  545. package/dist/services/migrations.js +164 -114
  546. package/dist/services/model-catalog.js +109 -0
  547. package/dist/services/model-discovery.js +46 -0
  548. package/dist/services/native-daemon-history.js +50 -0
  549. package/dist/services/native-session-control.js +39 -0
  550. package/dist/services/operator-profiles.js +236 -0
  551. package/dist/services/physical-startup.js +45 -0
  552. package/dist/services/plan-runtime.js +26 -11
  553. package/dist/services/plans.js +28 -25
  554. package/dist/services/project-flow-source.js +195 -0
  555. package/dist/services/project-git-integration.js +92 -0
  556. package/dist/services/project-registry.js +97 -0
  557. package/dist/services/project-summary.js +1 -1
  558. package/dist/services/projects.js +17 -87
  559. package/dist/services/prompt-delivery.js +71 -0
  560. package/dist/services/prompts.js +28 -17
  561. package/dist/services/protocols.js +163 -87
  562. package/dist/services/provider-capacity.js +463 -0
  563. package/dist/services/recovery-observation-budget.js +121 -0
  564. package/dist/services/recovery-snapshot-database.js +588 -0
  565. package/dist/services/release-archive-producer.js +52 -0
  566. package/dist/services/release-archive.js +143 -0
  567. package/dist/services/release-cache.js +150 -0
  568. package/dist/services/release-compatibility.js +18 -0
  569. package/dist/services/release-download.js +67 -0
  570. package/dist/services/release-envelope.js +104 -0
  571. package/dist/services/release-manifest.js +45 -0
  572. package/dist/services/release-payload.js +114 -0
  573. package/dist/services/release-producer.js +63 -0
  574. package/dist/services/release-provenance.js +160 -0
  575. package/dist/services/release-registry.js +66 -0
  576. package/dist/services/release-source.js +194 -0
  577. package/dist/services/repair-intents.js +29 -0
  578. package/dist/services/repair-receipt.js +13 -0
  579. package/dist/services/retained-native-hook.js +274 -0
  580. package/dist/services/review-copy.js +165 -0
  581. package/dist/services/review-deferral.js +23 -0
  582. package/dist/services/review-fix-handoff.js +82 -0
  583. package/dist/services/review-fix-intake-custody.js +17 -0
  584. package/dist/services/review-fix-stage-entry.js +43 -0
  585. package/dist/services/review-fix-stage-handlers.js +176 -0
  586. package/dist/services/review-fix-stage-results.js +86 -0
  587. package/dist/services/run-control-receipt.js +56 -0
  588. package/dist/services/run-control-worker.js +469 -0
  589. package/dist/services/run-control.js +1109 -0
  590. package/dist/services/run-controller-adapter.js +434 -0
  591. package/dist/services/run-controller-capture.js +186 -0
  592. package/dist/services/run-controller-process.js +226 -0
  593. package/dist/services/run-controller-recovery.js +335 -0
  594. package/dist/services/run-controller-state.js +106 -0
  595. package/dist/services/run-controller.js +1717 -0
  596. package/dist/services/run-engine-bindings.js +1 -1
  597. package/dist/services/run-fork.js +147 -0
  598. package/dist/services/run-health.js +149 -0
  599. package/dist/services/run-observations.js +152 -0
  600. package/dist/services/run-projection.js +22 -64
  601. package/dist/services/run-recovery-guard.js +107 -0
  602. package/dist/services/run-recovery-runtime.js +112 -0
  603. package/dist/services/run-recovery.js +485 -0
  604. package/dist/services/run-start-custody.js +189 -0
  605. package/dist/services/run-timeline.js +59 -0
  606. package/dist/services/runs.js +996 -271
  607. package/dist/services/runtime-budget.js +446 -0
  608. package/dist/services/runtime-command.js +43 -0
  609. package/dist/services/runtime-scope-capture.js +52 -0
  610. package/dist/services/runtime-scope-control.js +468 -0
  611. package/dist/services/runtime-scope-resume.js +605 -0
  612. package/dist/services/runtime-scope-stop.js +211 -0
  613. package/dist/services/runtime-scope-worker.js +314 -0
  614. package/dist/services/runtime-service.js +19 -14
  615. package/dist/services/schema-validation.js +10 -8
  616. package/dist/services/scope-writer-cleanup.js +27 -0
  617. package/dist/services/sessions.js +37 -91
  618. package/dist/services/setup.js +427 -0
  619. package/dist/services/stage-blocker.js +17 -7
  620. package/dist/services/stage-context.js +240 -18
  621. package/dist/services/stage-handlers.js +141 -0
  622. package/dist/services/stage-lifecycle.js +156 -119
  623. package/dist/services/stage-pause.js +222 -94
  624. package/dist/services/stage-report-renderer.js +230 -6
  625. package/dist/services/stage-work-graph.js +52 -0
  626. package/dist/services/technical-notes.js +31 -0
  627. package/dist/services/tooling.js +111 -44
  628. package/dist/services/usage.js +49 -101
  629. package/dist/services/verification-matrix.js +393 -0
  630. package/dist/services/version-status.js +1 -1
  631. package/dist/services/vnext-code-review.js +302 -91
  632. package/dist/services/vnext-code.js +325 -251
  633. package/dist/services/vnext-contracts.js +10 -1
  634. package/dist/services/vnext-execution-profile.js +80 -5
  635. package/dist/services/vnext-fanout.js +399 -20
  636. package/dist/services/vnext-merge.js +336 -114
  637. package/dist/services/vnext-plan-review.js +288 -133
  638. package/dist/services/vnext-plan.js +257 -110
  639. package/dist/services/vnext-protocolize.js +534 -96
  640. package/dist/services/vnext-specify.js +131 -84
  641. package/dist/services/vnext-workspace-policy.js +7 -2
  642. package/dist/services/work-registry.js +1950 -215
  643. package/dist/services/work-retry-context.js +55 -0
  644. package/dist/services/workspace-bootstrap.js +53 -15
  645. package/dist/services/workspace-files.js +85 -0
  646. package/dist/services/worktrees.js +108 -48
  647. package/dist/shared/entity-references.js +1 -0
  648. package/dist/shared/errors.js +27 -0
  649. package/dist/storage/agent-output.js +26 -0
  650. package/dist/storage/data-preservation.js +65 -0
  651. package/dist/storage/database.js +931 -40
  652. package/dist/storage/foreign-key-transaction.js +39 -0
  653. package/dist/storage/immutable-input.js +45 -0
  654. package/dist/storage/import-guard.js +159 -0
  655. package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
  656. package/dist/storage/native-cleanup.js +52 -0
  657. package/dist/storage/paired-writer-migration.js +424 -0
  658. package/dist/storage/paths.js +17 -4
  659. package/dist/storage/projection-lock.js +73 -0
  660. package/dist/storage/work-references.js +23 -0
  661. package/dist/storage/writer-contract.js +96 -0
  662. package/dist/storage/writer-migration.js +346 -0
  663. package/dist/storage/writer-restore.js +217 -0
  664. package/dist/verification-components.json +384 -0
  665. package/package.json +52 -13
  666. package/tools/create-canon-release.mjs +33 -0
  667. package/tools/probe-codex-history.mjs +37 -0
  668. package/tools/produce-hybrid-release.mjs +41 -0
  669. package/tools/repair-paused-run-status.mjs +84 -0
  670. package/tools/verification-jobs.mjs +342 -0
@@ -2,18 +2,30 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- import { parse, stringify } from "smol-toml";
5
+ import { parse, stringify, TomlError } from "smol-toml";
6
6
  import { AppError } from "../shared/errors.js";
7
+ import { entityReferenceVariants } from "../shared/entity-references.js";
7
8
  import { parseJsonObject } from "../shared/json.js";
8
- import { ensureDir, resolveProjectRoot } from "../storage/paths.js";
9
+ import { canonicalPath, ensureDir, resolveProjectRoot } from "../storage/paths.js";
10
+ import { resolveWorkReference } from "../storage/work-references.js";
9
11
  import { appendAudit } from "./audit.js";
12
+ import { memoryRepairAssignedHookCwd, memoryNativeReviewAssignedHookCwd } from "./hook-memory-routing.js";
13
+ import { prepareTextFile, prepareJsonText, prepareError } from "../cli/input-preparation.js";
14
+ import { nativeHookNonParticipation } from "../cli/native-hook-preparation.js";
10
15
  import { nativeSessionIdentity, storageSessionId } from "./session-identity.js";
11
- import { commandHasOption, commandOption, commandPosition, parseLifecycleCommand, unwrapShellCommand } from "./lifecycle-command.js";
12
- import { registerProject, requireProjectByRoot } from "./projects.js";
13
- import { activeFlowSessionsForProject, bindObservedFlowSession, flowSessionPayloadFromRegisterCommand, recordFlowSessionObservation } from "./sessions.js";
16
+ import { assertRecoveryHookCurrent } from "./hook-recovery-guard.js";
17
+ import { observeLifecycleInvocation, observedLifecycleInvocation, managedNativeInvocationScope } from "./lifecycle-invocations.js";
18
+ import { commandHasOption, commandOption, commandPosition, shellSuffixOffset, parseLifecycleCommand, lifecycleCommandFromPayload, continuationCommand } from "./lifecycle-command.js";
19
+ import { registerProject, requireProjectByRoot } from "./project-registry.js";
20
+ import { activeFlowSessionsForProject, recordFlowSessionObservation } from "./hook-session-observation.js";
14
21
  const defaultProfileName = "default";
15
22
  const maxSanitizedSummaryLength = 1600;
16
- const sharedEntries = [
23
+ function lifecycleObservationResult(receipt) {
24
+ return { observed: receipt.disposition === "admitted", duplicate: receipt.duplicate,
25
+ event_key: receipt.authoritativeEventKey, current_event_key: receipt.currentEventKey,
26
+ authoritative_event_key: receipt.authoritativeEventKey, receipt_disposition: receipt.disposition };
27
+ }
28
+ export const codexHomeSharedEntries = [
17
29
  "auth.json",
18
30
  "auth.bk",
19
31
  "sessions",
@@ -50,22 +62,28 @@ export function planCodexHome(context, input) {
50
62
  config_path: paths.configPath,
51
63
  hooks_path: paths.hooksPath,
52
64
  owned_files: ["config.toml", "hooks.json", ".dd-flow-home.json", "log/", ".tmp/", "tmp/"],
53
- shared_entries: sharedEntries,
65
+ shared_entries: codexHomeSharedEntries,
54
66
  exists: fs.existsSync(paths.targetHome),
55
67
  codex_env: { CODEX_HOME: paths.targetHome }
56
68
  };
57
69
  }
58
- export function initCodexHome(context, input) {
70
+ export function prepareCodexHome(context, input) {
59
71
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
60
72
  const profile = resolveProfileName(input.profile);
61
73
  const paths = resolveHomePaths(context, project, profile, input);
74
+ if (canonicalPath(paths.sourceHome, false) === canonicalPath(paths.targetHome, false))
75
+ throw new AppError("validation", "Managed Codex home must differ from its source home", 2);
76
+ const config = readTomlIfExists(path.join(paths.sourceHome, "config.toml"));
77
+ return { project, profile, paths, config };
78
+ }
79
+ export function initCodexHome(context, input, prepared = prepareCodexHome(context, input)) {
80
+ const { project, profile, paths, config } = prepared;
62
81
  const now = context.now();
63
82
  ensureDir(paths.targetHome);
64
83
  ensureDir(path.join(paths.targetHome, "log"));
65
84
  ensureDir(path.join(paths.targetHome, ".tmp"));
66
85
  ensureDir(path.join(paths.targetHome, "tmp"));
67
86
  const linked = createSharedLinks(paths.sourceHome, paths.targetHome);
68
- const config = readTomlIfExists(path.join(paths.sourceHome, "config.toml"));
69
87
  fs.writeFileSync(paths.configPath, stringify(withManagedConfig(config, paths.sourceHome, paths.targetHome)));
70
88
  writeJsonFile(paths.hooksPath, expectedHooksConfig(project.root));
71
89
  writeJsonFile(path.join(paths.targetHome, ".dd-flow-home.json"), {
@@ -136,7 +154,7 @@ export function printCodexHomeEnv(context, input) {
136
154
  command_prefix: `CODEX_HOME=${JSON.stringify(record.target_home)}`
137
155
  };
138
156
  }
139
- export function removeCodexHome(context, input) {
157
+ export function prepareCodexHomeRemoval(context, input) {
140
158
  if (!["keep-shared", "remove-owned"].includes(input.mode)) {
141
159
  throw new AppError("validation", "--mode must be keep-shared or remove-owned", 2);
142
160
  }
@@ -144,9 +162,30 @@ export function removeCodexHome(context, input) {
144
162
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
145
163
  const profile = resolveProfileName(input.profile);
146
164
  const record = homeProfile(context, project.id, profile);
165
+ if (record && record.status !== "removed" && fs.existsSync(record.target_home)) {
166
+ let marker;
167
+ try {
168
+ const value = JSON.parse(fs.readFileSync(path.join(record.target_home, ".dd-flow-home.json"), "utf8"));
169
+ if (!value || typeof value !== "object" || Array.isArray(value))
170
+ throw new Error("Invalid marker");
171
+ marker = value;
172
+ }
173
+ catch {
174
+ throw new AppError("runtime_artifact_invalid", "Managed Codex home ownership marker is missing or invalid; refusing removal", 1, { target_home: record.target_home });
175
+ }
176
+ if (marker.managed_by !== "dd-flow-cli" || marker.project_id !== project.id || marker.profile !== profile || marker.target_home !== record.target_home || marker.source_home !== record.source_home || canonicalPath(record.target_home, false) === canonicalPath(record.source_home, false)) {
177
+ throw new AppError("runtime_artifact_invalid", "Managed Codex home ownership does not match its registration; refusing removal", 1, { target_home: record.target_home });
178
+ }
179
+ }
180
+ return { project, profile, record, mode };
181
+ }
182
+ export function removeCodexHome(context, input) {
183
+ const { project, profile, record, mode } = prepareCodexHomeRemoval(context, input);
147
184
  if (!record) {
148
185
  return { ok: true, removed: false, profile, status: "not_initialized" };
149
186
  }
187
+ if (record.status === "removed")
188
+ return { ok: true, removed: false, profile, status: "already_removed" };
150
189
  const removedEntries = [];
151
190
  for (const relative of ["config.toml", "hooks.json", ".dd-flow-home.json", "log", ".tmp", "tmp"]) {
152
191
  const target = path.join(record.target_home, relative);
@@ -207,7 +246,7 @@ export function getCodexHooksStatus(context, input) {
207
246
  recorded
208
247
  };
209
248
  }
210
- export function installCodexHooks(context, input) {
249
+ export function prepareCodexHooks(context, input) {
211
250
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
212
251
  const target = resolveHookTarget(input.target);
213
252
  const location = requireHookLocation(context, project, target, resolveProfileName(input.profile));
@@ -217,9 +256,16 @@ export function installCodexHooks(context, input) {
217
256
  hooks_path: location.hooksPath
218
257
  });
219
258
  }
220
- const existing = readJsonIfExists(location.hooksPath) ?? {};
221
- const backup = target === "default" && fs.existsSync(location.hooksPath) ? backupFile(location.hooksPath, context.now()) : null;
259
+ const text = fs.existsSync(location.hooksPath) ? prepareTextFile(location.hooksPath, "hooks configuration").text : null;
260
+ const existing = text === null ? {} : prepareJsonText(text, { file: location.hooksPath }).value;
261
+ if (!existing || typeof existing !== "object" || Array.isArray(existing) || (existing.hooks !== undefined && (!existing.hooks || typeof existing.hooks !== "object" || Array.isArray(existing.hooks) || Object.values(existing.hooks).some(entries => !Array.isArray(entries)))))
262
+ return prepareError(new AppError("validation", "Hooks configuration requires an object with event arrays", 2), { file: location.hooksPath });
263
+ return { project, target, location, existing, text };
264
+ }
265
+ export function installCodexHooks(context, input, prepared = prepareCodexHooks(context, input)) {
266
+ const { project, target, location, existing, text } = prepared;
222
267
  const nextConfig = mergeHooksConfig(existing, project.root);
268
+ const backup = target === "default" && text !== null ? backupFile(location.hooksPath, context.now(), text) : null;
223
269
  ensureDir(path.dirname(location.hooksPath));
224
270
  writeJsonFile(location.hooksPath, nextConfig);
225
271
  recordHookInstallation(context, project.id, installationScope(target, location.profile, location.hooksPath), location.hooksPath, true, "none");
@@ -238,20 +284,11 @@ export function installCodexHooks(context, input) {
238
284
  expected_hash: hashObject(expectedHooksConfig(project.root))
239
285
  };
240
286
  }
241
- export function removeCodexHooks(context, input) {
242
- const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
243
- const target = resolveHookTarget(input.target);
244
- const location = requireHookLocation(context, project, target, resolveProfileName(input.profile));
245
- if (target === "default" && !input.yes) {
246
- throw new AppError("confirmation_required", "Direct default Codex home mutation requires --yes", 1, {
247
- target,
248
- hooks_path: location.hooksPath
249
- });
250
- }
251
- const existing = readJsonIfExists(location.hooksPath) ?? {};
287
+ export function removeCodexHooks(context, input, prepared = prepareCodexHooks(context, input)) {
288
+ const { project, target, location, existing, text } = prepared;
252
289
  const hadManaged = hasManagedHooks(existing, project.root);
253
- const backup = target === "default" && fs.existsSync(location.hooksPath) ? backupFile(location.hooksPath, context.now()) : null;
254
290
  const nextConfig = removeManagedHooks(existing, project.root);
291
+ const backup = target === "default" && text !== null ? backupFile(location.hooksPath, context.now(), text) : null;
255
292
  ensureDir(path.dirname(location.hooksPath));
256
293
  writeJsonFile(location.hooksPath, nextConfig);
257
294
  recordHookInstallation(context, project.id, installationScope(target, location.profile, location.hooksPath), location.hooksPath, false, "not_installed");
@@ -274,102 +311,163 @@ export function handleCodexHook(context, input) {
274
311
  // therefore the only valid identity for a fresh Work.
275
312
  const rawChildProviderSessionId = stringValue(payload.agent_id) ?? null;
276
313
  const providerSessionId = rawChildProviderSessionId ?? rootProviderSessionId;
277
- const parentProviderSessionId = rawChildProviderSessionId ? codexNativeParentId(payload, rawChildProviderSessionId) : null;
278
314
  const turnId = stringValue(payload.turn_id);
279
315
  const toolName = stringValue(payload.tool_name) ?? toolNameFromPayload(payload);
280
316
  const command = lifecycleCommandFromPayload(payload);
281
317
  if (!command)
282
318
  return { ok: true, observed: false, reason: "non_bash_tool" };
283
319
  const lifecycle = lifecycleFacts(command);
284
- if (!lifecycle)
320
+ if (!lifecycle && !continuationCommand(command))
285
321
  return { ok: true, observed: false, reason: "event_not_participating", event: eventName };
322
+ const parentProviderSessionId = rawChildProviderSessionId
323
+ ? context.codexHookProof?.sessionId === rawChildProviderSessionId
324
+ ? context.codexHookProof.parentSessionId
325
+ : codexNativeParentId(payload, rawChildProviderSessionId)
326
+ : null;
286
327
  // Resume legitimately receives an answer through stdin. It is matched by
287
328
  // immutable lifecycle arguments below; never reject or rewrite that pipe.
288
- if (lifecycle.analysis.kind === "compound") {
289
- return {
290
- ok: false,
291
- observed: false,
292
- reason: "compound_lifecycle_command",
293
- message: "Run the dd-flow lifecycle invocation as the first technical action and as one standalone Bash command in this same Session.",
294
- standalone_command: lifecycle.invocation.command,
295
- hookSpecificOutput: {
296
- hookEventName: "PreToolUse",
297
- permissionDecision: "deny",
298
- permissionDecisionReason: `dd-flow lifecycle commands must be standalone. Retry in this same Session: ${lifecycle.invocation.command}`
299
- }
300
- };
329
+ const bootstrapStageStart = lifecycle?.bootstrapStageStart;
330
+ const invocationId = lifecycle ? commandOption(lifecycle.invocation, "invocation-id") : undefined;
331
+ if (invocationId || context.env.DD_FLOW_DAEMON_ID) {
332
+ const proof = context.codexHookProof;
333
+ if (!proof || proof.daemonId !== context.env.DD_FLOW_DAEMON_ID || proof.rootSessionId !== rootProviderSessionId
334
+ || proof.sessionId !== providerSessionId || proof.parentSessionId !== parentProviderSessionId
335
+ || proof.toolCallId !== payload.tool_use_id || proof.command !== command) {
336
+ throw new AppError("native_hook_unproven", "Managed Codex admission requires its active native hook delivery, not a manual handler call", 1, { ...(invocationId ? { invocation_id: invocationId } : {}), effect: "no_effect" });
337
+ }
301
338
  }
302
- const { flowPayload, stageStart, stageResume, workStart, bootstrapStageStart, commandProjectRoot } = lifecycle;
303
- const matchKey = commandProjectRoot ? lifecycleMatchKey(lifecycle, commandProjectRoot) : null;
339
+ const nativeProjectRoot = context.codexHookProof?.projectRoot ?? input.projectRoot;
304
340
  // A bootstrap stage is the first stateful command for a new materialized project.
305
341
  // Its PreToolUse event arrives before stage start can register that project.
306
- if (bootstrapStageStart && commandProjectRoot) {
307
- registerProject(context, { root: commandProjectRoot });
342
+ const bootstrapRoot = nativeProjectRoot ?? stringValue(payload.cwd);
343
+ if (bootstrapStageStart && bootstrapRoot) {
344
+ registerProject(context, { root: bootstrapRoot });
308
345
  }
309
- const project = projectForHook(context, input.projectRoot ?? commandProjectRoot, stringValue(payload.cwd));
310
- if (!project)
346
+ const project = projectForHook(context, nativeProjectRoot, stringValue(payload.cwd));
347
+ if (!project) {
348
+ if (invocationId)
349
+ throw new AppError("project_not_registered", "Managed lifecycle invocation has no registered project", 1, { project_root: nativeProjectRoot ?? null });
311
350
  return { ok: true, observed: false, reason: "unrelated_cwd" };
351
+ }
352
+ let matchKey = lifecycle ? lifecycleMatchKey(lifecycle, project.root) : null;
312
353
  // Codex retains the root session_id for a thread-spawned child, while the
313
354
  // payload transcript belongs to the current agent_id. Keep lineage separate
314
355
  // from ownership: otherwise a child overwrites the root transcript binding.
315
- const bindingOwnerId = providerSessionId ?? rootProviderSessionId;
316
- const binding = bindingOwnerId ? upsertSessionBindingFromPayload(context, project, bindingOwnerId, payload) : undefined;
356
+ const eventKey = hookEventKey(payload, eventName, toolName, command);
317
357
  const storageId = providerSessionId ? storageSessionId(nativeSessionIdentity("codex-desktop", providerSessionId)) : null;
318
358
  const parentSessionId = parentProviderSessionId ? storageSessionId(nativeSessionIdentity("codex-desktop", parentProviderSessionId)) : null;
319
- const observedSession = flowPayload
320
- ? bindObservedFlowSession(context, project, {
321
- ...flowPayload,
322
- harness: "codex-desktop",
323
- provider_session_id: providerSessionId ?? flowPayload.provider_session_id ?? null,
324
- parent_session_id: parentSessionId
325
- }, storageId ?? undefined)
326
- : undefined;
327
- const effectiveSessionId = observedSession?.session_id ?? storageId;
328
- const protocolId = observedSession?.protocol_id ?? binding?.protocol_id ?? null;
329
- const eventKey = hookEventKey(payload, eventName, toolName, command);
330
- const inserted = recordHookEvent(context, {
331
- projectId: project.id,
332
- protocolId,
333
- harness: "codex-desktop",
334
- providerSessionId: providerSessionId ?? null,
335
- parentSessionId,
336
- sessionId: effectiveSessionId,
337
- agentId: rawChildProviderSessionId ?? null,
338
- turnId: turnId ?? null,
339
- eventName,
340
- toolName: toolName ?? null,
341
- status: "observed",
342
- payload,
343
- eventKey,
344
- matchKey,
345
- transcriptPath: binding?.transcript_path ?? null,
346
- cwd: stringValue(payload.cwd) ?? null
347
- });
348
- if (effectiveSessionId) {
349
- recordFlowSessionObservation(context, {
359
+ const eventCwd = stringValue(payload.cwd) ?? project.root;
360
+ assertHookEventReplay(context, { projectId: project.id, eventKey, harness: "codex-desktop", providerSessionId: providerSessionId ?? null, parentSessionId, daemonId: stringValue(context.env.DD_FLOW_DAEMON_ID) ?? null, sessionId: storageId, turnId: turnId ?? null, eventName, toolName: toolName ?? null, matchKey, cwd: eventCwd });
361
+ const bindingOwnerId = providerSessionId ?? rootProviderSessionId;
362
+ // Resolve legacy transcript discovery before acquiring the receipt write lock.
363
+ const transcriptPath = stringValue(payload.transcript_path)
364
+ ?? (bindingOwnerId ? sessionBinding(context, project.id, bindingOwnerId)?.transcript_path ?? locateCodexTranscript(bindingOwnerId, context.env.CODEX_HOME) : null);
365
+ const persist = (scope) => {
366
+ if (scope)
367
+ matchKey = lifecycleMatchKey(lifecycleFacts(scope.command), project.root, scope);
368
+ const binding = bindingOwnerId ? upsertSessionBindingFromPayload(context, project, bindingOwnerId, payload, transcriptPath) : undefined;
369
+ const effectiveSessionId = storageId;
370
+ const protocolId = binding?.protocol_id ?? null;
371
+ const inserted = recordHookEvent(context, {
350
372
  projectId: project.id,
351
- sessionId: effectiveSessionId,
352
- runId: observedSession?.run_id ?? null,
353
373
  protocolId,
354
- cwd: stringValue(payload.cwd) ?? null,
374
+ harness: "codex-desktop",
375
+ providerSessionId: providerSessionId ?? null,
376
+ parentSessionId,
377
+ // Pinned by the owning app-server process, never derived from current RUN
378
+ // state or an agent-supplied shell command/payload.
379
+ daemonId: stringValue(context.env.DD_FLOW_DAEMON_ID) ?? null,
380
+ sessionId: effectiveSessionId,
381
+ agentId: rawChildProviderSessionId ?? null,
382
+ turnId: turnId ?? null,
383
+ eventName,
355
384
  toolName: toolName ?? null,
356
- eventKey
385
+ status: "observed",
386
+ payload,
387
+ eventKey,
388
+ matchKey,
389
+ transcriptPath: binding?.transcript_path ?? null,
390
+ cwd: stringValue(payload.cwd) ?? project.root
357
391
  });
358
- }
359
- return {
392
+ if (effectiveSessionId) {
393
+ recordFlowSessionObservation(context, {
394
+ projectId: project.id,
395
+ sessionId: effectiveSessionId,
396
+ protocolId,
397
+ cwd: eventCwd,
398
+ toolName: toolName ?? null,
399
+ eventKey
400
+ });
401
+ }
402
+ return { inserted, protocolId };
403
+ };
404
+ const common = {
360
405
  ok: true,
361
- observed: inserted,
362
- duplicate: !inserted,
363
406
  event_key: eventKey,
364
407
  session: providerSessionId ? nativeSessionIdentity("codex-desktop", providerSessionId) : null,
365
408
  ...(parentProviderSessionId ? { parent_session: nativeSessionIdentity("codex-desktop", parentProviderSessionId) } : {}),
366
- protocol_id: protocolId,
367
- ...(providerSessionId && (flowPayload || stageStart || stageResume || workStart)
409
+ protocol_id: null
410
+ };
411
+ if (!lifecycle) {
412
+ if (providerSessionId)
413
+ assignedLifecycleCwd(context, project.root, null, providerSessionId, command, stringValue(payload.cwd));
414
+ const persisted = context.db.writeTransaction(() => persist());
415
+ return { ...common, observed: persisted.inserted, duplicate: !persisted.inserted };
416
+ }
417
+ if (invocationId || context.env.DD_FLOW_DAEMON_ID) {
418
+ const daemonId = stringValue(context.env.DD_FLOW_DAEMON_ID);
419
+ const toolCallId = stringValue(payload.tool_use_id) ?? stringValue(payload.event_id) ?? stringValue(payload.delivery_id) ?? stringValue(payload.id);
420
+ if (!daemonId || !rootProviderSessionId || !providerSessionId || !toolCallId)
421
+ throw new AppError("codex_identity_missing", "Managed Codex lifecycle hook has no complete native invocation identity", 1, { invocation_id: invocationId });
422
+ let executionCwd;
423
+ try {
424
+ executionCwd = assignedLifecycleCwd(context, project.root, lifecycle, providerSessionId, command, stringValue(payload.cwd), { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId, toolCallId }, "codex-desktop");
425
+ }
426
+ catch (error) {
427
+ // A proven external Work is not its Stage coordinator. Deny the tool
428
+ // normally; a policy refusal is not a broken native hook or a receipt.
429
+ if (!(error instanceof AppError) || error.code !== "work_stage_boundary")
430
+ throw error;
431
+ return { ...common, observed: false, reason: error.code, hookSpecificOutput: { hookEventName: "PreToolUse",
432
+ permissionDecision: "deny", permissionDecisionReason: error.message } };
433
+ }
434
+ const receipt = observeLifecycleInvocation(context, {
435
+ currentEventKey: eventKey,
436
+ ...(invocationId ? { id: invocationId } : {}),
437
+ command,
438
+ harness: "codex-desktop",
439
+ projectId: project.id,
440
+ ...(executionCwd ? { executionCwd } : {}),
441
+ identity: { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId, toolCallId },
442
+ recordReceipt: scope => { persist(scope); return eventKey; }
443
+ });
444
+ const stored = context.db.get("SELECT protocol_id FROM hook_events WHERE project_id = ? AND event_key = ?", [project.id, receipt.eventKey]);
445
+ const admittedInvocationId = receipt.invocationId ?? invocationId;
446
+ if (admittedInvocationId)
447
+ return { ...common, protocol_id: stored?.protocol_id ?? null, ...lifecycleObservationResult(receipt), invocation_id: admittedInvocationId };
448
+ return {
449
+ ...common,
450
+ protocol_id: stored?.protocol_id ?? null,
451
+ ...lifecycleObservationResult(receipt),
452
+ hookSpecificOutput: {
453
+ hookEventName: "PreToolUse",
454
+ permissionDecision: "allow",
455
+ updatedInput: { ...objectRecord(payload.tool_input), command: commandWithHookEvent(command, eventKey) }
456
+ }
457
+ };
458
+ }
459
+ const persisted = persist();
460
+ return {
461
+ ...common,
462
+ protocol_id: persisted.protocolId,
463
+ observed: persisted.inserted,
464
+ duplicate: !persisted.inserted,
465
+ ...(providerSessionId
368
466
  ? {
369
467
  hookSpecificOutput: {
370
468
  hookEventName: "PreToolUse",
371
469
  permissionDecision: "allow",
372
- updatedInput: { command: commandWithHookEvent(command, eventKey) }
470
+ updatedInput: { ...objectRecord(payload.tool_input), command: commandWithHookEvent(command, eventKey) }
373
471
  }
374
472
  }
375
473
  : {})
@@ -381,7 +479,24 @@ function codexNativeParentId(payload, childId) {
381
479
  if (!transcript || !path.isAbsolute(transcript))
382
480
  return null;
383
481
  try {
384
- const firstLine = fs.readFileSync(transcript, "utf8").split("\n", 1)[0];
482
+ const descriptor = fs.openSync(transcript, "r");
483
+ const chunks = [];
484
+ try {
485
+ for (;;) {
486
+ const buffer = Buffer.alloc(4096);
487
+ const length = fs.readSync(descriptor, buffer, 0, buffer.length, null);
488
+ if (!length)
489
+ break;
490
+ const newline = buffer.subarray(0, length).indexOf(10);
491
+ chunks.push(buffer.subarray(0, newline < 0 ? length : newline));
492
+ if (newline >= 0)
493
+ break;
494
+ }
495
+ }
496
+ finally {
497
+ fs.closeSync(descriptor);
498
+ }
499
+ const firstLine = Buffer.concat(chunks).toString("utf8");
385
500
  if (!firstLine)
386
501
  return null;
387
502
  const record = JSON.parse(firstLine);
@@ -393,120 +508,153 @@ function codexNativeParentId(payload, childId) {
393
508
  return null;
394
509
  }
395
510
  }
396
- /** Convert a zcode-acp Bash tool notification into the same trusted lifecycle receipt used by Codex hooks. */
511
+ /** Qualified ACP admission binds public commands to an exact issued assignment.
512
+ * Its provenance is an observation, never a synchronous PreToolUse assertion. */
397
513
  export function handleZcodeEvent(context, input) {
514
+ const unrelated = nativeHookNonParticipation(["zcode", "event"], input.stdin);
515
+ if (unrelated)
516
+ return unrelated;
398
517
  const notification = parseJsonObject(input.stdin || "{}", "zcode ACP notification");
399
- if (notification.method !== "session/update")
400
- return { ok: true, observed: false, reason: "event_not_participating" };
401
- const params = objectRecord(notification.params);
402
- const update = objectRecord(params.update);
403
- if (update.sessionUpdate !== "tool_call")
518
+ const params = objectRecord(notification.params), update = objectRecord(params.update);
519
+ if (notification.method !== "session/update" || update.sessionUpdate !== "tool_call")
404
520
  return { ok: true, observed: false, reason: "event_not_participating" };
405
- const updateMeta = objectRecord(update._meta);
406
- const claudeCode = objectRecord(updateMeta.claudeCode);
407
- const runtime = objectRecord(updateMeta.zcodeRuntime);
408
- const toolName = stringValue(claudeCode.toolName) ?? stringValue(update.kind);
409
- const rawInput = objectRecord(update.rawInput);
521
+ const rawInput = objectRecord(update.rawInput), meta = objectRecord(update._meta), native = objectRecord(meta.zcodeRuntime);
522
+ const toolName = stringValue(objectRecord(meta.claudeCode).toolName);
523
+ if (toolName !== "Bash" && toolName !== "bash" && toolName !== "execute")
524
+ return { ok: true, observed: false, reason: "non_bash_tool" };
410
525
  const command = stringValue(rawInput.command) ?? stringValue(rawInput.cmd);
411
- if (!command || (toolName && toolName !== "Bash" && toolName !== "bash" && update.kind !== "execute")) {
526
+ const parsed = command ? parseLifecycleCommand(command) : { kind: "none", participation: "none" };
527
+ if (parsed.participation === "none" || parsed.participation === "unproven")
528
+ return { ok: true, observed: false, reason: "acp_evidence_only" };
529
+ if (parsed.kind === "none" && !continuationCommand(command))
530
+ throw new AppError("lifecycle_shell_syntax_invalid", "Known lifecycle shell execution is unsupported", 1, { reason: parsed.reason, effect: "unknown", recoverable: false });
531
+ const controlled = objectRecord(objectRecord(notification._meta).ddZcode);
532
+ const providerSessionId = stringValue(native.childSessionId) ?? stringValue(controlled.rootProviderSessionId);
533
+ return recordZcodeLifecycle(context, { projectRoot: input.projectRoot, stdin: JSON.stringify({
534
+ session_id: providerSessionId, tool_use_id: update.toolCallId, tool_name: toolName, tool_input: rawInput,
535
+ agent_id: native.agentId, _meta: { ddZcode: controlled }
536
+ }) }, parsed.kind !== "none" ? commandOption(parsed.invocation, "invocation-id") : undefined);
537
+ }
538
+ /**
539
+ * Accept the synchronous native ZCode PreToolUse callback. Invocation-ID
540
+ * commands instead rendezvous with their ACP event while the CLI awaits
541
+ * admission; the native hook must not create a competing receipt for them.
542
+ */
543
+ export function handleZcodeHook(context, input) {
544
+ const unrelated = nativeHookNonParticipation(["zcode", "hook"], input.stdin);
545
+ if (unrelated)
546
+ return unrelated;
547
+ return recordZcodeLifecycle(context, input, undefined, true);
548
+ }
549
+ function recordZcodeLifecycle(context, input, invocationId, nativePreTool = false) {
550
+ const hook = parseJsonObject(input.stdin || "{}", "ZCode hook event");
551
+ const eventName = stringValue(hook.hook_event_name) ?? stringValue(hook.hookEventName);
552
+ if (eventName && eventName !== "PreToolUse" && eventName !== "pre_tool_use")
553
+ return { ok: true, observed: false, reason: "event_not_participating" };
554
+ const toolName = stringValue(hook.tool_name) ?? stringValue(hook.toolName);
555
+ const rawInput = objectRecord(hook.tool_input ?? hook.toolInput);
556
+ const command = stringValue(rawInput.command) ?? stringValue(rawInput.cmd) ?? stringValue(hook.command);
557
+ if (!command || (toolName && toolName !== "Bash" && toolName !== "bash" && toolName !== "execute"))
412
558
  return { ok: true, observed: false, reason: "non_bash_tool" };
413
- }
414
559
  const lifecycle = lifecycleFacts(command);
415
- if (!lifecycle)
560
+ if (!lifecycle && !continuationCommand(command))
416
561
  return { ok: true, observed: false, reason: "event_not_participating" };
417
- if (lifecycle.analysis.kind === "compound") {
418
- throw new AppError("compound_lifecycle_command", "dd-flow lifecycle commands must be a standalone ZCode Bash tool call", 1, {
419
- standalone_command: lifecycle.invocation.command
420
- });
562
+ // ZCode can execute background Bash without emitting the ACP tool_call that admits an issued CLI invocation.
563
+ if (nativePreTool && lifecycle && rawInput.run_in_background === true) {
564
+ const reason = "Run dd-flow lifecycle commands in the foreground; ZCode background Bash does not provide the required admission receipt.";
565
+ return { continue: false, reason, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } };
421
566
  }
422
- const { flowPayload, bootstrapStageStart, commandProjectRoot } = lifecycle;
567
+ if (!invocationId && lifecycle && commandOption(lifecycle.invocation, "invocation-id"))
568
+ return { ok: true, observed: false, reason: "cli_invocation_owns_admission" };
423
569
  const expectedRoot = resolveProjectRoot(input.projectRoot);
424
- if (commandProjectRoot && resolveProjectRoot(commandProjectRoot) !== expectedRoot) {
425
- throw new AppError("project_mismatch", "ZCode lifecycle command project root does not match the controlled workspace", 1, {
426
- expected: expectedRoot,
427
- actual: commandProjectRoot
428
- });
570
+ const meta = objectRecord(hook._meta);
571
+ const ddZcode = objectRecord(meta.ddZcode);
572
+ const providerSessionId = stringValue(hook.session_id) ?? stringValue(hook.sessionId);
573
+ const rootProviderSessionId = stringValue(ddZcode.rootProviderSessionId);
574
+ const parentProviderSessionId = stringValue(ddZcode.parentProviderSessionId);
575
+ const daemonId = stringValue(ddZcode.daemonId);
576
+ if (!providerSessionId || !rootProviderSessionId || !daemonId) {
577
+ throw new AppError("zcode_identity_missing", "ZCode native hook has no controlled Session identity", 1);
429
578
  }
430
- const matchKey = lifecycleMatchKey(lifecycle, expectedRoot);
431
- if (bootstrapStageStart)
579
+ if (providerSessionId === rootProviderSessionId ? parentProviderSessionId : !parentProviderSessionId) {
580
+ throw new AppError("zcode_identity_missing", "ZCode native hook has inconsistent Session ancestry", 1);
581
+ }
582
+ const toolUseId = stringValue(hook.tool_use_id) ?? stringValue(hook.toolUseId) ?? stringValue(hook.event_id) ?? stringValue(hook.eventId);
583
+ if (!toolUseId)
584
+ throw new AppError("zcode_hook_event_missing", "ZCode native hook has no tool-use ID", 1);
585
+ const eventKey = crypto.createHash("sha256").update(JSON.stringify(["zcode-acp", daemonId, providerSessionId, toolUseId])).digest("hex");
586
+ const rewritten = invocationId ? command : commandWithHookEvent(command, eventKey);
587
+ if (lifecycle?.bootstrapStageStart)
432
588
  registerProject(context, { root: expectedRoot });
433
589
  const project = requireProjectByRoot(context, expectedRoot);
434
- const envelopeMeta = objectRecord(notification._meta);
435
- const ddZcode = objectRecord(envelopeMeta.ddZcode);
436
- const observedProfile = objectRecord(ddZcode.observedProfile);
437
- const rootProviderSessionId = stringValue(ddZcode.rootProviderSessionId) ?? stringValue(params.sessionId);
438
- const daemonId = stringValue(ddZcode.daemonId);
439
- if (!rootProviderSessionId)
440
- throw new AppError("zcode_identity_missing", "ZCode ACP event has no root provider Session ID", 1);
441
- const childProviderSessionId = stringValue(runtime.childSessionId);
442
- const providerSessionId = childProviderSessionId ?? rootProviderSessionId;
443
590
  const sessionId = storageSessionId(nativeSessionIdentity("zcode-acp", providerSessionId));
444
- const parentSessionId = childProviderSessionId ? storageSessionId(nativeSessionIdentity("zcode-acp", rootProviderSessionId)) : null;
445
- const agentId = stringValue(runtime.agentId);
446
- const observedSession = flowPayload ? bindObservedFlowSession(context, project, {
447
- ...flowPayload,
448
- harness: "zcode-acp",
449
- provider_session_id: providerSessionId,
450
- agent_id: agentId ?? null,
451
- parent_session_id: parentSessionId
452
- }, sessionId) : undefined;
453
- const eventKey = stringValue(update.toolCallId) ?? crypto.createHash("sha256").update(JSON.stringify({
454
- harness: "zcode-acp", providerSessionId, command
455
- })).digest("hex");
456
- const payload = {
457
- session_id: providerSessionId,
458
- agent_id: agentId,
459
- cwd: expectedRoot,
460
- tool_name: toolName ?? "Bash",
461
- tool_input: rawInput,
462
- command,
463
- provider: stringValue(observedProfile.provider),
464
- model: stringValue(observedProfile.model),
465
- reasoning: stringValue(observedProfile.reasoning),
466
- mode: stringValue(observedProfile.mode),
467
- agent_type: stringValue(runtime.source)
591
+ const parentSessionId = parentProviderSessionId ? storageSessionId(nativeSessionIdentity("zcode-acp", parentProviderSessionId)) : null;
592
+ const observedProfile = objectRecord(ddZcode.observedProfile);
593
+ const observedCwd = stringValue(hook.cwd) && path.isAbsolute(String(hook.cwd)) ? resolveProjectRoot(String(hook.cwd)) : null;
594
+ let matchKey = lifecycle ? lifecycleMatchKey(lifecycle, expectedRoot) : null;
595
+ const eventNameForReceipt = nativePreTool ? "PreToolUse" : "ToolCallObserved";
596
+ const persist = (scope) => {
597
+ if (scope)
598
+ matchKey = lifecycleMatchKey(lifecycleFacts(scope.command), expectedRoot, scope);
599
+ assertHookEventReplay(context, { projectId: project.id, eventKey, harness: "zcode-acp", providerSessionId, parentSessionId, daemonId, sessionId, turnId: null, eventName: eventNameForReceipt, toolName: toolName ?? "Bash", matchKey, cwd: observedCwd });
600
+ const payload = { ...hook, session_id: providerSessionId, cwd: observedCwd, tool_name: toolName ?? "Bash", tool_input: rawInput,
601
+ command, provider: stringValue(observedProfile.provider), model: stringValue(observedProfile.model), reasoning: stringValue(observedProfile.reasoning), mode: stringValue(observedProfile.mode) };
602
+ const inserted = recordHookEvent(context, {
603
+ projectId: project.id, protocolId: null, harness: "zcode-acp", providerSessionId, parentSessionId,
604
+ daemonId, sessionId, agentId: stringValue(hook.agent_id) ?? stringValue(hook.agentId) ?? null, turnId: null,
605
+ eventName: eventNameForReceipt, toolName: toolName ?? "Bash", status: "observed", payload, eventKey, matchKey,
606
+ transcriptPath: stringValue(hook.transcript_path) ?? null, cwd: observedCwd
607
+ });
608
+ recordFlowSessionObservation(context, { projectId: project.id, sessionId, cwd: observedCwd, toolName: toolName ?? "Bash", eventKey });
609
+ const result = { ok: true, observed: inserted, duplicate: !inserted, event_key: eventKey, harness: "zcode-acp", session: nativeSessionIdentity("zcode-acp", providerSessionId), ...(parentProviderSessionId ? { parent_session: nativeSessionIdentity("zcode-acp", parentProviderSessionId) } : {}), daemon_id: daemonId };
610
+ return { ...result, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: rewritten } } };
468
611
  };
469
- const inserted = recordHookEvent(context, {
470
- projectId: project.id,
471
- protocolId: observedSession?.protocol_id ?? null,
612
+ if (!lifecycle) {
613
+ assignedLifecycleCwd(context, expectedRoot, null, providerSessionId, command, observedCwd ?? undefined);
614
+ return context.db.writeTransaction(() => persist());
615
+ }
616
+ let persistedResult;
617
+ const executionCwd = assignedLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, command, observedCwd ?? undefined, { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId ?? null, toolCallId: toolUseId }, "zcode-acp");
618
+ const receipt = observeLifecycleInvocation(context, {
619
+ currentEventKey: eventKey,
620
+ ...(invocationId ? { id: invocationId } : {}), command,
472
621
  harness: "zcode-acp",
473
- providerSessionId,
474
- parentSessionId,
475
- daemonId: daemonId ?? null,
476
- sessionId,
477
- agentId: agentId ?? null,
478
- turnId: null,
479
- eventName: "PreToolUse",
480
- toolName: toolName ?? "Bash",
481
- status: "observed",
482
- payload,
483
- eventKey,
484
- matchKey,
485
- transcriptPath: null,
486
- cwd: expectedRoot
487
- });
488
- recordFlowSessionObservation(context, {
489
622
  projectId: project.id,
490
- sessionId,
491
- runId: observedSession?.run_id ?? null,
492
- protocolId: observedSession?.protocol_id ?? null,
493
- cwd: expectedRoot,
494
- toolName: toolName ?? "Bash",
495
- eventKey
623
+ ...(executionCwd ? { executionCwd } : {}),
624
+ identity: { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId ?? null, toolCallId: toolUseId },
625
+ recordReceipt: scope => { persistedResult = persist(scope); return eventKey; }
496
626
  });
497
- return {
498
- ok: true,
499
- observed: inserted,
500
- duplicate: !inserted,
501
- event_key: eventKey,
502
- harness: "zcode-acp",
503
- session: nativeSessionIdentity("zcode-acp", providerSessionId),
504
- ...(childProviderSessionId ? { parent_session: nativeSessionIdentity("zcode-acp", rootProviderSessionId) } : {}),
505
- daemon_id: daemonId ?? null
506
- };
627
+ if (receipt.disposition === "replay") {
628
+ // Keep first admission immutable. A second transport is independently
629
+ // retained corroboration, never another claim or an invented ACP cwd.
630
+ const retained = context.db.get("SELECT event_name, match_key, harness, provider_session_id, parent_session_id, daemon_id FROM hook_events WHERE project_id = ? AND event_key = ?", [project.id, receipt.eventKey]);
631
+ if (retained && retained.event_name !== eventNameForReceipt) {
632
+ if (retained.harness !== "zcode-acp" || retained.provider_session_id !== providerSessionId || retained.parent_session_id !== parentSessionId || retained.daemon_id !== daemonId) {
633
+ throw new AppError("hook_event_replay_mismatch", "Corroborating transport has different native ownership", 1, { event_key: receipt.eventKey });
634
+ }
635
+ context.db.writeTransaction(() => recordHookEvent(context, { projectId: project.id, protocolId: null, harness: "zcode-acp", providerSessionId, parentSessionId,
636
+ daemonId, sessionId, agentId: stringValue(hook.agent_id) ?? null, turnId: null, eventName: eventNameForReceipt, toolName: toolName ?? "Bash", status: "corroborating",
637
+ payload: { ...hook, command, corroborates_event_key: receipt.eventKey, admission_transport: nativePreTool ? "native_pre_tool" : "acp_observation" },
638
+ eventKey: `${receipt.eventKey}:${eventNameForReceipt}`, matchKey: null, transcriptPath: stringValue(hook.transcript_path) ?? null, cwd: observedCwd }));
639
+ }
640
+ else if (retained) {
641
+ assertHookEventReplay(context, { projectId: project.id, eventKey: receipt.eventKey, harness: "zcode-acp", providerSessionId, parentSessionId, daemonId,
642
+ sessionId, turnId: null, eventName: eventNameForReceipt, toolName: toolName ?? "Bash", matchKey: retained.match_key, cwd: observedCwd });
643
+ }
644
+ }
645
+ const admittedInvocationId = receipt.invocationId ?? invocationId;
646
+ if (admittedInvocationId)
647
+ return { ok: true, ...lifecycleObservationResult(receipt), invocation_id: admittedInvocationId };
648
+ // Without an issued managed attempt this is still ordinary hook evidence.
649
+ // Preserve persist()'s replay result instead of overwriting it with the
650
+ // admission layer's non-authoritative duplicate flag.
651
+ return persistedResult ?? persist();
507
652
  }
508
653
  /** Convert a Grok Build PreToolUse hook into a trusted lifecycle receipt. */
509
654
  export function handleGrokEvent(context, input) {
655
+ const unrelated = nativeHookNonParticipation(["grok", "event"], input.stdin);
656
+ if (unrelated)
657
+ return unrelated;
510
658
  const hook = parseJsonObject(input.stdin || "{}", "Grok Build hook event");
511
659
  const eventName = stringValue(hook.hook_event_name) ?? stringValue(hook.hookEventName);
512
660
  if (eventName && eventName !== "PreToolUse" && eventName !== "pre_tool_use") {
@@ -519,27 +667,18 @@ export function handleGrokEvent(context, input) {
519
667
  return { ok: true, observed: false, reason: "non_bash_tool" };
520
668
  }
521
669
  const lifecycle = lifecycleFacts(command);
522
- if (!lifecycle)
670
+ if (!lifecycle && !continuationCommand(command))
523
671
  return { ok: true, observed: false, reason: "event_not_participating" };
524
- if (lifecycle.analysis.kind === "compound") {
525
- throw new AppError("compound_lifecycle_command", "dd-flow lifecycle commands must be a standalone Grok Build tool call", 1, {
526
- standalone_command: lifecycle.invocation.command
527
- });
528
- }
529
- const { flowPayload, stageStart, stageResume, workStart, bootstrapStageStart, commandProjectRoot } = lifecycle;
672
+ const bootstrapStageStart = lifecycle?.bootstrapStageStart;
530
673
  const expectedRoot = resolveProjectRoot(input.projectRoot);
531
- if (commandProjectRoot && resolveProjectRoot(commandProjectRoot) !== expectedRoot) {
532
- throw new AppError("project_mismatch", "Grok Build lifecycle command project root does not match the controlled workspace", 1, {
533
- expected: expectedRoot, actual: commandProjectRoot
534
- });
535
- }
536
- const matchKey = lifecycleMatchKey(lifecycle, expectedRoot);
674
+ let matchKey = lifecycle ? lifecycleMatchKey(lifecycle, expectedRoot) : null;
537
675
  if (bootstrapStageStart)
538
676
  registerProject(context, { root: expectedRoot });
539
677
  const project = requireProjectByRoot(context, expectedRoot);
540
678
  const meta = objectRecord(hook._meta);
541
679
  const ddGrok = objectRecord(meta.ddGrok);
542
680
  const observedProfile = objectRecord(ddGrok.observedProfile);
681
+ const observedCwd = stringValue(hook.cwd) && path.isAbsolute(String(hook.cwd)) ? resolveProjectRoot(String(hook.cwd)) : null;
543
682
  const rootProviderSessionId = stringValue(ddGrok.rootProviderSessionId);
544
683
  const providerSessionId = stringValue(hook.session_id) ?? stringValue(hook.sessionId);
545
684
  if (!rootProviderSessionId || !providerSessionId)
@@ -549,41 +688,183 @@ export function handleGrokEvent(context, input) {
549
688
  const parentSessionId = isChild ? storageSessionId(nativeSessionIdentity("grok-acp", stringValue(ddGrok.parentProviderSessionId) ?? rootProviderSessionId)) : null;
550
689
  const daemonId = stringValue(ddGrok.daemonId);
551
690
  const agentId = stringValue(hook.agent_id) ?? stringValue(hook.agentId);
552
- const observedSession = flowPayload ? bindObservedFlowSession(context, project, {
553
- ...flowPayload, harness: "grok-acp", provider_session_id: providerSessionId, agent_id: agentId ?? null, parent_session_id: parentSessionId
554
- }, sessionId) : undefined;
555
691
  const eventKey = stringValue(hook.event_id) ?? stringValue(hook.eventId) ?? crypto.createHash("sha256").update(JSON.stringify({
556
- harness: "grok-acp", providerSessionId, command, turn: hook.turn_id ?? hook.turnId ?? null
692
+ harness: "grok-acp", providerSessionId, toolUseId: hook.tool_use_id ?? hook.toolUseId ?? null,
693
+ promptId: hook.prompt_id ?? hook.promptId ?? null, command, turn: hook.turn_id ?? hook.turnId ?? null
557
694
  })).digest("hex");
558
- const payload = {
559
- ...hook, session_id: providerSessionId, agent_id: agentId, cwd: expectedRoot, tool_name: toolName ?? "Bash", tool_input: rawInput,
560
- command, provider: stringValue(observedProfile.provider), model: stringValue(observedProfile.model), reasoning: stringValue(observedProfile.reasoning), mode: stringValue(observedProfile.mode), agent_type: stringValue(hook.subagent_type) ?? stringValue(hook.subagentType)
695
+ const persist = (scope) => {
696
+ if (scope)
697
+ matchKey = lifecycleMatchKey(lifecycleFacts(scope.command), expectedRoot, scope);
698
+ assertHookEventReplay(context, { projectId: project.id, eventKey, harness: "grok-acp", providerSessionId, parentSessionId, daemonId: daemonId ?? null, sessionId, turnId: stringValue(hook.turn_id) ?? stringValue(hook.turnId) ?? null, eventName: "PreToolUse", toolName: toolName ?? "Bash", matchKey, cwd: observedCwd });
699
+ const payload = {
700
+ ...hook, session_id: providerSessionId, agent_id: agentId, cwd: observedCwd, tool_name: toolName ?? "Bash", tool_input: rawInput,
701
+ command, provider: stringValue(observedProfile.provider), model: stringValue(observedProfile.model), reasoning: stringValue(observedProfile.reasoning), mode: stringValue(observedProfile.mode), agent_type: stringValue(hook.subagent_type) ?? stringValue(hook.subagentType)
702
+ };
703
+ const inserted = recordHookEvent(context, {
704
+ projectId: project.id, protocolId: null, harness: "grok-acp", providerSessionId, parentSessionId,
705
+ daemonId: daemonId ?? null, sessionId, agentId: agentId ?? null, turnId: stringValue(hook.turn_id) ?? stringValue(hook.turnId) ?? null,
706
+ eventName: "PreToolUse", toolName: toolName ?? "Bash", status: "observed", payload, eventKey, matchKey, transcriptPath: null, cwd: observedCwd
707
+ });
708
+ recordFlowSessionObservation(context, { projectId: project.id, sessionId, cwd: observedCwd, toolName: toolName ?? "Bash", eventKey });
709
+ return inserted;
561
710
  };
562
- const inserted = recordHookEvent(context, {
563
- projectId: project.id, protocolId: observedSession?.protocol_id ?? null, harness: "grok-acp", providerSessionId, parentSessionId,
564
- daemonId: daemonId ?? null, sessionId, agentId: agentId ?? null, turnId: stringValue(hook.turn_id) ?? stringValue(hook.turnId) ?? null,
565
- eventName: "PreToolUse", toolName: toolName ?? "Bash", status: "observed", payload, eventKey, matchKey, transcriptPath: null, cwd: expectedRoot
566
- });
567
- recordFlowSessionObservation(context, {
568
- projectId: project.id, sessionId, runId: observedSession?.run_id ?? null, protocolId: observedSession?.protocol_id ?? null,
569
- cwd: expectedRoot, toolName: toolName ?? "Bash", eventKey
711
+ const result = (inserted) => ({ ok: true, observed: inserted, duplicate: !inserted, event_key: eventKey, session: nativeSessionIdentity("grok-acp", providerSessionId),
712
+ ...(isChild ? { parent_session: nativeSessionIdentity("grok-acp", stringValue(ddGrok.parentProviderSessionId) ?? rootProviderSessionId) } : {}), daemon_id: daemonId ?? null });
713
+ if (!lifecycle) {
714
+ assignedLifecycleCwd(context, expectedRoot, null, providerSessionId, command, observedCwd ?? undefined);
715
+ return { ...result(context.db.writeTransaction(() => persist())), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, eventKey) } } };
716
+ }
717
+ let inserted = false;
718
+ const executionCwd = assignedLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, command, observedCwd ?? undefined, { daemonId: daemonId ?? "", rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId, toolCallId: eventKey }, "grok-acp");
719
+ const receipt = observeLifecycleInvocation(context, {
720
+ currentEventKey: eventKey,
721
+ command, harness: "grok-acp", projectId: project.id,
722
+ ...(executionCwd ? { executionCwd } : {}),
723
+ identity: { daemonId: daemonId ?? "", rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId, toolCallId: eventKey },
724
+ recordReceipt: scope => { inserted = persist(scope); return eventKey; }
570
725
  });
571
- const result = { ok: true, observed: inserted, duplicate: !inserted, event_key: eventKey, session: nativeSessionIdentity("grok-acp", providerSessionId),
572
- ...(isChild ? { parent_session: nativeSessionIdentity("grok-acp", stringValue(ddGrok.parentProviderSessionId) ?? rootProviderSessionId) } : {}), daemon_id: daemonId ?? null };
573
- return (flowPayload || stageStart || stageResume || workStart)
574
- ? { ...result, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...(rawInput ?? {}), command: commandWithHookEvent(command, eventKey) } } }
575
- : result;
726
+ return { ...result(inserted), ...lifecycleObservationResult(receipt), ...(receipt.invocationId ? { invocation_id: receipt.invocationId } : {}), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: receipt.eventKey ? commandWithHookEvent(command, receipt.eventKey) : command } } };
576
727
  }
577
728
  /** Convert a controlled OpenCode tool hook into the shared lifecycle receipt. */
578
729
  export function handleOpenCodeEvent(context, input) {
730
+ const unrelated = nativeHookNonParticipation(["opencode", "event"], input.stdin);
731
+ if (unrelated)
732
+ return unrelated;
579
733
  const event = parseJsonObject(input.stdin || "{}", "OpenCode tool event");
580
- const agy = event.source_harness === "antigravity-cli";
581
- const harness = agy ? "antigravity-cli" : "opencode-server";
582
734
  if (event.schema_id !== "dd-flow/opencode-tool-event@1")
583
735
  throw new AppError("unsupported_opencode_event_schema", "Unsupported OpenCode event schema", 1);
736
+ return handleControlledToolEvent(context, input, event, event.source_harness === "antigravity-cli" ? "antigravity-cli" : "opencode-server");
737
+ }
738
+ /** Accept adapter-observed Factory identity, never a model-supplied Session ID. */
739
+ export function handleDroidEvent(context, input) {
740
+ const unrelated = nativeHookNonParticipation(["droid", "event"], input.stdin);
741
+ if (unrelated)
742
+ return unrelated;
743
+ const event = parseJsonObject(input.stdin || "{}", "Droid tool event");
744
+ if (event.schema_id !== "dd-flow/droid-tool-event@1")
745
+ throw new AppError("unsupported_droid_event_schema", "Unsupported Droid event schema", 1);
746
+ return handleControlledToolEvent(context, input, event, "droid-cli");
747
+ }
748
+ /** Stable project identity is not the native Session's execution workspace. */
749
+ function requiresAssignedLifecycleCwd(facts) {
750
+ // Settlement and user interaction address the retained RUN explicitly;
751
+ // their tools/checks receive owned paths, not the shell's working directory.
752
+ return !facts || !["stage_finish", "stage_pause", "stage_resume", "work_finish", "work_fail"].includes(facts.invocation.operation);
753
+ }
754
+ function assignedLifecycleCwd(context, projectRoot, facts, providerSessionId, command, observedCwd, identity, harness) {
755
+ const auxiliary = continuationCommand(command);
756
+ if ((!facts && !auxiliary) || facts?.analysis.supported === false)
757
+ return undefined;
758
+ const proven = identity && harness ? managedNativeInvocationScope(context, projectRoot, identity, harness) : null;
759
+ if (proven && (facts?.invocation.operation.startsWith("stage_") || facts?.invocation.operation.startsWith("delivery_")) && !facts.bootstrapStageStart
760
+ && context.db.get("SELECT 1 FROM sqlite_master WHERE type='table' AND name='work_launches'")) {
761
+ const worker = context.db.get("SELECT w.work_id FROM works w JOIN work_launches l ON l.work_id=w.work_id JOIN work_sessions ws ON ws.work_id=w.work_id JOIN sessions s ON s.session_id=ws.session_id AND s.project_id=w.project_id JOIN projects p ON p.id=w.project_id WHERE p.root=? AND w.run_id=? AND s.harness=? AND s.provider_session_id=? AND l.status IN ('launching','start_requested') LIMIT 1", [proven.projectRoot, proven.runId, harness, providerSessionId]);
762
+ if (worker)
763
+ throw new AppError("work_stage_boundary", "This external Work cannot issue coordinator Stage or delivery commands. Complete only its assigned Work; after successful work finish, end this Turn. The coordinator continues the Stage.", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
764
+ }
765
+ const constraint = proven ?? (identity ? { projectRoot, daemonId: identity.daemonId, rootSessionId: identity.rootSessionId } : undefined);
766
+ const issuedRunId = facts && !facts.runId && !facts.workId && !facts.bootstrapStageStart && constraint ? observedLifecycleInvocation(context, command, constraint, identity ? { identity } : undefined)?.scope.runId : null;
767
+ if (facts?.invocation.operation.startsWith("stage_") && !facts.bootstrapStageStart && !facts.runId && !issuedRunId)
768
+ throw new AppError("invocation_assignment_missing", "Short lifecycle call has no unique issued RUN assignment; its workspace cannot be inferred", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
769
+ if (!requiresAssignedLifecycleCwd(facts))
770
+ return undefined;
771
+ const expected = controlledLifecycleCwd(context, projectRoot, facts, providerSessionId, auxiliary, issuedRunId, observedCwd);
772
+ // Unknown CLI targets have no assigned workspace. Recording their native
773
+ // observation must not turn the stable identity root into cwd authority.
774
+ if (!expected)
775
+ return undefined;
776
+ const route = facts?.invocation.expectedCwd ?? auxiliary?.expectedCwd;
777
+ if (route && resolveProjectRoot(route) !== expected)
778
+ throw new AppError("invocation_directory_mismatch", "Literal lifecycle route differs from the assigned workspace", 1, { expected, actual: route, actual_provenance: "shell_literal_route", phase: "prepare", effect: "no_effect", recoverable: false });
779
+ if (!route && observedCwd && resolveProjectRoot(observedCwd) !== expected)
780
+ throw new AppError("invocation_directory_mismatch", "Native lifecycle directory differs from the assigned workspace", 1, { expected, actual: observedCwd, actual_provenance: "native_tool_cwd", phase: "prepare", effect: "no_effect", recoverable: false });
781
+ // ACP without native cwd still binds assigned authority; CLI confirms the
782
+ // physical process directory, rather than inventing native observation.
783
+ return expected;
784
+ }
785
+ function controlledLifecycleCwd(context, projectRoot, facts, providerSessionId, auxiliary, issuedRunId, observedCwd) {
786
+ if (facts?.bootstrapStageStart)
787
+ return projectRoot;
788
+ const project = context.db.get("SELECT id FROM projects WHERE root = ?", [projectRoot]);
789
+ if (!project)
790
+ return undefined;
791
+ if (facts?.invocation.operation === "merge_apply" || facts?.invocation.operation === "merge_repair") {
792
+ const requestId = commandPosition(facts.invocation, 0);
793
+ const request = requestId && context.db.get("SELECT target_workspace FROM merge_requests WHERE project_id = ? AND merge_request_id = ?", [project.id, requestId]);
794
+ if (request)
795
+ return resolveProjectRoot(request.target_workspace);
796
+ }
797
+ let work;
798
+ if (facts?.workId) {
799
+ try {
800
+ work = resolveWorkReference(context, facts.workId, { projectId: project.id });
801
+ }
802
+ catch (error) {
803
+ if (!(error instanceof AppError) || !["not_found", "ambiguous_work_alias"].includes(error.code))
804
+ throw error;
805
+ }
806
+ if (!work)
807
+ return undefined;
808
+ }
809
+ const reference = work?.run_id ?? facts?.runId ?? auxiliary?.args.options.get("run")?.[0] ?? auxiliary?.args.positional[2] ?? issuedRunId;
810
+ if (!reference)
811
+ return undefined;
812
+ const run = context.db.get("SELECT id, run_root, workspace_root FROM runs WHERE project_id = ? AND (id = ? OR short_id = ?)", [project.id, reference, reference]);
813
+ if (!run)
814
+ return undefined; // Hook ingress does not validate a mistyped CLI target.
815
+ if (work) {
816
+ const repair = memoryRepairAssignedHookCwd(context, { projectRoot, projectId: project.id, runId: run.id,
817
+ workId: work.work_id, providerSessionId, ...(observedCwd ? { observedCwd: resolveProjectRoot(observedCwd) } : {}),
818
+ ...(facts?.invocation.expectedCwd ? { literalExecutionCwd: resolveProjectRoot(facts.invocation.expectedCwd) } : {}) });
819
+ if (repair)
820
+ return resolveProjectRoot(repair.root);
821
+ const nativeReview = memoryNativeReviewAssignedHookCwd(context, { projectId: project.id, runId: run.id, workId: work.work_id, providerSessionId });
822
+ if (nativeReview)
823
+ return resolveProjectRoot(nativeReview);
824
+ // PROTOCOLIZE provisions the feature checkout after its coordinator
825
+ // Session has launched in the stable project root. Its transition Work
826
+ // must keep that physical route until the Stage is complete. Child Works
827
+ // share stage membership, not the root coordinator's execution directory.
828
+ if (work.parent_work_id === null && context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'stage_work_memberships'")
829
+ && context.db.get("SELECT 1 FROM stage_work_memberships WHERE project_id = ? AND run_id = ? AND work_id = ? AND stage IN ('specify','protocolize') LIMIT 1", [project.id, run.id, work.work_id]))
830
+ return projectRoot;
831
+ const merge = context.db.get("SELECT target_workspace FROM merge_requests WHERE executor_work_id = ? AND run_id = ?", [work.work_id, run.id]);
832
+ if (merge)
833
+ return resolveProjectRoot(merge.target_workspace);
834
+ if (context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'work_launches'")) {
835
+ const launch = context.db.get("SELECT provider_session_id, profile_json FROM work_launches WHERE work_id = ? AND status IN ('launching', 'start_requested')", [work.work_id]);
836
+ if (launch?.provider_session_id === providerSessionId && run.run_root) {
837
+ const copy = JSON.parse(launch.profile_json).review_copy?.root;
838
+ if (copy) {
839
+ const target = path.resolve(run.run_root, copy);
840
+ const relative = path.relative(run.run_root, target);
841
+ if (relative === ".." || relative.startsWith(`..${path.sep}`))
842
+ throw new AppError("read_only_workspace_unbound", "Review copy escapes its owning RUN", 1);
843
+ return resolveProjectRoot(target);
844
+ }
845
+ }
846
+ }
847
+ }
848
+ if (facts?.stageName === "specify" || facts?.stageName === "protocolize")
849
+ return projectRoot;
850
+ if (facts?.stageName === "merge" || facts?.invocation.operation === "merge_apply" || facts?.invocation.operation === "merge_repair") {
851
+ const pending = context.db.all("SELECT target_workspace FROM merge_requests WHERE project_id = ? AND run_id = ? AND status IN ('dispatching','active','action_required') ORDER BY updated_at DESC", [project.id, run.id]);
852
+ const targets = [...new Set(pending.map(item => resolveProjectRoot(item.target_workspace)))];
853
+ if (targets.length > 1)
854
+ throw new AppError("merge_workspace_ambiguous", "Current MERGE requests disagree on the native execution workspace", 1, { run_id: run.id, target_workspaces: targets });
855
+ if (targets.length === 1)
856
+ return targets[0];
857
+ }
858
+ return resolveProjectRoot(run.workspace_root);
859
+ }
860
+ function handleControlledToolEvent(context, input, event, harness) {
861
+ const agy = harness === "antigravity-cli";
862
+ const droid = harness === "droid-cli";
863
+ const label = droid ? "Droid" : agy ? "Antigravity" : "OpenCode";
864
+ const errorPrefix = droid ? "droid" : agy ? "agy" : "opencode";
584
865
  const phase = stringValue(event.phase);
585
866
  if (phase !== "before" && phase !== "after")
586
- throw new AppError("unsupported_opencode_event_schema", "OpenCode event phase must be before or after", 1);
867
+ throw new AppError(`unsupported_${errorPrefix}_event_schema`, `${label} event phase must be before or after`, 1);
587
868
  const session = objectRecord(event.session);
588
869
  const profile = objectRecord(event.profile);
589
870
  const rawInput = objectRecord(event.input);
@@ -591,16 +872,16 @@ export function handleOpenCodeEvent(context, input) {
591
872
  const nativeParentId = stringValue(session.parent_provider_session_id);
592
873
  const daemonId = stringValue(event.daemon_id);
593
874
  const toolCallId = stringValue(event.tool_call_id);
594
- const eventId = stringValue(event.event_id) ?? toolCallId;
875
+ const eventId = stringValue(event.event_id) ?? (droid ? undefined : toolCallId);
595
876
  const toolName = stringValue(event.tool);
596
877
  const directory = stringValue(session.directory);
597
- if (!providerSessionId || !daemonId || !toolCallId || !eventId || !toolName || !directory || !path.isAbsolute(directory)) {
598
- throw new AppError("opencode_session_identity_invalid", "OpenCode event is missing trusted physical identity", 1);
878
+ if (!providerSessionId || !daemonId || (!droid && !toolCallId) || !eventId || !toolName || !directory || !path.isAbsolute(directory)) {
879
+ throw new AppError(`${errorPrefix}_session_identity_invalid`, `${label} event is missing trusted physical identity`, 1);
599
880
  }
600
881
  const expectedRoot = resolveProjectRoot(input.projectRoot);
601
- if (resolveProjectRoot(directory) !== expectedRoot) {
602
- throw new AppError(agy ? "agy_directory_mismatch" : "opencode_directory_mismatch", `${agy ? "Antigravity" : "OpenCode"} Session directory does not match the controlled workspace`, 1, { expected: expectedRoot, actual: directory });
603
- }
882
+ const observedCwd = resolveProjectRoot(directory);
883
+ if (droid && nativeParentId === providerSessionId)
884
+ throw new AppError("droid_session_identity_invalid", "A Droid Session cannot parent itself", 1);
604
885
  const sessionId = storageSessionId(nativeSessionIdentity(harness, providerSessionId));
605
886
  const parentSessionId = nativeParentId ? storageSessionId(nativeSessionIdentity(harness, nativeParentId)) : null;
606
887
  const command = stringValue(rawInput.command) ?? stringValue(rawInput.cmd) ?? stringValue(rawInput.CommandLine);
@@ -610,67 +891,131 @@ export function handleOpenCodeEvent(context, input) {
610
891
  const inserted = recordHookEvent(context, {
611
892
  projectId: project.id, protocolId: null, harness, providerSessionId, parentSessionId,
612
893
  daemonId, sessionId, agentId: stringValue(session.agent_id) ?? null, turnId: stringValue(event.message_id) ?? null,
613
- eventName: "PostToolUse", toolName, status: "observed", payload: { session_id: providerSessionId, cwd: expectedRoot, tool_name: toolName, status: objectRecord(event.outcome).status },
614
- eventKey: baseKey, matchKey: null, transcriptPath: null, cwd: expectedRoot
894
+ eventName: "PostToolUse", toolName, status: "observed", payload: { session_id: providerSessionId, cwd: observedCwd, tool_name: toolName, status: objectRecord(event.outcome).status },
895
+ eventKey: baseKey, matchKey: null, transcriptPath: droid ? stringValue(event.transcript_path) ?? null : null, cwd: observedCwd
615
896
  });
616
897
  return { ok: true, observed: inserted, duplicate: !inserted, event_key: baseKey, phase, harness, session: nativeSessionIdentity(harness, providerSessionId), ...(nativeParentId ? { parent_session: nativeSessionIdentity(harness, nativeParentId) } : {}) };
617
898
  }
618
- if (!command || !["bash", "Bash", "run_command", "run_terminal_command", "RunTerminalCommand"].includes(toolName)) {
899
+ if (!command || !(droid ? ["Execute"] : ["bash", "Bash", "shell", "run_command", "run_terminal_command", "RunTerminalCommand"]).includes(toolName)) {
619
900
  return { ok: true, observed: false, reason: command ? "non_bash_tool" : "event_not_participating" };
620
901
  }
621
902
  const lifecycle = lifecycleFacts(command);
622
- if (!lifecycle)
903
+ const auxiliary = continuationCommand(command);
904
+ if (!lifecycle && !auxiliary)
623
905
  return { ok: true, observed: false, reason: "event_not_participating" };
624
- if (lifecycle.analysis.kind === "compound") {
625
- throw new AppError("compound_lifecycle_command", `dd-flow lifecycle commands must be a standalone ${agy ? "Antigravity" : "OpenCode"} shell tool call`, 1, { standalone_command: lifecycle.invocation.command });
906
+ // Syntax refusal precedes assignment/cwd lookup: an unsupported AST cannot
907
+ // supply a route or execution authority, including a short public Stage call.
908
+ const shellRefusal = lifecycle?.analysis.supported === false;
909
+ const proof = context.nativeHookProof;
910
+ if (proof && (proof.harness !== harness || proof.daemonId !== daemonId || proof.sessionId !== providerSessionId || proof.toolCallId !== toolCallId))
911
+ throw new AppError("native_hook_unproven", "Verified native Work callback differs from its physical identity", 1);
912
+ const nativeIdentity = { daemonId, rootSessionId: proof?.rootSessionId ?? nativeParentId ?? providerSessionId, sessionId: providerSessionId, parentSessionId: nativeParentId ?? null, toolCallId: toolCallId };
913
+ const provenScope = lifecycle && !shellRefusal ? managedNativeInvocationScope(context, expectedRoot, nativeIdentity, harness) : null;
914
+ const issuedRunId = lifecycle && !lifecycle.runId && !lifecycle.workId && !lifecycle.bootstrapStageStart
915
+ ? observedLifecycleInvocation(context, command, provenScope ?? { projectRoot: expectedRoot, daemonId, rootSessionId: nativeIdentity.rootSessionId }, { identity: nativeIdentity })?.scope.runId : null;
916
+ if (!shellRefusal && lifecycle && lifecycle.invocation.operation.startsWith("stage_") && !lifecycle.bootstrapStageStart && !lifecycle.runId && !issuedRunId) {
917
+ throw new AppError("invocation_assignment_missing", "Short lifecycle call has no unique issued RUN assignment; its workspace cannot be inferred", 1, { effect: "no_effect", recoverable: false });
918
+ }
919
+ // These controlled platforms additionally qualify their native launch
920
+ // route against the stable project root when no CLI target is assigned.
921
+ const workspaceBound = requiresAssignedLifecycleCwd(lifecycle);
922
+ const expectedCwd = shellRefusal || !workspaceBound ? observedCwd : controlledLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, auxiliary, issuedRunId, observedCwd) ?? expectedRoot;
923
+ const executionRoute = shellRefusal || !workspaceBound ? undefined : lifecycle?.invocation.expectedCwd ?? auxiliary?.expectedCwd;
924
+ if (executionRoute && resolveProjectRoot(executionRoute) !== expectedCwd) {
925
+ throw new AppError(`${errorPrefix}_directory_mismatch`, "Literal shell cd route does not match the assigned lifecycle workspace", 1, { expected: expectedCwd, actual: executionRoute, actual_provenance: "shell_literal_route", effect: "no_effect", recoverable: false });
626
926
  }
627
- const { flowPayload, bootstrapStageStart, commandProjectRoot } = lifecycle;
628
- if (commandProjectRoot && resolveProjectRoot(commandProjectRoot) !== expectedRoot) {
629
- throw new AppError("project_mismatch", "OpenCode lifecycle command project root does not match the controlled workspace", 1, { expected: expectedRoot, actual: commandProjectRoot });
927
+ if (workspaceBound && !executionRoute && observedCwd !== expectedCwd) {
928
+ throw new AppError(`${errorPrefix}_directory_mismatch`, `${label} ${agy ? "launch route" : "Session directory"} does not match the assigned execution workspace`, 1, { expected: expectedCwd, actual: observedCwd, effect: "no_effect", recoverable: false, ...(agy ? { actual_provenance: stringValue(event.cwd_provenance) ?? "unknown" } : {}) });
630
929
  }
631
- const matchKey = lifecycleMatchKey(lifecycle, expectedRoot);
930
+ const bootstrapStageStart = !shellRefusal && lifecycle?.bootstrapStageStart;
931
+ let matchKey = lifecycle && !shellRefusal ? lifecycleMatchKey(lifecycle, expectedRoot) : null;
632
932
  if (bootstrapStageStart)
633
933
  registerProject(context, { root: expectedRoot });
634
934
  const project = requireProjectByRoot(context, expectedRoot);
935
+ if (droid) {
936
+ const existing = context.db.get("SELECT harness, provider_session_id, parent_session_id, daemon_id, match_key, tool_name FROM hook_events WHERE project_id = ? AND event_key = ?", [project.id, baseKey]);
937
+ if (existing && (existing.harness !== harness || existing.provider_session_id !== providerSessionId || existing.parent_session_id !== parentSessionId || existing.daemon_id !== daemonId || existing.match_key !== matchKey || existing.tool_name !== toolName)) {
938
+ throw new AppError("droid_event_replay_mismatch", "Droid event ID was already observed with different lifecycle identity", 1);
939
+ }
940
+ }
635
941
  const agentId = stringValue(session.agent_id);
636
- const observedSession = flowPayload ? bindObservedFlowSession(context, project, {
637
- ...flowPayload, harness, provider_session_id: providerSessionId, agent_id: agentId ?? null,
638
- parent_session_id: parentSessionId, provider: stringValue(profile.provider) ?? null, model: stringValue(profile.model) ?? null,
639
- reasoning: stringValue(profile.variant) ?? null, mode: stringValue(profile.agent) ?? null, agent_type: stringValue(profile.agent) ?? null,
640
- metadata: { opencode_version: stringValue(event.server_version) ?? null, api_fingerprint: stringValue(event.api_fingerprint) ?? null, plugin_sha256: stringValue(event.plugin_sha256) ?? null, native_project_id: stringValue(session.project_id) ?? null }
641
- }, sessionId) : undefined;
642
- const payload = {
643
- session_id: providerSessionId, agent_id: agentId, cwd: expectedRoot, tool_name: toolName, tool_input: rawInput, command,
644
- provider: stringValue(profile.provider), model: stringValue(profile.model), reasoning: stringValue(profile.variant),
645
- mode: stringValue(profile.agent), agent_type: stringValue(profile.agent)
942
+ const persist = (scope) => {
943
+ if (scope)
944
+ matchKey = lifecycleMatchKey(lifecycleFacts(scope.command), expectedRoot, scope);
945
+ assertHookEventReplay(context, { projectId: project.id, eventKey: baseKey, harness, providerSessionId, parentSessionId, daemonId, sessionId, turnId: stringValue(event.message_id) ?? null, eventName: "PreToolUse", toolName, matchKey, cwd: observedCwd });
946
+ const payload = {
947
+ session_id: providerSessionId, agent_id: agentId, cwd: observedCwd, tool_name: toolName, tool_input: rawInput, command,
948
+ ...(agy ? { cwd_provenance: stringValue(event.cwd_provenance) ?? "unknown" } : {}),
949
+ ...(executionRoute ? { lifecycle_execution_cwd: expectedCwd, lifecycle_execution_cwd_provenance: "literal_cd_intent_requires_cli_confirmation" } : {}),
950
+ provider: stringValue(profile.provider), model: stringValue(profile.model), reasoning: stringValue(droid ? profile.reasoning : profile.variant),
951
+ mode: stringValue(droid ? profile.mode : profile.agent), agent_type: stringValue(droid ? profile.agent_type : profile.agent)
952
+ };
953
+ const inserted = recordHookEvent(context, {
954
+ projectId: project.id, protocolId: null, harness, providerSessionId, parentSessionId,
955
+ daemonId, sessionId, agentId: agentId ?? null, turnId: stringValue(event.message_id) ?? null, eventName: "PreToolUse", toolName,
956
+ status: "observed", payload, eventKey: baseKey, matchKey, transcriptPath: droid ? stringValue(event.transcript_path) ?? null : null, cwd: observedCwd
957
+ });
958
+ recordFlowSessionObservation(context, { projectId: project.id, sessionId, cwd: observedCwd, toolName, eventKey: baseKey });
959
+ return inserted;
646
960
  };
647
- const inserted = recordHookEvent(context, {
648
- projectId: project.id, protocolId: observedSession?.protocol_id ?? null, harness, providerSessionId, parentSessionId,
649
- daemonId, sessionId, agentId: agentId ?? null, turnId: stringValue(event.message_id) ?? null, eventName: "PreToolUse", toolName,
650
- status: "observed", payload, eventKey: baseKey, matchKey, transcriptPath: null, cwd: expectedRoot
651
- });
652
- recordFlowSessionObservation(context, { projectId: project.id, sessionId, runId: observedSession?.run_id ?? null, protocolId: observedSession?.protocol_id ?? null, cwd: expectedRoot, toolName, eventKey: baseKey });
653
- const result = { ok: true, observed: inserted, duplicate: !inserted, event_key: baseKey, phase, harness, session: nativeSessionIdentity(harness, providerSessionId), ...(nativeParentId ? { parent_session: nativeSessionIdentity(harness, nativeParentId) } : {}), daemon_id: daemonId };
654
- return agy ? result : { ...result, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, baseKey) } } };
961
+ const result = (inserted) => ({ ok: true, observed: inserted, duplicate: !inserted, event_key: baseKey, phase, harness, session: nativeSessionIdentity(harness, providerSessionId), ...(nativeParentId ? { parent_session: nativeSessionIdentity(harness, nativeParentId) } : {}), daemon_id: daemonId });
962
+ if (lifecycle) {
963
+ let inserted = false;
964
+ const receipt = observeLifecycleInvocation(context, {
965
+ currentEventKey: baseKey,
966
+ command, harness, projectId: project.id, ...(!shellRefusal && workspaceBound ? { executionCwd: expectedCwd } : {}), identity: nativeIdentity,
967
+ recordReceipt: scope => { inserted = persist(scope); return baseKey; }
968
+ });
969
+ const admitted = { ...result(inserted), ...lifecycleObservationResult(receipt), ...(receipt.invocationId ? { invocation_id: receipt.invocationId } : {}) };
970
+ return agy || shellRefusal ? admitted : { ...admitted, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: receipt.eventKey ? commandWithHookEvent(command, receipt.eventKey) : command } } };
971
+ }
972
+ const inserted = context.db.writeTransaction(() => persist());
973
+ return agy ? result(inserted) : { ...result(inserted), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, baseKey) } } };
655
974
  }
656
975
  /** Convert an Antigravity CLI tool hook into the shared lifecycle receipt. */
657
976
  export function handleAgyEvent(context, input) {
977
+ const unrelated = nativeHookNonParticipation(["agy", "event"], input.stdin);
978
+ if (unrelated)
979
+ return unrelated;
658
980
  const event = parseJsonObject(input.stdin || "{}", "Antigravity tool event");
659
981
  if (event.schema_id !== "dd-flow/agy-tool-event@1")
660
982
  throw new AppError("unsupported_agy_event_schema", "Unsupported Antigravity event schema", 1);
661
- const workspacePaths = Array.isArray(event.workspace_paths) ? event.workspace_paths : [];
662
- const directory = workspacePaths.find((value) => typeof value === "string" && path.isAbsolute(value));
983
+ // workspacePaths lists available workspaces; its first element is not a
984
+ // native selected-cwd observation. The daemon's spawn cwd is a separate,
985
+ // launch-bound route, and the list only corroborates its membership.
986
+ const workspacePaths = Array.isArray(event.workspace_paths)
987
+ ? event.workspace_paths.filter((value) => typeof value === "string" && path.isAbsolute(value)) : [];
988
+ const launchCwd = stringValue(event.launch_cwd);
989
+ const qualifiedLaunchCwd = launchCwd && path.isAbsolute(launchCwd) ? launchCwd : undefined;
990
+ const directory = qualifiedLaunchCwd ?? (workspacePaths.length === 1 ? workspacePaths[0] : undefined);
991
+ if (!directory && workspacePaths.length > 1)
992
+ throw new AppError("agy_workspace_route_unproven", "Antigravity did not identify the active route among multiple workspaces", 1, { workspace_paths: workspacePaths });
993
+ if (directory && qualifiedLaunchCwd && workspacePaths.length
994
+ && !workspacePaths.some(value => resolveProjectRoot(value) === resolveProjectRoot(directory))) {
995
+ throw new AppError("agy_workspace_route_unproven", "Antigravity launch cwd is absent from the native workspace list", 1, { launch_cwd: directory, workspace_paths: workspacePaths });
996
+ }
663
997
  const providerSessionId = stringValue(event.conversation_id);
664
998
  const daemonId = stringValue(event.daemon_id);
665
999
  const eventId = stringValue(event.event_id);
666
1000
  const toolName = stringValue(event.tool);
667
1001
  if (!directory || !providerSessionId || !daemonId || !eventId || !toolName)
668
1002
  throw new AppError("agy_session_identity_invalid", "Antigravity event is missing trusted physical identity", 1);
1003
+ const rawInput = objectRecord(event.input);
1004
+ const command = stringValue(rawInput.command) ?? stringValue(rawInput.cmd) ?? stringValue(rawInput.CommandLine);
1005
+ if (command && (lifecycleFacts(command) || continuationCommand(command)) && !qualifiedLaunchCwd) {
1006
+ throw new AppError("agy_workspace_route_unproven", "Managed Antigravity call has no launch-bound execution cwd", 1, { phase: "prepare", effect: "no_effect", recoverable: false, workspace_paths: workspacePaths });
1007
+ }
1008
+ const invalidStep = !Number.isSafeInteger(event.step_index) || Number(event.step_index) < 0;
1009
+ const invalidExecution = event.execution_num !== undefined && (!Number.isSafeInteger(event.execution_num) || Number(event.execution_num) < 0);
1010
+ if (event.phase === "before" && command && (lifecycleFacts(command) || continuationCommand(command)) && (invalidStep || invalidExecution)) {
1011
+ throw new AppError("agy_native_call_identity_missing", "Participating Antigravity lifecycle calls require a native step identity and a valid execution identity when provided", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
1012
+ }
669
1013
  const translated = {
670
1014
  schema_id: "dd-flow/opencode-tool-event@1", source_harness: "antigravity-cli", phase: event.phase, event_id: eventId,
671
1015
  daemon_id: daemonId, tool_call_id: eventId, tool: toolName, input: event.input,
672
1016
  message_id: event.step_index === undefined ? null : String(event.step_index),
673
1017
  session: { provider_session_id: providerSessionId, parent_provider_session_id: event.parent_conversation_id, directory },
1018
+ cwd_provenance: qualifiedLaunchCwd ? "launch_bound" : "workspace_paths_singleton",
674
1019
  profile: { provider: objectRecord(event.profile).provider, model: objectRecord(event.profile).model, variant: objectRecord(event.profile).reasoning, agent: objectRecord(event.profile).mode },
675
1020
  outcome: { status: stringValue(event.error) ? "error" : "completed" }
676
1021
  };
@@ -698,15 +1043,25 @@ export function isolatedDdFlowHomeFromHookStdin(stdin) {
698
1043
  * hook contract intentionally narrow: unwrap only that whole-command form,
699
1044
  * then apply the same lifecycle parser to its actual shell payload.
700
1045
  */
701
- function lifecycleCommandFromPayload(payload) {
702
- const command = commandFromPayload(payload);
703
- return command ? unwrapShellCommand(command) : undefined;
704
- }
705
- function commandWithHookEvent(command, eventKey) {
1046
+ /**
1047
+ * Adds the receipt to the command that the shell will execute. Heredoc input
1048
+ * belongs to that command too, but an option appended after its delimiter is a
1049
+ * second shell command rather than an argument, so insert it before `<<`.
1050
+ */
1051
+ export function commandWithHookEvent(command, eventKey) {
706
1052
  const analysis = parseLifecycleCommand(command);
707
- if (analysis.kind !== "standalone" || !analysis.invocation.rewriteSafe || commandHasOption(analysis.invocation, "hook-event-id"))
1053
+ if (analysis.kind === "none" || analysis.supported === false)
1054
+ return command;
1055
+ if (commandHasOption(analysis.invocation, "hook-event-id")) {
708
1056
  return command;
709
- return `${command.trim()} --hook-event-id ${JSON.stringify(eventKey)}`;
1057
+ }
1058
+ const option = `--hook-event-id '${eventKey.replaceAll("'", "'\\''")}'`;
1059
+ if (analysis.invocation.rewriteSafe) {
1060
+ const index = analysis.invocation.sourceRange?.end ?? shellSuffixOffset(command);
1061
+ const suffix = command.slice(index);
1062
+ return `${command.slice(0, index)} ${option}${suffix ? /^\s/.test(suffix) ? suffix : ` ${suffix}` : ""}`;
1063
+ }
1064
+ return command;
710
1065
  }
711
1066
  /**
712
1067
  * Recognise the small shell surface used by generated dd-flow lifecycle
@@ -723,29 +1078,47 @@ export function analyzeLifecycleShellCommand(command, includeSessionRegister = f
723
1078
  }
724
1079
  function lifecycleFacts(command) {
725
1080
  const analysis = parseLifecycleCommand(command);
1081
+ if (analysis.kind === "none" && analysis.unresolved && ["lifecycle", "auxiliary"].includes(analysis.participation))
1082
+ throw new AppError("lifecycle_shell_syntax_invalid", "Known lifecycle execution is unsupported; use a literal issued command", 1, { phase: "prepare", effect: "no_effect", recoverable: true, disposition: "pre_cli_shell_refusal", reason: analysis.reason });
726
1083
  if (analysis.kind === "none")
727
1084
  return null;
728
1085
  const invocation = analysis.invocation;
729
1086
  const stageStart = invocation.operation === "stage_start";
1087
+ const stageFinish = invocation.operation === "stage_finish";
1088
+ const stagePause = invocation.operation === "stage_pause";
730
1089
  const stageResume = invocation.operation === "stage_resume";
731
1090
  const workStart = invocation.operation === "work_start";
1091
+ const workFinish = invocation.operation === "work_finish";
1092
+ const workFail = invocation.operation === "work_fail";
1093
+ const delivery = invocation.operation.startsWith("delivery_");
732
1094
  return {
733
1095
  analysis,
734
1096
  invocation,
735
- flowPayload: flowSessionPayloadFromRegisterCommand(invocation),
736
1097
  stageStart,
1098
+ stageFinish,
1099
+ stagePause,
737
1100
  stageResume,
738
- workStart,
1101
+ workStart, workFinish, workFail,
739
1102
  bootstrapStageStart: stageStart && commandHasOption(invocation, "bootstrap"),
740
1103
  commandProjectRoot: commandOption(invocation, "project-root"),
741
- stageName: stageStart || stageResume ? commandOption(invocation, "stage") : undefined,
742
- runId: stageStart || stageResume ? commandPosition(invocation, 0) : undefined,
743
- workId: stageResume ? commandOption(invocation, "work") : workStart ? commandPosition(invocation, 0) : undefined,
1104
+ stageName: stageStart || stageFinish || stagePause || stageResume || delivery ? commandOption(invocation, "stage") : undefined,
1105
+ runId: stageStart || stageFinish || stagePause || stageResume || delivery ? commandPosition(invocation, 0) : undefined,
1106
+ workId: stagePause || stageResume ? commandOption(invocation, "work") : workStart || workFinish || workFail ? commandPosition(invocation, 0) : undefined,
744
1107
  contextSha256: commandOption(invocation, "context-sha256")
745
1108
  };
746
1109
  }
747
- function lifecycleMatchKey(facts, projectRoot) {
748
- const { invocation, bootstrapStageStart, stageStart, stageResume, workStart, stageName, runId, workId, contextSha256 } = facts;
1110
+ function lifecycleMatchKey(facts, projectRoot, scope) {
1111
+ // Observe the actual argument without validating it. A mistyped project
1112
+ // must not leave a receipt matching a later, corrected non-rewriting call.
1113
+ if (!scope && facts.commandProjectRoot && canonicalPath(facts.commandProjectRoot, false) !== canonicalPath(projectRoot, false))
1114
+ return null;
1115
+ const { invocation, bootstrapStageStart, stageStart, stageFinish, stagePause, stageResume, workStart, workFinish, workFail, stageName, runId, workId, contextSha256 } = facts;
1116
+ const effectiveRoot = scope?.projectRoot ?? projectRoot;
1117
+ const effectiveRunId = scope?.runId ?? runId;
1118
+ if (invocation.operation === "recovery_accept") {
1119
+ const run = commandPosition(invocation, 0), recovery = commandOption(invocation, "recovery-id");
1120
+ return (scope?.runId ?? run) && recovery ? recoveryAcceptMatchKey((scope?.runId ?? run), recovery, effectiveRoot) : null;
1121
+ }
749
1122
  if (bootstrapStageStart)
750
1123
  return bootstrapMatchKey({
751
1124
  projectRoot,
@@ -753,14 +1126,100 @@ function lifecycleMatchKey(facts, projectRoot) {
753
1126
  intakeMode: contextSha256 ? "context" : commandHasOption(invocation, "intake-stdin") ? "stdin" : "file",
754
1127
  ...(contextSha256 ? { contextSha256 } : {})
755
1128
  });
756
- if (stageStart && runId && stageName)
757
- return stageStartMatchKey(runId, stageName, projectRoot, contextSha256);
758
- if (stageResume && runId && stageName && workId)
759
- return stageResumeMatchKey(runId, stageName, workId, projectRoot);
1129
+ if (stageStart && effectiveRunId && stageName)
1130
+ return stageStartMatchKey(effectiveRunId, stageName, effectiveRoot, contextSha256);
1131
+ if (stageFinish && effectiveRunId && stageName)
1132
+ return stageLifecycleMatchKey("stage_finish", effectiveRunId, stageName, effectiveRoot);
1133
+ if (stagePause && effectiveRunId && stageName && workId)
1134
+ return stageLifecycleMatchKey("stage_pause", effectiveRunId, stageName, effectiveRoot, workId);
1135
+ if (stageResume && effectiveRunId && stageName && workId)
1136
+ return stageResumeMatchKey(effectiveRunId, stageName, workId, effectiveRoot);
760
1137
  if (workStart && workId)
761
- return workStartMatchKey(workId, projectRoot);
1138
+ return workStartMatchKey(workId, effectiveRoot, commandOption(invocation, "recovery-id"));
1139
+ if (workFinish && workId)
1140
+ return workLifecycleMatchKey("work_finish", workId, effectiveRoot);
1141
+ if (workFail && workId)
1142
+ return workLifecycleMatchKey("work_fail", workId, effectiveRoot);
762
1143
  return null;
763
1144
  }
1145
+ /** Observe exact immutable command attribution before reading model-supplied
1146
+ * result files. This does not claim or authorize the lifecycle mutation. */
1147
+ export function observeCommandHook(context, command) {
1148
+ const facts = lifecycleFacts(command);
1149
+ if (!facts || facts.analysis.kind !== "standalone" || !facts.commandProjectRoot)
1150
+ return;
1151
+ const project = context.db.get("SELECT id FROM projects WHERE root = ?", [resolveProjectRoot(facts.commandProjectRoot)]);
1152
+ if (!project)
1153
+ return; // Bootstrap has not registered a project yet.
1154
+ const matchKey = lifecycleMatchKey(facts, facts.commandProjectRoot);
1155
+ if (!matchKey)
1156
+ return;
1157
+ let key = commandOption(facts.invocation, "hook-event-id");
1158
+ if (!key && commandOption(facts.invocation, "invocation-id"))
1159
+ return;
1160
+ if (!key) {
1161
+ try {
1162
+ key = findRecentMatchingHookEvent(context, { projectId: project.id, matchKey, errorCode: "trusted_session_binding_required", operation: facts.invocation.operation }).eventKey;
1163
+ }
1164
+ catch {
1165
+ return;
1166
+ } // Dispatch still owns its mandatory admission error.
1167
+ }
1168
+ const event = context.db.get("SELECT id FROM hook_events WHERE project_id = ? AND event_key = ? AND match_key = ? AND status = 'observed'", [project.id, key, matchKey]);
1169
+ if (event)
1170
+ context.observeLifecycleHook?.(project.id, event.id);
1171
+ }
1172
+ export function stageLifecycleMatchKey(operation, runId, stage, projectRoot, workId) {
1173
+ return crypto.createHash("sha256").update(JSON.stringify({ operation, run_id: runId, stage, project_root: resolveProjectRoot(projectRoot), ...(workId ? { work_id: workId } : {}) })).digest("hex");
1174
+ }
1175
+ export function recoveryAcceptMatchKey(runId, recoveryId, projectRoot) {
1176
+ return crypto.createHash("sha256").update(JSON.stringify({ operation: "recovery_accept", run_id: runId, recovery_id: recoveryId, project_root: resolveProjectRoot(projectRoot) })).digest("hex");
1177
+ }
1178
+ export function workLifecycleMatchKey(operation, workId, projectRoot) {
1179
+ return crypto.createHash("sha256").update(JSON.stringify({ operation, work_id: workId, project_root: resolveProjectRoot(projectRoot) })).digest("hex");
1180
+ }
1181
+ function workLifecycleMatchKeys(operation, workId, projectRoot) {
1182
+ return entityReferenceVariants(workId).map((reference) => workLifecycleMatchKey(operation, reference, projectRoot));
1183
+ }
1184
+ function stageLifecycleMatchKeys(operation, runId, stage, projectRoot, workId) {
1185
+ const runReferences = entityReferenceVariants(runId);
1186
+ const workReferences = workId ? entityReferenceVariants(workId) : [undefined];
1187
+ return [...new Set(runReferences.flatMap((runReference) => workReferences.map((workReference) => stageLifecycleMatchKey(operation, runReference, stage, projectRoot, workReference))))];
1188
+ }
1189
+ /** Claims a Work terminal command only after its exact hook receipt has proved
1190
+ * the physical Session that issued it. Work code performs the owner check. */
1191
+ export function claimWorkLifecycleHookEvent(context, input) {
1192
+ const matchKeys = workLifecycleMatchKeys(input.operation, input.workId, input.projectRoot);
1193
+ const eventKey = input.eventKey ?? findRecentMatchingHookEvent(context, {
1194
+ projectId: input.projectId, matchKey: matchKeys, errorCode: "trusted_session_binding_required", operation: input.operation.replace("_", " ")
1195
+ }).eventKey;
1196
+ const event = context.db.get("SELECT id, session_id, match_key, status FROM hook_events WHERE project_id = ? AND event_key = ?", [input.projectId, eventKey]);
1197
+ if (!event?.session_id || event.status !== "observed" || !event.match_key || !matchKeys.includes(event.match_key)) {
1198
+ const reason = !event ? "event_missing" : !event.session_id ? "identity_missing" : event.status !== "observed" ? "event_consumed" : "target_mismatch";
1199
+ throw new AppError("trusted_session_binding_required", `${input.operation.replace("_", " ")} requires one fresh matching PreToolUse hook event`, 1, { work_id: input.workId, event_key: eventKey, reason, observed_status: event?.status ?? null });
1200
+ }
1201
+ const claimed = context.db.run("UPDATE hook_events SET status = 'claimed', claimed_at = ? WHERE id = ? AND status = 'observed'", [context.now(), event.id]);
1202
+ if (claimed.changes !== 1)
1203
+ throw new AppError("trusted_session_binding_required", "matching Work lifecycle hook event was already claimed", 1, { work_id: input.workId });
1204
+ return hookSessionIdentity(context, input.projectId, eventKey);
1205
+ }
1206
+ /** Claims a lifecycle command only after its exact hook receipt has proved the
1207
+ * physical Session that issued it. Stage code performs the Work-owner check. */
1208
+ export function claimStageLifecycleHookEvent(context, input) {
1209
+ const matchKeys = stageLifecycleMatchKeys(input.operation, input.runId, input.stage, input.projectRoot, input.workId);
1210
+ const eventKey = input.eventKey ?? findRecentMatchingHookEvent(context, {
1211
+ projectId: input.projectId, matchKey: matchKeys, errorCode: "trusted_session_binding_required", operation: input.operation.replace("_", " ")
1212
+ }).eventKey;
1213
+ const event = context.db.get("SELECT id, session_id, match_key, status FROM hook_events WHERE project_id = ? AND event_key = ?", [input.projectId, eventKey]);
1214
+ if (!event?.session_id || event.status !== "observed" || !event.match_key || !matchKeys.includes(event.match_key)) {
1215
+ const reason = !event ? "event_missing" : !event.session_id ? "identity_missing" : event.status !== "observed" ? "event_consumed" : "target_mismatch";
1216
+ throw new AppError("trusted_session_binding_required", `${input.operation.replace("_", " ")} requires one fresh matching PreToolUse hook event`, 1, { run_id: input.runId, stage: input.stage, event_key: eventKey, reason, observed_status: event?.status ?? null });
1217
+ }
1218
+ const claimed = context.db.run("UPDATE hook_events SET status = 'claimed', claimed_at = ? WHERE id = ? AND status = 'observed'", [context.now(), event.id]);
1219
+ if (claimed.changes !== 1)
1220
+ throw new AppError("trusted_session_binding_required", "matching lifecycle hook event was already claimed", 1, { run_id: input.runId, stage: input.stage });
1221
+ return hookSessionIdentity(context, input.projectId, eventKey);
1222
+ }
764
1223
  /** Trusted session identity is created by PreToolUse, never supplied by an agent. */
765
1224
  export function sessionIdForHookEvent(context, projectId, eventKey) {
766
1225
  const event = context.db.get("SELECT session_id FROM hook_events WHERE project_id = ? AND event_key = ?", [projectId, eventKey]);
@@ -776,6 +1235,7 @@ export function hookSessionIdentity(context, projectId, eventKey) {
776
1235
  WHERE he.project_id = ? AND he.event_key = ?`, [projectId, eventKey]);
777
1236
  if (!event?.session_id)
778
1237
  throw new AppError("hook_event_not_found", "stage start requires a trusted PreToolUse hook event", 1, { event_key: eventKey });
1238
+ context.observeLifecycleHook?.(projectId, event.id);
779
1239
  return {
780
1240
  hookEventId: event.id,
781
1241
  harness: event.harness,
@@ -812,21 +1272,30 @@ export function claimRecentMatchingHookEvent(context, input) {
812
1272
  /** Finds the trusted event that a stage- or Work-specific claimant will atomically claim. */
813
1273
  export function findRecentMatchingHookEvent(context, input) {
814
1274
  const freshAfter = new Date(Date.parse(context.now()) - 60_000).toISOString();
815
- const find = () => context.db.get(`SELECT id, event_key FROM hook_events
816
- WHERE project_id = ? AND match_key = ? AND status = 'observed' AND created_at >= ?
817
- ORDER BY id DESC LIMIT 1`, [input.projectId, input.matchKey, freshAfter]);
818
- let event = find();
819
- // Some harnesses publish nested-agent tool events concurrently with command
820
- // execution. Give the trusted event a short bounded window to reach SQLite.
1275
+ const matchKeys = Array.isArray(input.matchKey) ? [...new Set(input.matchKey)] : [input.matchKey];
1276
+ const placeholders = matchKeys.map(() => "?").join(", ");
1277
+ const find = () => context.db.all(`SELECT id, event_key FROM hook_events
1278
+ WHERE project_id = ? AND match_key IN (${placeholders}) AND status = 'observed' AND created_at >= ?
1279
+ AND NOT (COALESCE(json_extract(outcome_json, '$.effect'), '') = 'no_effect'
1280
+ AND COALESCE(json_extract(outcome_json, '$.disposition'), '') IN ('correctable', 'fatal'))
1281
+ AND harness IN ('codex-desktop', 'antigravity-cli')
1282
+ AND (? IS NULL OR session_id = ?)
1283
+ ORDER BY id DESC LIMIT 2`, [input.projectId, ...matchKeys, freshAfter, input.sessionId ?? null, input.sessionId ?? null]);
1284
+ let events = find();
1285
+ // Only the qualified non-rewriting transports may correlate the original
1286
+ // command: Codex Desktop and Antigravity. New harnesses fail closed by default.
821
1287
  const deadline = Date.now() + 250;
822
- while (!event?.event_key && Date.now() < deadline) {
1288
+ while (events.length === 0 && Date.now() < deadline) {
823
1289
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
824
- event = find();
1290
+ events = find();
825
1291
  }
826
- if (!event?.event_key) {
827
- throw new AppError(input.errorCode, `${input.operation} requires one matching harness lifecycle event. Stay in this Session and retry the exact standalone lifecycle command; do not create a new Session.`, 1);
1292
+ if (events.length === 0 || !events[0]?.event_key) {
1293
+ throw new AppError(input.errorCode, `${input.operation} requires a trusted lifecycle receipt. Check native hook delivery; managed commands are admitted from committed native identity, not from model-supplied invocation IDs or updatedInput.`, 1, { reason: "event_missing" });
828
1294
  }
829
- return { id: event.id, eventKey: event.event_key };
1295
+ if (events.length > 1) {
1296
+ throw new AppError("ambiguous_lifecycle_receipt", `${input.operation} has more than one fresh matching lifecycle event; stop and let the controller reconcile native delivery`, 1, { reason: "multiple_live_events", match_keys: matchKeys, event_keys: events.map((event) => event.event_key) });
1297
+ }
1298
+ return { id: events[0].id, eventKey: events[0].event_key };
830
1299
  }
831
1300
  /** Fallback for Desktop paths that execute the original command after a hook rewrite. */
832
1301
  export function claimBootstrapHookEvent(context, input) {
@@ -849,9 +1318,9 @@ export function bootstrapMatchKey(input) {
849
1318
  })).digest("hex");
850
1319
  }
851
1320
  export function claimWorkStartHookEvent(context, input) {
852
- const expected = new Set([workStartMatchKey(input.workId, input.projectRoot), workStartMatchKey(shortWorkReference(input.workId), input.projectRoot)]);
1321
+ const expected = entityReferenceVariants(input.workId).map((reference) => workStartMatchKey(reference, input.projectRoot, input.recoveryId));
853
1322
  const event = context.db.get("SELECT id, session_id, agent_id, turn_id, transcript_path, model, agent_type, match_key, status FROM hook_events WHERE project_id = ? AND event_key = ?", [input.projectId, input.eventKey]);
854
- if (!event?.session_id || event.status !== "observed" || !event.match_key || !expected.has(event.match_key)) {
1323
+ if (!event?.session_id || event.status !== "observed" || !event.match_key || !expected.includes(event.match_key)) {
855
1324
  throw new AppError("trusted_work_launch_required", "work start requires one fresh matching PreToolUse event", 1, { work_id: input.workId, event_key: input.eventKey });
856
1325
  }
857
1326
  const claimed = context.db.run("UPDATE hook_events SET status = 'claimed', claimed_at = ? WHERE id = ? AND status = 'observed'", [context.now(), event.id]);
@@ -859,15 +1328,16 @@ export function claimWorkStartHookEvent(context, input) {
859
1328
  throw new AppError("trusted_work_launch_required", "matching work-start hook event was already claimed", 1, { work_id: input.workId });
860
1329
  return hookSessionIdentity(context, input.projectId, input.eventKey);
861
1330
  }
862
- function shortWorkReference(id) { return /^WRK-\d{3,}(?:-|$)/.exec(id)?.[0]?.replace(/-$/, "") ?? id; }
863
1331
  /** Claims the exact non-bootstrap stage entry which starts a coordinator Work. */
864
1332
  export function assertStageStartHookEvent(context, input) {
865
- const expected = stageStartMatchKey(input.runId, input.stage, input.projectRoot, input.contextSha256);
1333
+ const expected = entityReferenceVariants(input.runId).map((reference) => stageStartMatchKey(reference, input.stage, input.projectRoot, input.contextSha256));
866
1334
  const event = context.db.get("SELECT session_id, match_key, status FROM hook_events WHERE project_id = ? AND event_key = ?", [input.projectId, input.eventKey]);
867
- if (!event?.session_id || event.status !== "observed" || event.match_key !== expected) {
868
- throw new AppError("trusted_stage_launch_required", "stage start requires one fresh matching PreToolUse event", 1, { run_id: input.runId, stage: input.stage, event_key: input.eventKey, expected_match_key: expected, observed_match_key: event?.match_key ?? null, observed_status: event?.status ?? null });
1335
+ if (!event?.session_id || event.status !== "observed" || !event.match_key || !expected.includes(event.match_key)) {
1336
+ throw new AppError("trusted_stage_launch_required", "stage start requires one fresh matching PreToolUse event", 1, { run_id: input.runId, stage: input.stage, event_key: input.eventKey, expected_match_keys: expected, observed_match_key: event?.match_key ?? null, observed_status: event?.status ?? null });
869
1337
  }
870
- return hookSessionIdentity(context, input.projectId, input.eventKey);
1338
+ const identity = hookSessionIdentity(context, input.projectId, input.eventKey);
1339
+ assertRecoveryHookCurrent(context, input.projectId, input.runId, identity);
1340
+ return identity;
871
1341
  }
872
1342
  export function claimStageStartHookEvent(context, input) {
873
1343
  assertStageStartHookEvent(context, input);
@@ -897,24 +1367,25 @@ export function stageResumeMatchKey(runId, stage, workId, projectRoot) {
897
1367
  }
898
1368
  /** Claims the resume command observed in the Session that received the user answer. */
899
1369
  export function claimStageResumeHookEvent(context, input) {
900
- const matchKey = stageResumeMatchKey(input.runId, input.stage, input.workId, input.projectRoot);
901
- const freshAfter = new Date(Date.parse(context.now()) - 60_000).toISOString();
902
- const event = input.eventKey
903
- ? context.db.get("SELECT id, event_key, session_id, match_key, status FROM hook_events WHERE project_id = ? AND event_key = ?", [input.projectId, input.eventKey])
904
- : context.db.get("SELECT id, event_key, session_id, match_key, status FROM hook_events WHERE project_id = ? AND session_id = ? AND match_key = ? AND status = 'observed' AND created_at >= ? ORDER BY id DESC LIMIT 1", [input.projectId, input.sessionId ?? "", matchKey, freshAfter]);
905
- if (!event?.event_key || !event.session_id || event.match_key !== matchKey || event.status !== "observed") {
1370
+ const matchKeys = entityReferenceVariants(input.runId).flatMap((runReference) => entityReferenceVariants(input.workId).map((workReference) => stageResumeMatchKey(runReference, input.stage, workReference, input.projectRoot)));
1371
+ const eventKey = input.eventKey ?? findRecentMatchingHookEvent(context, { projectId: input.projectId, matchKey: matchKeys, sessionId: input.sessionId ?? "", errorCode: "trusted_stage_resume_required", operation: "stage resume" }).eventKey;
1372
+ const event = context.db.get("SELECT id, event_key, session_id, match_key, status FROM hook_events WHERE project_id = ? AND event_key = ?", [input.projectId, eventKey]);
1373
+ if (!event?.event_key || !event.session_id || !event.match_key || !matchKeys.includes(event.match_key) || event.status !== "observed") {
906
1374
  throw new AppError("trusted_stage_resume_required", "stage resume requires one fresh matching PreToolUse event", 1, { run_id: input.runId, stage: input.stage, work_id: input.workId });
907
1375
  }
1376
+ const identity = hookSessionIdentity(context, input.projectId, event.event_key);
1377
+ assertRecoveryHookCurrent(context, input.projectId, input.runId, identity);
908
1378
  const claimed = context.db.run("UPDATE hook_events SET status = 'claimed', claimed_at = ? WHERE id = ? AND status = 'observed'", [context.now(), event.id]);
909
1379
  if (claimed.changes !== 1)
910
1380
  throw new AppError("trusted_stage_resume_required", "matching stage-resume hook event was already claimed", 1, { run_id: input.runId, stage: input.stage });
911
- return hookSessionIdentity(context, input.projectId, event.event_key);
1381
+ return identity;
912
1382
  }
913
- export function workStartMatchKey(workId, projectRoot) {
1383
+ export function workStartMatchKey(workId, projectRoot, recoveryId) {
914
1384
  return crypto.createHash("sha256").update(JSON.stringify({
915
1385
  operation: "work_start",
916
1386
  work_id: workId,
917
- project_root: resolveProjectRoot(projectRoot)
1387
+ project_root: resolveProjectRoot(projectRoot),
1388
+ ...(recoveryId ? { recovery_id: recoveryId } : {})
918
1389
  })).digest("hex");
919
1390
  }
920
1391
  export function hookStatusForProject(context, projectId) {
@@ -954,6 +1425,8 @@ export function codexHookEventsForProject(context, projectId) {
954
1425
  FROM hook_events WHERE project_id = ? ORDER BY created_at DESC, id DESC LIMIT 20`, [projectId]);
955
1426
  }
956
1427
  function resolveProfileName(profile) {
1428
+ if (profile !== undefined && (typeof profile !== "string" || !profile.trim()))
1429
+ throw new AppError("validation", "--profile must be nonempty text", 2);
957
1430
  return sanitizeProfile(profile ?? defaultProfileName);
958
1431
  }
959
1432
  function sanitizeProfile(value) {
@@ -981,11 +1454,12 @@ function readTomlIfExists(file) {
981
1454
  if (!fs.existsSync(file)) {
982
1455
  return {};
983
1456
  }
1457
+ const { text } = prepareTextFile(file, "Codex config");
984
1458
  try {
985
- return parse(fs.readFileSync(file, "utf8"));
1459
+ return parse(text);
986
1460
  }
987
1461
  catch (error) {
988
- throw new AppError("validation", `Invalid TOML in ${file}: ${String(error)}`, 2);
1462
+ throw new AppError("validation", `Invalid TOML in ${file}`, 2, { file, ...(error instanceof TomlError ? { line: error.line, column: error.column } : {}) });
989
1463
  }
990
1464
  }
991
1465
  function withManagedConfig(config, sourceHome, targetHome) {
@@ -1004,7 +1478,7 @@ function objectTable(value) {
1004
1478
  function createSharedLinks(sourceHome, targetHome) {
1005
1479
  const linked = [];
1006
1480
  const skipped = [];
1007
- for (const entry of sharedEntries) {
1481
+ for (const entry of codexHomeSharedEntries) {
1008
1482
  const source = path.join(sourceHome, entry);
1009
1483
  const target = path.join(targetHome, entry);
1010
1484
  if (!fs.existsSync(source)) {
@@ -1027,7 +1501,7 @@ function createSharedLinks(sourceHome, targetHome) {
1027
1501
  return { linked, skipped };
1028
1502
  }
1029
1503
  function removeSharedSymlinks(targetHome) {
1030
- for (const entry of sharedEntries) {
1504
+ for (const entry of codexHomeSharedEntries) {
1031
1505
  const target = path.join(targetHome, entry);
1032
1506
  if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink()) {
1033
1507
  fs.rmSync(target);
@@ -1040,7 +1514,7 @@ function removeDirIfEmpty(dir) {
1040
1514
  }
1041
1515
  }
1042
1516
  function sharedEntryStatus(sourceHome, targetHome) {
1043
- return sharedEntries.map((entry) => {
1517
+ return codexHomeSharedEntries.map((entry) => {
1044
1518
  const source = path.join(sourceHome, entry);
1045
1519
  const target = path.join(targetHome, entry);
1046
1520
  return {
@@ -1061,11 +1535,16 @@ function hookEntry(matcher, command, statusMessage) {
1061
1535
  return { matcher, hooks: [commandHook(command, statusMessage)] };
1062
1536
  }
1063
1537
  function commandHook(command, statusMessage) {
1064
- return { type: "command", command, timeout: 5, statusMessage };
1538
+ // Hook admission is synchronous (the CLI must see a committed receipt), but
1539
+ // Admission keeps its own shared 20s deadline. Leave separate headroom for
1540
+ // the shell, cold CLI imports and bounded subprocess cleanup: the host must
1541
+ // not kill the hook before it can report its typed admission failure.
1542
+ return { type: "command", command, timeout: 60, statusMessage };
1065
1543
  }
1066
1544
  function hookCommand(event) {
1067
- const handler = `PATH="\${HOME}/Library/pnpm:/opt/homebrew/bin:/usr/local/bin:\${PATH}" dd-flow codex hook handle --event ${event} --json`;
1068
- return `/bin/bash -c 'log_dir="\${DD_FLOW_HOME:-\${HOME}/.dd-flow}/logs"; mkdir -p "$log_dir"; log="$log_dir/codex-hook.log"; printf "%s event=${event} phase=start pid=%s\\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$$" >>"$log"; ${handler} 2> >(tee -a "$log" >&2); status=$?; printf "%s event=${event} phase=finish pid=%s exit=%s\\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$$" "$status" >>"$log"; exit "$status"'`;
1545
+ const args = `codex hook handle --event ${event} --json`;
1546
+ const handler = `if [ -n "\${DD_FLOW_BIN:-}" ]; then "$DD_FLOW_BIN" ${args}; else dd-flow ${args}; fi`;
1547
+ return `/bin/bash -c 'export PATH="\${DD_FLOW_HOME:+\${DD_FLOW_HOME}/bin:}\${PATH}:\${HOME}/Library/pnpm:/opt/homebrew/bin:/usr/local/bin"; log_dir="\${DD_FLOW_HOME:-\${HOME}/.dd-flow}/logs"; mkdir -p "$log_dir"; log="$log_dir/codex-hook.log"; printf "%s event=${event} phase=start pid=%s\\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$$" >>"$log"; ${handler} 2> >(tee -a "$log" >&2); status=$?; printf "%s event=${event} phase=finish pid=%s exit=%s\\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$$" "$status" >>"$log"; exit "$status"'`;
1069
1548
  }
1070
1549
  function resolveHookTarget(target) {
1071
1550
  if (!target || target === "isolated") {
@@ -1240,11 +1719,10 @@ function recordHookInstallation(context, projectId, scope, configPath, installed
1240
1719
  function installationScope(target, profile, hooksPath) {
1241
1720
  return target === "isolated" ? `isolated:${profile}` : `default:${path.dirname(hooksPath)}`;
1242
1721
  }
1243
- function upsertSessionBindingFromPayload(context, project, sessionId, payload) {
1722
+ function upsertSessionBindingFromPayload(context, project, sessionId, payload, transcriptPath) {
1244
1723
  const existing = sessionBinding(context, project.id, sessionId);
1245
1724
  const now = context.now();
1246
1725
  const cwd = stringValue(payload.cwd) ?? existing?.cwd ?? null;
1247
- const transcriptPath = stringValue(payload.transcript_path) ?? existing?.transcript_path ?? locateCodexTranscript(sessionId, context.env.CODEX_HOME);
1248
1726
  context.db.run(`INSERT INTO codex_session_bindings
1249
1727
  (session_id, project_id, protocol_id, handshake_id, cwd, transcript_path, status, created_at, updated_at)
1250
1728
  VALUES (?, ?, NULL, NULL, ?, ?, 'active', ?, ?)
@@ -1298,12 +1776,11 @@ function sessionBinding(context, projectId, sessionId) {
1298
1776
  return context.db.get("SELECT * FROM codex_session_bindings WHERE project_id = ? AND session_id = ?", [projectId, sessionId]);
1299
1777
  }
1300
1778
  function recordHookEvent(context, input) {
1301
- const existing = context.db.get("SELECT id FROM hook_events WHERE project_id = ? AND event_key = ?", [input.projectId, input.eventKey]);
1302
- if (existing)
1779
+ if (assertHookEventReplay(context, input))
1303
1780
  return false;
1304
1781
  context.db.run(`INSERT INTO hook_events
1305
- (project_id, protocol_id, harness, provider_session_id, parent_session_id, daemon_id, session_id, agent_id, turn_id, transcript_path, provider, model, reasoning, mode, agent_type, cwd, event_key, match_key, event_name, tool_name, status, sanitized_summary, created_at)
1306
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1782
+ (project_id, protocol_id, harness, provider_session_id, parent_session_id, daemon_id, session_id, agent_id, turn_id, transcript_path, provider, model, reasoning, mode, agent_type, cwd, event_key, match_key, event_name, tool_name, status, sanitized_summary, created_at)
1783
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1307
1784
  input.projectId,
1308
1785
  input.protocolId,
1309
1786
  input.harness,
@@ -1330,6 +1807,23 @@ function recordHookEvent(context, input) {
1330
1807
  ]);
1331
1808
  return true;
1332
1809
  }
1810
+ /** A duplicate delivery is harmless only when all durable identity facts match. */
1811
+ function assertHookEventReplay(context, input) {
1812
+ const existing = context.db.get(`SELECT harness, provider_session_id, parent_session_id, daemon_id, session_id, turn_id, event_name, tool_name, match_key, cwd
1813
+ FROM hook_events WHERE project_id = ? AND event_key = ?`, [input.projectId, input.eventKey]);
1814
+ if (existing) {
1815
+ const observed = {
1816
+ harness: input.harness, provider_session_id: input.providerSessionId, parent_session_id: input.parentSessionId,
1817
+ daemon_id: input.daemonId ?? null, session_id: input.sessionId, turn_id: input.turnId,
1818
+ event_name: input.eventName, tool_name: input.toolName, match_key: input.matchKey, cwd: input.cwd
1819
+ };
1820
+ if (Object.entries(observed).some(([key, value]) => existing[key] !== value)) {
1821
+ throw new AppError("hook_event_replay_mismatch", "Lifecycle event ID was already observed with different immutable identity", 1, { event_key: input.eventKey });
1822
+ }
1823
+ return true;
1824
+ }
1825
+ return false;
1826
+ }
1333
1827
  function projectForHook(context, explicitRoot, cwd) {
1334
1828
  if (explicitRoot)
1335
1829
  return requireProjectByRoot(context, resolveProjectRoot(explicitRoot));
@@ -1356,14 +1850,6 @@ function hookEventKey(payload, eventName, toolName, command) {
1356
1850
  command
1357
1851
  })).digest("hex");
1358
1852
  }
1359
- function commandFromPayload(payload) {
1360
- const direct = stringValue(payload.command);
1361
- if (direct) {
1362
- return direct;
1363
- }
1364
- const toolInput = objectRecord(payload.tool_input);
1365
- return stringValue(toolInput.command) ?? stringValue(toolInput.cmd);
1366
- }
1367
1853
  function toolNameFromPayload(payload) {
1368
1854
  const toolInput = objectRecord(payload.tool_input);
1369
1855
  return stringValue(toolInput.name);
@@ -1406,10 +1892,10 @@ function writeJsonFile(file, value) {
1406
1892
  ensureDir(path.dirname(file));
1407
1893
  fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
1408
1894
  }
1409
- function backupFile(file, now) {
1895
+ function backupFile(file, now, text) {
1410
1896
  const suffix = now.replace(/[:.]/g, "-");
1411
1897
  const backup = `${file}.${suffix}.bak`;
1412
- fs.copyFileSync(file, backup);
1898
+ fs.writeFileSync(backup, text, { mode: 0o600 });
1413
1899
  return backup;
1414
1900
  }
1415
1901
  function hashObject(value) {