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

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