oh-my-codex 0.20.3 → 0.20.5

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 (661) hide show
  1. package/Cargo.lock +35 -18
  2. package/Cargo.toml +1 -1
  3. package/README.md +2 -2
  4. package/crates/omx-runtime/Cargo.toml +3 -0
  5. package/crates/omx-runtime/src/main.rs +390 -0
  6. package/crates/omx-runtime/tests/execution.rs +264 -0
  7. package/dist/adapt/__tests__/herdr-target.test.d.ts +2 -0
  8. package/dist/adapt/__tests__/herdr-target.test.d.ts.map +1 -0
  9. package/dist/adapt/__tests__/herdr-target.test.js +72 -0
  10. package/dist/adapt/__tests__/herdr-target.test.js.map +1 -0
  11. package/dist/adapt/contracts.d.ts +1 -1
  12. package/dist/adapt/contracts.d.ts.map +1 -1
  13. package/dist/adapt/contracts.js +1 -1
  14. package/dist/adapt/contracts.js.map +1 -1
  15. package/dist/adapt/herdr/__tests__/authority.test.d.ts +2 -0
  16. package/dist/adapt/herdr/__tests__/authority.test.d.ts.map +1 -0
  17. package/dist/adapt/herdr/__tests__/authority.test.js +49 -0
  18. package/dist/adapt/herdr/__tests__/authority.test.js.map +1 -0
  19. package/dist/adapt/herdr/__tests__/bridge.test.d.ts +2 -0
  20. package/dist/adapt/herdr/__tests__/bridge.test.d.ts.map +1 -0
  21. package/dist/adapt/herdr/__tests__/bridge.test.js +176 -0
  22. package/dist/adapt/herdr/__tests__/bridge.test.js.map +1 -0
  23. package/dist/adapt/herdr/__tests__/sanitize.test.d.ts +2 -0
  24. package/dist/adapt/herdr/__tests__/sanitize.test.d.ts.map +1 -0
  25. package/dist/adapt/herdr/__tests__/sanitize.test.js +45 -0
  26. package/dist/adapt/herdr/__tests__/sanitize.test.js.map +1 -0
  27. package/dist/adapt/herdr/__tests__/semantic.test.d.ts +2 -0
  28. package/dist/adapt/herdr/__tests__/semantic.test.d.ts.map +1 -0
  29. package/dist/adapt/herdr/__tests__/semantic.test.js +130 -0
  30. package/dist/adapt/herdr/__tests__/semantic.test.js.map +1 -0
  31. package/dist/adapt/herdr/__tests__/seq-store.test.d.ts +2 -0
  32. package/dist/adapt/herdr/__tests__/seq-store.test.d.ts.map +1 -0
  33. package/dist/adapt/herdr/__tests__/seq-store.test.js +54 -0
  34. package/dist/adapt/herdr/__tests__/seq-store.test.js.map +1 -0
  35. package/dist/adapt/herdr/__tests__/socket-live.test.d.ts +2 -0
  36. package/dist/adapt/herdr/__tests__/socket-live.test.d.ts.map +1 -0
  37. package/dist/adapt/herdr/__tests__/socket-live.test.js +104 -0
  38. package/dist/adapt/herdr/__tests__/socket-live.test.js.map +1 -0
  39. package/dist/adapt/herdr/__tests__/team-map.test.d.ts +2 -0
  40. package/dist/adapt/herdr/__tests__/team-map.test.d.ts.map +1 -0
  41. package/dist/adapt/herdr/__tests__/team-map.test.js +46 -0
  42. package/dist/adapt/herdr/__tests__/team-map.test.js.map +1 -0
  43. package/dist/adapt/herdr/__tests__/transport.test.d.ts +2 -0
  44. package/dist/adapt/herdr/__tests__/transport.test.d.ts.map +1 -0
  45. package/dist/adapt/herdr/__tests__/transport.test.js +160 -0
  46. package/dist/adapt/herdr/__tests__/transport.test.js.map +1 -0
  47. package/dist/adapt/herdr/__tests__/wiring.test.d.ts +2 -0
  48. package/dist/adapt/herdr/__tests__/wiring.test.d.ts.map +1 -0
  49. package/dist/adapt/herdr/__tests__/wiring.test.js +118 -0
  50. package/dist/adapt/herdr/__tests__/wiring.test.js.map +1 -0
  51. package/dist/adapt/herdr/authority.d.ts +34 -0
  52. package/dist/adapt/herdr/authority.d.ts.map +1 -0
  53. package/dist/adapt/herdr/authority.js +71 -0
  54. package/dist/adapt/herdr/authority.js.map +1 -0
  55. package/dist/adapt/herdr/bridge.d.ts +84 -0
  56. package/dist/adapt/herdr/bridge.d.ts.map +1 -0
  57. package/dist/adapt/herdr/bridge.js +222 -0
  58. package/dist/adapt/herdr/bridge.js.map +1 -0
  59. package/dist/adapt/herdr/sanitize.d.ts +20 -0
  60. package/dist/adapt/herdr/sanitize.d.ts.map +1 -0
  61. package/dist/adapt/herdr/sanitize.js +70 -0
  62. package/dist/adapt/herdr/sanitize.js.map +1 -0
  63. package/dist/adapt/herdr/semantic.d.ts +67 -0
  64. package/dist/adapt/herdr/semantic.d.ts.map +1 -0
  65. package/dist/adapt/herdr/semantic.js +170 -0
  66. package/dist/adapt/herdr/semantic.js.map +1 -0
  67. package/dist/adapt/herdr/seq-store.d.ts +32 -0
  68. package/dist/adapt/herdr/seq-store.d.ts.map +1 -0
  69. package/dist/adapt/herdr/seq-store.js +126 -0
  70. package/dist/adapt/herdr/seq-store.js.map +1 -0
  71. package/dist/adapt/herdr/team-map.d.ts +30 -0
  72. package/dist/adapt/herdr/team-map.d.ts.map +1 -0
  73. package/dist/adapt/herdr/team-map.js +37 -0
  74. package/dist/adapt/herdr/team-map.js.map +1 -0
  75. package/dist/adapt/herdr/transport.d.ts +115 -0
  76. package/dist/adapt/herdr/transport.d.ts.map +1 -0
  77. package/dist/adapt/herdr/transport.js +260 -0
  78. package/dist/adapt/herdr/transport.js.map +1 -0
  79. package/dist/adapt/herdr/wiring.d.ts +24 -0
  80. package/dist/adapt/herdr/wiring.d.ts.map +1 -0
  81. package/dist/adapt/herdr/wiring.js +62 -0
  82. package/dist/adapt/herdr/wiring.js.map +1 -0
  83. package/dist/adapt/herdr.d.ts +19 -0
  84. package/dist/adapt/herdr.d.ts.map +1 -0
  85. package/dist/adapt/herdr.js +115 -0
  86. package/dist/adapt/herdr.js.map +1 -0
  87. package/dist/adapt/index.d.ts +4 -0
  88. package/dist/adapt/index.d.ts.map +1 -1
  89. package/dist/adapt/index.js +48 -0
  90. package/dist/adapt/index.js.map +1 -1
  91. package/dist/adapt/registry.d.ts.map +1 -1
  92. package/dist/adapt/registry.js +11 -0
  93. package/dist/adapt/registry.js.map +1 -1
  94. package/dist/auth/__tests__/hotswap.test.js +60 -1
  95. package/dist/auth/__tests__/hotswap.test.js.map +1 -1
  96. package/dist/auth/__tests__/storage.test.js +33 -1
  97. package/dist/auth/__tests__/storage.test.js.map +1 -1
  98. package/dist/auth/hotswap.d.ts +16 -2
  99. package/dist/auth/hotswap.d.ts.map +1 -1
  100. package/dist/auth/hotswap.js +22 -7
  101. package/dist/auth/hotswap.js.map +1 -1
  102. package/dist/auth/storage.js +1 -1
  103. package/dist/auth/storage.js.map +1 -1
  104. package/dist/autopilot/__tests__/ralplan-gate.test.js +86 -1333
  105. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  106. package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
  107. package/dist/autopilot/ralplan-gate.js +7 -6
  108. package/dist/autopilot/ralplan-gate.js.map +1 -1
  109. package/dist/cli/__tests__/agents-init.test.js +4 -23
  110. package/dist/cli/__tests__/agents-init.test.js.map +1 -1
  111. package/dist/cli/__tests__/api.test.js +61 -24
  112. package/dist/cli/__tests__/api.test.js.map +1 -1
  113. package/dist/cli/__tests__/auth.test.js +3 -1
  114. package/dist/cli/__tests__/auth.test.js.map +1 -1
  115. package/dist/cli/__tests__/codex-feature-probe.test.js +66 -1
  116. package/dist/cli/__tests__/codex-feature-probe.test.js.map +1 -1
  117. package/dist/cli/__tests__/codex-plugin-layout.test.js +70 -2
  118. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  119. package/dist/cli/__tests__/detached-leader-teardown.test.d.ts +2 -0
  120. package/dist/cli/__tests__/detached-leader-teardown.test.d.ts.map +1 -0
  121. package/dist/cli/__tests__/detached-leader-teardown.test.js +473 -0
  122. package/dist/cli/__tests__/detached-leader-teardown.test.js.map +1 -0
  123. package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts +2 -0
  124. package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts.map +1 -0
  125. package/dist/cli/__tests__/doctor-process-identity-readiness.test.js +103 -0
  126. package/dist/cli/__tests__/doctor-process-identity-readiness.test.js.map +1 -0
  127. package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts +2 -0
  128. package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts.map +1 -0
  129. package/dist/cli/__tests__/doctor-state-root-binding.test.js +139 -0
  130. package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -0
  131. package/dist/cli/__tests__/doctor-warning-copy.test.js +35 -3
  132. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  133. package/dist/cli/__tests__/exec.test.js +75 -0
  134. package/dist/cli/__tests__/exec.test.js.map +1 -1
  135. package/dist/cli/__tests__/index.test.js +621 -460
  136. package/dist/cli/__tests__/index.test.js.map +1 -1
  137. package/dist/cli/__tests__/launch-fallback.test.js +1020 -143
  138. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  139. package/dist/cli/__tests__/mcp-parity.test.js +4 -3
  140. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
  141. package/dist/cli/__tests__/native-assets.test.js +398 -16
  142. package/dist/cli/__tests__/native-assets.test.js.map +1 -1
  143. package/dist/cli/__tests__/native-hook-claim-journal.test.js +53 -5
  144. package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
  145. package/dist/cli/__tests__/ralph.test.js +2 -2
  146. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  147. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +57 -57
  148. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
  149. package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts +2 -0
  150. package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts.map +1 -0
  151. package/dist/cli/__tests__/ralplan-child-binding-3212.test.js +56 -0
  152. package/dist/cli/__tests__/ralplan-child-binding-3212.test.js.map +1 -0
  153. package/dist/cli/__tests__/ralplan.test.js +84 -12
  154. package/dist/cli/__tests__/ralplan.test.js.map +1 -1
  155. package/dist/cli/__tests__/session-scoped-runtime.test.js +1118 -43
  156. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  157. package/dist/cli/__tests__/setup-agents-overwrite.test.js +8 -55
  158. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  159. package/dist/cli/__tests__/setup-install-mode.test.js +183 -4
  160. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  161. package/dist/cli/__tests__/sparkshell-cli.test.js +93 -41
  162. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  163. package/dist/cli/__tests__/team.test.js +45 -1
  164. package/dist/cli/__tests__/team.test.js.map +1 -1
  165. package/dist/cli/__tests__/ultragoal.test.js +117 -2
  166. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  167. package/dist/cli/__tests__/uninstall.test.js +50 -5
  168. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  169. package/dist/cli/__tests__/update.test.js +669 -177
  170. package/dist/cli/__tests__/update.test.js.map +1 -1
  171. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +32 -2
  172. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  173. package/dist/cli/api.d.ts.map +1 -1
  174. package/dist/cli/api.js +9 -10
  175. package/dist/cli/api.js.map +1 -1
  176. package/dist/cli/codex-feature-probe.d.ts +16 -0
  177. package/dist/cli/codex-feature-probe.d.ts.map +1 -1
  178. package/dist/cli/codex-feature-probe.js +44 -0
  179. package/dist/cli/codex-feature-probe.js.map +1 -1
  180. package/dist/cli/codex-home.d.ts.map +1 -1
  181. package/dist/cli/codex-home.js +7 -4
  182. package/dist/cli/codex-home.js.map +1 -1
  183. package/dist/cli/doctor.d.ts +17 -0
  184. package/dist/cli/doctor.d.ts.map +1 -1
  185. package/dist/cli/doctor.js +277 -3
  186. package/dist/cli/doctor.js.map +1 -1
  187. package/dist/cli/index.d.ts +231 -12
  188. package/dist/cli/index.d.ts.map +1 -1
  189. package/dist/cli/index.js +2298 -735
  190. package/dist/cli/index.js.map +1 -1
  191. package/dist/cli/native-assets.d.ts +25 -0
  192. package/dist/cli/native-assets.d.ts.map +1 -1
  193. package/dist/cli/native-assets.js +448 -51
  194. package/dist/cli/native-assets.js.map +1 -1
  195. package/dist/cli/native-hook-claim-journal.d.ts +8 -4
  196. package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
  197. package/dist/cli/native-hook-claim-journal.js +30 -8
  198. package/dist/cli/native-hook-claim-journal.js.map +1 -1
  199. package/dist/cli/package-manager-ownership.d.ts +63 -0
  200. package/dist/cli/package-manager-ownership.d.ts.map +1 -0
  201. package/dist/cli/package-manager-ownership.js +323 -0
  202. package/dist/cli/package-manager-ownership.js.map +1 -0
  203. package/dist/cli/ralph.js +2 -2
  204. package/dist/cli/ralph.js.map +1 -1
  205. package/dist/cli/ralplan.d.ts +4 -2
  206. package/dist/cli/ralplan.d.ts.map +1 -1
  207. package/dist/cli/ralplan.js +76 -34
  208. package/dist/cli/ralplan.js.map +1 -1
  209. package/dist/cli/setup-preferences.d.ts +15 -0
  210. package/dist/cli/setup-preferences.d.ts.map +1 -1
  211. package/dist/cli/setup-preferences.js +27 -1
  212. package/dist/cli/setup-preferences.js.map +1 -1
  213. package/dist/cli/setup.d.ts +3 -0
  214. package/dist/cli/setup.d.ts.map +1 -1
  215. package/dist/cli/setup.js +55 -32
  216. package/dist/cli/setup.js.map +1 -1
  217. package/dist/cli/sparkshell.d.ts.map +1 -1
  218. package/dist/cli/sparkshell.js +52 -9
  219. package/dist/cli/sparkshell.js.map +1 -1
  220. package/dist/cli/ultragoal.d.ts.map +1 -1
  221. package/dist/cli/ultragoal.js +2 -2
  222. package/dist/cli/ultragoal.js.map +1 -1
  223. package/dist/cli/uninstall.d.ts.map +1 -1
  224. package/dist/cli/uninstall.js +27 -28
  225. package/dist/cli/uninstall.js.map +1 -1
  226. package/dist/cli/update-worker.d.ts +2 -0
  227. package/dist/cli/update-worker.d.ts.map +1 -0
  228. package/dist/cli/update-worker.js +142 -0
  229. package/dist/cli/update-worker.js.map +1 -0
  230. package/dist/cli/update.d.ts +14 -22
  231. package/dist/cli/update.d.ts.map +1 -1
  232. package/dist/cli/update.js +192 -241
  233. package/dist/cli/update.js.map +1 -1
  234. package/dist/config/__tests__/generator-idempotent.test.js +173 -29
  235. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  236. package/dist/config/__tests__/generator-notify.test.js +3 -1
  237. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  238. package/dist/config/generator.d.ts +25 -3
  239. package/dist/config/generator.d.ts.map +1 -1
  240. package/dist/config/generator.js +113 -35
  241. package/dist/config/generator.js.map +1 -1
  242. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +25 -0
  243. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
  244. package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -1
  245. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
  246. package/dist/goal-workflows/codex-goal-snapshot.js +2 -0
  247. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
  248. package/dist/hooks/__tests__/agents-overlay.test.js +3 -1
  249. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  250. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +27 -4
  251. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  252. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts +2 -0
  253. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts.map +1 -0
  254. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js +172 -0
  255. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js.map +1 -0
  256. package/dist/hooks/__tests__/keyword-detector.test.js +172 -33
  257. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  258. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +173 -11
  259. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  260. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +1 -0
  261. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  262. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +65 -17
  263. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  264. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +27 -0
  265. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  266. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +10 -118
  267. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
  268. package/dist/hooks/__tests__/notify-hook-regression-205.test.js +7 -0
  269. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
  270. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +2 -2
  271. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  272. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +4 -1
  273. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  274. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js +2 -1
  275. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js.map +1 -1
  276. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +1 -0
  277. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  278. package/dist/hooks/__tests__/prompt-guidance-contract.test.js +9 -0
  279. package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
  280. package/dist/hooks/__tests__/session.test.js +2976 -557
  281. package/dist/hooks/__tests__/session.test.js.map +1 -1
  282. package/dist/hooks/agents-overlay.js +1 -1
  283. package/dist/hooks/agents-overlay.js.map +1 -1
  284. package/dist/hooks/extensibility/__tests__/dispatcher.test.js +108 -2
  285. package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
  286. package/dist/hooks/extensibility/__tests__/sdk.test.js +151 -1
  287. package/dist/hooks/extensibility/__tests__/sdk.test.js.map +1 -1
  288. package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
  289. package/dist/hooks/extensibility/dispatcher.js +20 -3
  290. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  291. package/dist/hooks/extensibility/plugin-runner.js +1 -0
  292. package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
  293. package/dist/hooks/extensibility/runtime.d.ts.map +1 -1
  294. package/dist/hooks/extensibility/runtime.js.map +1 -1
  295. package/dist/hooks/extensibility/sdk/paths.d.ts +1 -1
  296. package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -1
  297. package/dist/hooks/extensibility/sdk/paths.js +2 -2
  298. package/dist/hooks/extensibility/sdk/paths.js.map +1 -1
  299. package/dist/hooks/extensibility/sdk/runtime-state.d.ts +1 -1
  300. package/dist/hooks/extensibility/sdk/runtime-state.d.ts.map +1 -1
  301. package/dist/hooks/extensibility/sdk/runtime-state.js +55 -15
  302. package/dist/hooks/extensibility/sdk/runtime-state.js.map +1 -1
  303. package/dist/hooks/extensibility/sdk.d.ts +1 -0
  304. package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
  305. package/dist/hooks/extensibility/sdk.js +1 -1
  306. package/dist/hooks/extensibility/sdk.js.map +1 -1
  307. package/dist/hooks/extensibility/types.d.ts +7 -0
  308. package/dist/hooks/extensibility/types.d.ts.map +1 -1
  309. package/dist/hooks/keyword-detector.d.ts +10 -3
  310. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  311. package/dist/hooks/keyword-detector.js +116 -20
  312. package/dist/hooks/keyword-detector.js.map +1 -1
  313. package/dist/hooks/session.d.ts +180 -14
  314. package/dist/hooks/session.d.ts.map +1 -1
  315. package/dist/hooks/session.js +1402 -281
  316. package/dist/hooks/session.js.map +1 -1
  317. package/dist/hud/__tests__/hud-tmux-injection.test.js +6 -6
  318. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  319. package/dist/hud/__tests__/index.test.js +26 -16
  320. package/dist/hud/__tests__/index.test.js.map +1 -1
  321. package/dist/hud/__tests__/reconcile.test.js +83 -78
  322. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  323. package/dist/hud/__tests__/state.test.js +36 -31
  324. package/dist/hud/__tests__/state.test.js.map +1 -1
  325. package/dist/hud/__tests__/tmux.test.js +155 -198
  326. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  327. package/dist/hud/tmux.d.ts +42 -4
  328. package/dist/hud/tmux.d.ts.map +1 -1
  329. package/dist/hud/tmux.js +1017 -139
  330. package/dist/hud/tmux.js.map +1 -1
  331. package/dist/leader/__tests__/contract.test.js +71 -34
  332. package/dist/leader/__tests__/contract.test.js.map +1 -1
  333. package/dist/leader/contract.d.ts +1 -6
  334. package/dist/leader/contract.d.ts.map +1 -1
  335. package/dist/leader/contract.js +25 -24
  336. package/dist/leader/contract.js.map +1 -1
  337. package/dist/mcp/__tests__/hermes-bridge.test.js +17 -0
  338. package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
  339. package/dist/mcp/__tests__/server-lifecycle.test.js +4 -4
  340. package/dist/mcp/__tests__/state-paths.test.js +735 -4
  341. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  342. package/dist/mcp/__tests__/state-server.test.js +1 -1
  343. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  344. package/dist/mcp/__tests__/trace-server.test.js +2 -2
  345. package/dist/mcp/__tests__/trace-server.test.js.map +1 -1
  346. package/dist/mcp/hermes-bridge.d.ts.map +1 -1
  347. package/dist/mcp/hermes-bridge.js +2 -2
  348. package/dist/mcp/hermes-bridge.js.map +1 -1
  349. package/dist/mcp/state-paths.d.ts +78 -4
  350. package/dist/mcp/state-paths.d.ts.map +1 -1
  351. package/dist/mcp/state-paths.js +543 -40
  352. package/dist/mcp/state-paths.js.map +1 -1
  353. package/dist/mcp/state-server.d.ts +4 -4
  354. package/dist/mcp/state-server.d.ts.map +1 -1
  355. package/dist/modes/__tests__/base-autopilot-gates.test.js +36 -7
  356. package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
  357. package/dist/modes/__tests__/base-multi-state-compat.test.js +1 -1
  358. package/dist/modes/__tests__/base-multi-state-compat.test.js.map +1 -1
  359. package/dist/modes/__tests__/base-session-scope.test.js +17 -8
  360. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  361. package/dist/modes/base.d.ts +5 -5
  362. package/dist/modes/base.d.ts.map +1 -1
  363. package/dist/modes/base.js +76 -21
  364. package/dist/modes/base.js.map +1 -1
  365. package/dist/native-assets/archive.d.ts +17 -0
  366. package/dist/native-assets/archive.d.ts.map +1 -0
  367. package/dist/native-assets/archive.js +238 -0
  368. package/dist/native-assets/archive.js.map +1 -0
  369. package/dist/native-assets/policy.d.ts +42 -0
  370. package/dist/native-assets/policy.d.ts.map +1 -0
  371. package/dist/native-assets/policy.js +155 -0
  372. package/dist/native-assets/policy.js.map +1 -0
  373. package/dist/pipeline/__tests__/orchestrator.test.js +162 -40
  374. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  375. package/dist/pipeline/__tests__/stages.test.js +66 -149
  376. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  377. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  378. package/dist/pipeline/orchestrator.js +124 -18
  379. package/dist/pipeline/orchestrator.js.map +1 -1
  380. package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
  381. package/dist/pipeline/stages/ralplan.js +36 -8
  382. package/dist/pipeline/stages/ralplan.js.map +1 -1
  383. package/dist/ralplan/__tests__/consensus-gate.test.js +70 -1437
  384. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
  385. package/dist/ralplan/__tests__/documented-leader-preflight.test.js +328 -39
  386. package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -1
  387. package/dist/ralplan/__tests__/runtime.test.js +82 -102
  388. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  389. package/dist/ralplan/consensus-gate.d.ts +9 -1
  390. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  391. package/dist/ralplan/consensus-gate.js +40 -304
  392. package/dist/ralplan/consensus-gate.js.map +1 -1
  393. package/dist/ralplan/documented-leader-preflight.d.ts +15 -8
  394. package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -1
  395. package/dist/ralplan/documented-leader-preflight.js +356 -54
  396. package/dist/ralplan/documented-leader-preflight.js.map +1 -1
  397. package/dist/ralplan/runtime.d.ts +1 -1
  398. package/dist/ralplan/runtime.d.ts.map +1 -1
  399. package/dist/ralplan/runtime.js +68 -173
  400. package/dist/ralplan/runtime.js.map +1 -1
  401. package/dist/runtime/run-state.d.ts +5 -1
  402. package/dist/runtime/run-state.d.ts.map +1 -1
  403. package/dist/runtime/run-state.js +15 -8
  404. package/dist/runtime/run-state.js.map +1 -1
  405. package/dist/scripts/__tests__/codex-native-hook.test.js +14071 -8859
  406. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  407. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts +2 -0
  408. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts.map +1 -0
  409. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js +225 -0
  410. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js.map +1 -0
  411. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts +2 -0
  412. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts.map +1 -0
  413. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js +188 -0
  414. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js.map +1 -0
  415. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts +2 -0
  416. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts.map +1 -0
  417. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js +259 -0
  418. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js.map +1 -0
  419. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts +2 -0
  420. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts.map +1 -0
  421. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js +491 -0
  422. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js.map +1 -0
  423. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts +2 -0
  424. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts.map +1 -0
  425. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js +708 -0
  426. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js.map +1 -0
  427. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts +2 -0
  428. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts.map +1 -0
  429. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js +208 -0
  430. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js.map +1 -0
  431. package/dist/scripts/__tests__/notify-state-io.test.js +26 -0
  432. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  433. package/dist/scripts/__tests__/notify-tmux-injection.test.js +23 -1
  434. package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -1
  435. package/dist/scripts/__tests__/postinstall.test.js +19 -1
  436. package/dist/scripts/__tests__/postinstall.test.js.map +1 -1
  437. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +25 -8
  438. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
  439. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +60 -25
  440. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
  441. package/dist/scripts/__tests__/smoke-packed-install.test.js +155 -16
  442. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  443. package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts +2 -0
  444. package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts.map +1 -0
  445. package/dist/scripts/__tests__/verify-native-release-assets.test.js +235 -0
  446. package/dist/scripts/__tests__/verify-native-release-assets.test.js.map +1 -0
  447. package/dist/scripts/codex-native-hook.d.ts +50 -1
  448. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  449. package/dist/scripts/codex-native-hook.js +3559 -451
  450. package/dist/scripts/codex-native-hook.js.map +1 -1
  451. package/dist/scripts/codex-native-pre-post.d.ts +9 -0
  452. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  453. package/dist/scripts/codex-native-pre-post.js +35 -12
  454. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  455. package/dist/scripts/generate-native-release-manifest.js +80 -51
  456. package/dist/scripts/generate-native-release-manifest.js.map +1 -1
  457. package/dist/scripts/notify-fallback-watcher.js +28 -39
  458. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  459. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  460. package/dist/scripts/notify-hook/state-io.js +5 -0
  461. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  462. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  463. package/dist/scripts/notify-hook/team-leader-nudge.js +32 -3
  464. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  465. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  466. package/dist/scripts/notify-hook/team-worker-stop.js +23 -3
  467. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  468. package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
  469. package/dist/scripts/notify-hook/team-worker.js +45 -5
  470. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  471. package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -0
  472. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  473. package/dist/scripts/notify-hook/tmux-injection.js +4 -1
  474. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  475. package/dist/scripts/notify-hook.js +78 -82
  476. package/dist/scripts/notify-hook.js.map +1 -1
  477. package/dist/scripts/postinstall-advisory.d.ts +14 -0
  478. package/dist/scripts/postinstall-advisory.d.ts.map +1 -0
  479. package/dist/scripts/postinstall-advisory.js +49 -0
  480. package/dist/scripts/postinstall-advisory.js.map +1 -0
  481. package/dist/scripts/postinstall.d.ts +1 -1
  482. package/dist/scripts/postinstall.d.ts.map +1 -1
  483. package/dist/scripts/postinstall.js +1 -1
  484. package/dist/scripts/postinstall.js.map +1 -1
  485. package/dist/scripts/smoke-packed-install.d.ts +32 -1
  486. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  487. package/dist/scripts/smoke-packed-install.js +413 -60
  488. package/dist/scripts/smoke-packed-install.js.map +1 -1
  489. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  490. package/dist/scripts/sync-plugin-mirror.js +16 -1
  491. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  492. package/dist/scripts/verify-native-release-assets.js +37 -23
  493. package/dist/scripts/verify-native-release-assets.js.map +1 -1
  494. package/dist/state/__tests__/mode-state-context.test.js +12 -5
  495. package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
  496. package/dist/state/__tests__/operations.test.js +460 -850
  497. package/dist/state/__tests__/operations.test.js.map +1 -1
  498. package/dist/state/__tests__/skill-active.test.js +400 -2
  499. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  500. package/dist/state/mode-state-context.d.ts +3 -0
  501. package/dist/state/mode-state-context.d.ts.map +1 -1
  502. package/dist/state/mode-state-context.js +27 -13
  503. package/dist/state/mode-state-context.js.map +1 -1
  504. package/dist/state/operations.d.ts +3 -0
  505. package/dist/state/operations.d.ts.map +1 -1
  506. package/dist/state/operations.js +144 -40
  507. package/dist/state/operations.js.map +1 -1
  508. package/dist/state/skill-active.d.ts +18 -1
  509. package/dist/state/skill-active.d.ts.map +1 -1
  510. package/dist/state/skill-active.js +420 -23
  511. package/dist/state/skill-active.js.map +1 -1
  512. package/dist/state/workflow-transition-reconcile.d.ts +3 -0
  513. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  514. package/dist/state/workflow-transition-reconcile.js +10 -4
  515. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  516. package/dist/state/workflow-transition.d.ts.map +1 -1
  517. package/dist/state/workflow-transition.js +3 -1
  518. package/dist/state/workflow-transition.js.map +1 -1
  519. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +27 -0
  520. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
  521. package/dist/subagents/__tests__/tracker.test.js +837 -1048
  522. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  523. package/dist/subagents/tracker.d.ts +64 -60
  524. package/dist/subagents/tracker.d.ts.map +1 -1
  525. package/dist/subagents/tracker.js +809 -358
  526. package/dist/subagents/tracker.js.map +1 -1
  527. package/dist/team/__tests__/delivery-e2e-smoke.test.js +6 -7
  528. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  529. package/dist/team/__tests__/mcp-comm.test.js +104 -3
  530. package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
  531. package/dist/team/__tests__/notice-ledger.test.d.ts +2 -0
  532. package/dist/team/__tests__/notice-ledger.test.d.ts.map +1 -0
  533. package/dist/team/__tests__/notice-ledger.test.js +131 -0
  534. package/dist/team/__tests__/notice-ledger.test.js.map +1 -0
  535. package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts +2 -0
  536. package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts.map +1 -0
  537. package/dist/team/__tests__/pane-teardown-residue-e2e.test.js +410 -0
  538. package/dist/team/__tests__/pane-teardown-residue-e2e.test.js.map +1 -0
  539. package/dist/team/__tests__/phase-controller.test.js +20 -0
  540. package/dist/team/__tests__/phase-controller.test.js.map +1 -1
  541. package/dist/team/__tests__/runtime.test.js +725 -59
  542. package/dist/team/__tests__/runtime.test.js.map +1 -1
  543. package/dist/team/__tests__/scaling.test.js +222 -237
  544. package/dist/team/__tests__/scaling.test.js.map +1 -1
  545. package/dist/team/__tests__/state-root.test.js +117 -2
  546. package/dist/team/__tests__/state-root.test.js.map +1 -1
  547. package/dist/team/__tests__/tmux-session.test.js +887 -92
  548. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  549. package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts +2 -0
  550. package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts.map +1 -0
  551. package/dist/team/__tests__/tmux-source-authority-realtmux.test.js +149 -0
  552. package/dist/team/__tests__/tmux-source-authority-realtmux.test.js.map +1 -0
  553. package/dist/team/__tests__/tmux-test-fixture.d.ts +69 -0
  554. package/dist/team/__tests__/tmux-test-fixture.d.ts.map +1 -1
  555. package/dist/team/__tests__/tmux-test-fixture.js +242 -15
  556. package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
  557. package/dist/team/__tests__/tmux-test-fixture.test.js +177 -1
  558. package/dist/team/__tests__/tmux-test-fixture.test.js.map +1 -1
  559. package/dist/team/__tests__/worker-runtime-identity.test.js +45 -72
  560. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  561. package/dist/team/notice-ledger.d.ts +54 -0
  562. package/dist/team/notice-ledger.d.ts.map +1 -0
  563. package/dist/team/notice-ledger.js +259 -0
  564. package/dist/team/notice-ledger.js.map +1 -0
  565. package/dist/team/phase-controller.d.ts.map +1 -1
  566. package/dist/team/phase-controller.js +2 -0
  567. package/dist/team/phase-controller.js.map +1 -1
  568. package/dist/team/runtime.d.ts +3 -1
  569. package/dist/team/runtime.d.ts.map +1 -1
  570. package/dist/team/runtime.js +314 -142
  571. package/dist/team/runtime.js.map +1 -1
  572. package/dist/team/scaling.d.ts.map +1 -1
  573. package/dist/team/scaling.js +161 -91
  574. package/dist/team/scaling.js.map +1 -1
  575. package/dist/team/state/monitor.d.ts +10 -0
  576. package/dist/team/state/monitor.d.ts.map +1 -1
  577. package/dist/team/state/monitor.js +13 -0
  578. package/dist/team/state/monitor.js.map +1 -1
  579. package/dist/team/state/types.d.ts +15 -0
  580. package/dist/team/state/types.d.ts.map +1 -1
  581. package/dist/team/state/types.js.map +1 -1
  582. package/dist/team/state-root.d.ts +12 -0
  583. package/dist/team/state-root.d.ts.map +1 -1
  584. package/dist/team/state-root.js +177 -0
  585. package/dist/team/state-root.js.map +1 -1
  586. package/dist/team/state.d.ts +18 -0
  587. package/dist/team/state.d.ts.map +1 -1
  588. package/dist/team/state.js +36 -0
  589. package/dist/team/state.js.map +1 -1
  590. package/dist/team/tmux-session.d.ts +40 -4
  591. package/dist/team/tmux-session.d.ts.map +1 -1
  592. package/dist/team/tmux-session.js +513 -166
  593. package/dist/team/tmux-session.js.map +1 -1
  594. package/dist/ultragoal/__tests__/artifacts.test.js +950 -2
  595. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  596. package/dist/ultragoal/__tests__/docs-contract.test.js +1 -0
  597. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  598. package/dist/ultragoal/artifacts.d.ts +35 -1
  599. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  600. package/dist/ultragoal/artifacts.js +232 -46
  601. package/dist/ultragoal/artifacts.js.map +1 -1
  602. package/dist/utils/__tests__/file-durability.test.js +58 -1
  603. package/dist/utils/__tests__/file-durability.test.js.map +1 -1
  604. package/dist/utils/__tests__/paths.test.js +21 -1
  605. package/dist/utils/__tests__/paths.test.js.map +1 -1
  606. package/dist/utils/file-durability.d.ts +10 -0
  607. package/dist/utils/file-durability.d.ts.map +1 -1
  608. package/dist/utils/file-durability.js +34 -4
  609. package/dist/utils/file-durability.js.map +1 -1
  610. package/dist/utils/paths.d.ts.map +1 -1
  611. package/dist/utils/paths.js +14 -6
  612. package/dist/utils/paths.js.map +1 -1
  613. package/dist/verification/__tests__/ci-rust-gates.test.js +49 -4
  614. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  615. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +60 -0
  616. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  617. package/dist/verification/__tests__/native-release-manifest.test.js +157 -4
  618. package/dist/verification/__tests__/native-release-manifest.test.js.map +1 -1
  619. package/package.json +13 -5
  620. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  621. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +81 -17
  622. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +13 -5
  623. package/plugins/oh-my-codex/skills/cancel/SKILL.md +30 -85
  624. package/plugins/oh-my-codex/skills/ralph/SKILL.md +5 -5
  625. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +15 -17
  626. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +3 -2
  627. package/skills/autopilot/SKILL.md +13 -5
  628. package/skills/cancel/SKILL.md +30 -85
  629. package/skills/ralph/SKILL.md +5 -5
  630. package/skills/ralplan/SKILL.md +15 -17
  631. package/skills/ultragoal/SKILL.md +3 -2
  632. package/src/scripts/__tests__/codex-native-hook.test.ts +6091 -540
  633. package/src/scripts/__tests__/issue-3293-callsite-parity.test.ts +187 -0
  634. package/src/scripts/__tests__/issue-3293-cancel-transaction.test.ts +189 -0
  635. package/src/scripts/__tests__/issue-3293-hook-owned-cancel.test.ts +187 -0
  636. package/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts +591 -0
  637. package/src/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.ts +701 -0
  638. package/src/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.ts +211 -0
  639. package/src/scripts/__tests__/notify-state-io.test.ts +29 -0
  640. package/src/scripts/__tests__/notify-tmux-injection.test.ts +26 -1
  641. package/src/scripts/__tests__/postinstall.test.ts +23 -1
  642. package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +27 -8
  643. package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +61 -27
  644. package/src/scripts/__tests__/smoke-packed-install.test.ts +209 -18
  645. package/src/scripts/__tests__/verify-native-release-assets.test.ts +240 -0
  646. package/src/scripts/codex-native-hook.ts +3638 -475
  647. package/src/scripts/codex-native-pre-post.ts +43 -12
  648. package/src/scripts/generate-native-release-manifest.ts +83 -79
  649. package/src/scripts/notify-fallback-watcher.ts +38 -38
  650. package/src/scripts/notify-hook/state-io.ts +11 -0
  651. package/src/scripts/notify-hook/team-leader-nudge.ts +32 -4
  652. package/src/scripts/notify-hook/team-worker-stop.ts +23 -4
  653. package/src/scripts/notify-hook/team-worker.ts +43 -5
  654. package/src/scripts/notify-hook/tmux-injection.ts +5 -1
  655. package/src/scripts/notify-hook.ts +83 -82
  656. package/src/scripts/postinstall-advisory.ts +67 -0
  657. package/src/scripts/postinstall.ts +1 -1
  658. package/src/scripts/smoke-packed-install.ts +446 -59
  659. package/src/scripts/sync-plugin-mirror.ts +17 -1
  660. package/src/scripts/verify-native-release-assets.ts +39 -31
  661. package/templates/AGENTS.md +1 -0
package/dist/cli/index.js CHANGED
@@ -3,9 +3,9 @@
3
3
  * Multi-agent orchestration for OpenAI Codex CLI
4
4
  */
5
5
  import { execFileSync, spawn } from "child_process";
6
- import { basename, dirname, join, posix, resolve, win32 } from "path";
7
- import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "fs";
8
- import { copyFile, cp, lstat, mkdir, readFile, readdir, rm, stat, symlink, utimes, writeFile } from "fs/promises";
6
+ import { basename, dirname, isAbsolute, join, posix, relative, resolve, win32 } from "path";
7
+ import { appendFileSync, chmodSync, closeSync, constants as fsConstants, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "fs";
8
+ import { copyFile, cp, lstat, mkdir, open, readFile, readdir, rm, stat, symlink, utimes, writeFile } from "fs/promises";
9
9
  import { constants as osConstants, homedir } from "os";
10
10
  import { createHash, randomUUID } from "crypto";
11
11
  import { setup, SETUP_MCP_MODES, SETUP_SCOPES, SETUP_TEAM_MODES, } from "./setup.js";
@@ -41,7 +41,7 @@ import { authCommand } from "./auth.js";
41
41
  import { missionCommand } from "./mission.js";
42
42
  import { resolveCodexGlobalOptionValue, runAuthHotswap, } from "../auth/hotswap.js";
43
43
  import { MADMAX_FLAG, CODEX_BYPASS_FLAG, HIGH_REASONING_FLAG, XHIGH_REASONING_FLAG, SPARK_FLAG, MADMAX_SPARK_FLAG, CONFIG_FLAG, LONG_CONFIG_FLAG, } from "./constants.js";
44
- import { getBaseStateDir, getBaseStateDirWithSource, getStateDir, listModeStateFilesWithScopePreference, resolveWritableStateScope, } from "../mcp/state-paths.js";
44
+ import { getBaseStateDir, getBaseStateDirWithSource, getStateDir, isModeStateFilename, listModeStateFilesWithScopePreference, resolveWritableStateScope, } from "../mcp/state-paths.js";
45
45
  import { evaluateRalphCompletionAuditEvidence, isRalphCompletePhase } from "../ralph/completion-audit.js";
46
46
  import { normalizeTerminalWorkflowState } from "../state/terminal-normalization.js";
47
47
  import { readPersistedSetupPreferences, resolveCodexConfigPathForLaunch, resolveCodexHomeForLaunch, resolveProjectLocalCodexHomeForLaunch, } from "./codex-home.js";
@@ -50,14 +50,14 @@ import { discoverOmxPluginCacheDirs, hasLocalOmxPluginEnablement, materializePac
50
50
  import { escapeTomlString, readTopLevelTomlString, upsertTopLevelTomlString } from "../utils/toml.js";
51
51
  import { ROOT_REASONING_EFFORTS, isUnsupportedRootReasoningEffort, normalizeUnsupportedRootReasoningEffort, } from "../config/models.js";
52
52
  export { readPersistedSetupPreferences, readPersistedSetupScope, resolveCodexConfigPathForLaunch, resolveCodexHomeForLaunch, resolveProjectLocalCodexHomeForLaunch, } from "./codex-home.js";
53
- import { SKILL_ACTIVE_STATE_MODE, extractSessionIdFromInitializedStatePath, getSkillActiveStatePathsForStateDir, listActiveSkills, readSkillActiveState, syncCanonicalSkillStateForMode, } from "../state/skill-active.js";
53
+ import { SKILL_ACTIVE_STATE_MODE, extractSessionIdFromInitializedStatePath, listActiveSkills, syncCanonicalSkillStateForMode, updateRootSkillActiveStateForStateDir, } from "../state/skill-active.js";
54
54
  import { isTrackedWorkflowMode } from "../state/workflow-transition.js";
55
55
  import { maybeCheckAndPromptUpdate, runImmediateUpdate } from "./update.js";
56
56
  import { maybePromptGithubStar } from "./star-prompt.js";
57
57
  import { generateOverlay, removeSessionModelInstructionsFile, resolveSessionOrchestrationMode, sessionModelInstructionsPath, writeSessionModelInstructionsFile, } from "../hooks/agents-overlay.js";
58
- import { isSessionPointerLaunchAbort, normalizeSessionId, readSessionState, writeSessionStart, writeSessionEnd, resetSessionMetrics, } from "../hooks/session.js";
58
+ import { closeLaunchSessionBindingOnce, establishLaunchSessionBinding, readNativeSessionOwnerEvidence, readSessionPointer, readSessionState, resolveSessionPointerContext, finalizeBoundOnce, isSessionPointerLaunchAbort, normalizeSessionId, resetSessionMetrics, } from "../hooks/session.js";
59
59
  import { probeActualTmuxInstanceEvidence, tmuxEvidenceBindsCandidate } from "../scripts/notify-hook/managed-tmux.js";
60
- import { buildClientAttachedReconcileHookName, buildReconcileHudResizeArgs, buildRegisterClientAttachedReconcileArgs, buildRegisterResizeHookArgs, buildResizeHookName, buildResizeHookTarget, buildScheduleDelayedHudResizeArgs, buildUnregisterClientAttachedReconcileArgs, buildUnregisterResizeHookArgs, enableMouseScrolling, isMsysOrGitBash, isNativeWindows, isTmuxAvailable, mitigateCopyModeUnderlineArtifacts, } from "../team/tmux-session.js";
60
+ import { buildClientAttachedReconcileHookName, buildReconcileHudResizeArgs, buildRegisterClientAttachedReconcileArgs, buildRegisterResizeHookArgs, buildResizeHookName, buildResizeHookTarget, buildScheduleDelayedHudResizeArgs, buildUnregisterClientAttachedReconcileArgs, buildUnregisterResizeHookArgs, enableMouseScrolling, isMsysOrGitBash, isNativeWindows, isTmuxAvailable, } from "../team/tmux-session.js";
61
61
  import { getPackageRoot } from "../utils/package.js";
62
62
  import { codexConfigPath, omxRoot, rememberOmxLaunchContext, resolveOmxCliEntryPath } from "../utils/paths.js";
63
63
  import { cleanCodexModelAvailabilityNuxIfNeeded, extractSharedMcpRegistryServersFromConfig, repairConfigIfNeeded, repairProjectScopeTrustStateForLaunch, syncProjectScopeTrustStateFromRuntime } from "../config/generator.js";
@@ -73,6 +73,7 @@ import { buildHookEvent } from "../hooks/extensibility/events.js";
73
73
  import { dispatchHookEvent } from "../hooks/extensibility/dispatcher.js";
74
74
  import { collectInheritableTeamWorkerArgs as collectInheritableTeamWorkerArgsShared, parseTeamWorkerLaunchArgs, resolveTeamWorkerLaunchArgs, resolveTeamLowComplexityDefaultModel, serializeTeamWorkerLaunchArgs, TEAM_WORKER_INHERITED_MODEL_ENV, } from "../team/model-contract.js";
75
75
  import { parseWorktreeMode, planWorktreeTarget, ensureWorktree, } from "../team/worktree.js";
76
+ import { resolveVerifiedDetachedTeamContext } from "../team/state-root.js";
76
77
  import { ensureReusableNodeModules } from "../utils/repo-deps.js";
77
78
  import { resolveWorktreeToolContext, worktreeToolContextEnv } from "../utils/worktree-tool-context.js";
78
79
  import { OMX_NOTIFY_TEMP_CONTRACT_ENV, parseNotifyTempContractFromArgs, serializeNotifyTempContract, } from "../notifications/temp-contract.js";
@@ -134,7 +135,8 @@ Usage:
134
135
  Alias for agents-init (lightweight AGENTS bootstrap only)
135
136
  omx team Spawn parallel worker panes in tmux and bootstrap inbox/task state
136
137
  omx ralph Launch Codex with ralph persistence mode active
137
- omx ralplan Record validated role intents for adapted native subagent spawns
138
+ omx ralplan Adapted Ralplan authority support; preflight applies only when native role routing is unavailable
139
+ and adapted Ralplan authority is requested; ordinary work remains under its own workflow gates
138
140
  omx ultragoal Create, resume, and checkpoint durable multi-goal plans over Codex goal mode
139
141
  omx performance-goal
140
142
  Create, hand off, and gate evaluator-backed performance goals
@@ -262,7 +264,6 @@ const ALLOWED_SHELLS = new Set([
262
264
  "/opt/local/bin/zsh",
263
265
  "/opt/homebrew/bin/zsh",
264
266
  ]);
265
- const WINDOWS_DETACHED_BOOTSTRAP_DELAY_MS = 2500;
266
267
  const CODEX_VERSION_FLAGS = new Set(["--version", "-V"]);
267
268
  const TMUX_EXTENDED_KEYS_MODE = "always";
268
269
  const TMUX_EXTENDED_KEYS_FALLBACK_MODE = "off";
@@ -270,6 +271,7 @@ const TMUX_EXTENDED_KEYS_LEASE_DIR = "tmux-extended-keys";
270
271
  const TMUX_EXTENDED_KEYS_LOCK_RETRY_MS = 20;
271
272
  const TMUX_EXTENDED_KEYS_LOCK_MAX_ATTEMPTS = 100;
272
273
  const TMUX_EXTENDED_KEYS_LOCK_STALE_MS = 30_000;
274
+ export const DETACHED_LEADER_READY_TIMEOUT_MS = 120_000;
273
275
  const RESUME_HELP = `Usage: omx resume [--project] [--codex-home <path>] [codex resume options]
274
276
 
275
277
  Read-only help does not prepare or launch a Codex session.`;
@@ -520,6 +522,9 @@ export function resolveCliInvocation(args) {
520
522
  if (firstArg === "resume") {
521
523
  return { command: "resume", launchArgs: args.slice(1) };
522
524
  }
525
+ if (firstArg === "__detached-session-leader") {
526
+ return { command: firstArg, launchArgs: args.slice(1) };
527
+ }
523
528
  return { command: firstArg, launchArgs: [] };
524
529
  }
525
530
  export function resolveUpdateChannelArg(args) {
@@ -1042,6 +1047,228 @@ function execTmuxFileSync(args, options) {
1042
1047
  ...(process.platform === "win32" ? { windowsHide: true } : {}),
1043
1048
  });
1044
1049
  }
1050
+ function captureDetachedLeaderAuthority(leaderPaneId, expectedSessionName, ownerId) {
1051
+ if (!/^%[0-9]+$/.test(leaderPaneId) || !ownerId)
1052
+ throw new Error("invalid detached leader authority target");
1053
+ const output = execTmuxFileSync([
1054
+ "display-message", "-p", "-t", leaderPaneId,
1055
+ "#{session_name}\t#{session_id}\t#{session_created}\t#{window_index}\t#{window_id}\t#{pane_id}\t#{pane_pid}",
1056
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1057
+ const fields = output.split("\t");
1058
+ if (fields.length !== 7)
1059
+ throw new Error("malformed detached leader authority");
1060
+ const [sessionName, sessionId, sessionCreated, windowIndex, windowId, paneId, panePid] = fields;
1061
+ if (sessionName !== expectedSessionName || paneId !== leaderPaneId || !/^\$[0-9]+$/.test(sessionId)
1062
+ || !/^[0-9]+$/.test(sessionCreated) || !/^[0-9]+$/.test(windowIndex)
1063
+ || !/^@[0-9]+$/.test(windowId) || !/^[1-9][0-9]*$/.test(panePid)) {
1064
+ throw new Error("malformed detached leader authority");
1065
+ }
1066
+ const parsedPid = Number(panePid);
1067
+ if (!Number.isSafeInteger(parsedPid) || parsedPid <= 0)
1068
+ throw new Error("malformed detached leader authority");
1069
+ return { paneId, panePid: parsedPid, sessionName, sessionId, sessionCreated, windowId, windowIndex, ownerId };
1070
+ }
1071
+ /** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
1072
+ export function parseDetachedLeaderPaneIdByPid(snapshot, leaderPid) {
1073
+ if (!Number.isSafeInteger(leaderPid) || leaderPid <= 0)
1074
+ throw new Error("invalid detached leader pid");
1075
+ const matches = snapshot
1076
+ .split("\n")
1077
+ .map((line) => line.trim())
1078
+ .filter(Boolean)
1079
+ .map((line) => line.split("\t"))
1080
+ .filter(([paneId, paneDead, panePid]) => /^%[0-9]+$/.test(paneId ?? "") && paneDead === "0" && Number(panePid) === leaderPid)
1081
+ .map(([paneId]) => paneId);
1082
+ if (matches.length !== 1)
1083
+ throw new Error("detached leader pane identity is unavailable");
1084
+ return matches[0];
1085
+ }
1086
+ function detachedLeaderAuthorityCondition(authority, requireOwner = true) {
1087
+ const conditions = [
1088
+ "#{==:#{pane_dead},0}",
1089
+ `#{==:#{pane_id},${authority.paneId}}`,
1090
+ `#{==:#{pane_pid},${authority.panePid}}`,
1091
+ `#{==:#{session_id},${authority.sessionId}}`,
1092
+ `#{==:#{session_created},${authority.sessionCreated}}`,
1093
+ `#{==:#{window_id},${authority.windowId}}`,
1094
+ ...(requireOwner ? [`#{==:#{@omx_instance_id},${authority.ownerId}}`] : []),
1095
+ ];
1096
+ return conditions.reduce((combined, condition) => `#{&&:${combined},${condition}}`);
1097
+ }
1098
+ function detachedHudAuthorityCondition(authority, requireMarker = true) {
1099
+ const conditions = [
1100
+ "#{==:#{pane_dead},0}",
1101
+ `#{==:#{pane_id},${authority.paneId}}`,
1102
+ `#{==:#{pane_pid},${authority.panePid}}`,
1103
+ `#{==:#{session_id},${authority.sessionId}}`,
1104
+ `#{==:#{window_id},${authority.windowId}}`,
1105
+ ...(requireMarker ? [`#{m:*OMX_DETACHED_HUD_OPERATION=${authority.operationMarker}*,#{pane_start_command}}`] : []),
1106
+ ];
1107
+ return conditions.reduce((combined, condition) => `#{&&:${combined},${condition}}`);
1108
+ }
1109
+ function detachedAuthorityReceipt() {
1110
+ return `omx_detached_${randomUUID()}`;
1111
+ }
1112
+ function runDetachedLeaderMutation(authority, args, requireOwner = true) {
1113
+ const receipt = detachedAuthorityReceipt();
1114
+ const success = `${args.map(quoteShellArg).join(" ")} ; display-message -p ${quoteShellArg(receipt)}`;
1115
+ const output = execTmuxFileSync([
1116
+ "if-shell", "-F", "-t", authority.paneId, detachedLeaderAuthorityCondition(authority, requireOwner),
1117
+ success, "display-message -p ''",
1118
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1119
+ if (output !== receipt)
1120
+ throw new Error("detached leader authority changed before tmux mutation");
1121
+ }
1122
+ function detachedPreReportCleanupCondition(authority) {
1123
+ const conditions = [
1124
+ "#{==:#{pane_dead},1}",
1125
+ `#{==:#{pane_id},${authority.paneId}}`,
1126
+ `#{==:#{session_name},${authority.sessionName}}`,
1127
+ `#{==:#{session_id},${authority.sessionId}}`,
1128
+ `#{==:#{session_created},${authority.sessionCreated}}`,
1129
+ `#{==:#{window_id},${authority.windowId}}`,
1130
+ ];
1131
+ return conditions.reduce((combined, condition) => `#{&&:${combined},${condition}}`);
1132
+ }
1133
+ export function cleanupDetachedPreReportSession(authority) {
1134
+ const receipt = detachedAuthorityReceipt();
1135
+ const success = `kill-session -t ${quoteShellArg(authority.sessionName)} ; display-message -p ${quoteShellArg(receipt)}`;
1136
+ const output = execTmuxFileSync([
1137
+ "if-shell", "-F", "-t", authority.paneId, detachedPreReportCleanupCondition(authority),
1138
+ success, "display-message -p ''",
1139
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1140
+ if (output !== receipt)
1141
+ throw new Error("detached pre-report topology changed before cleanup");
1142
+ }
1143
+ function runDetachedHudMutation(leaderAuthority, hudAuthority, args, requireMarker = true) {
1144
+ const receipt = detachedAuthorityReceipt();
1145
+ const success = `${args.map(quoteShellArg).join(" ")} ; display-message -p ${quoteShellArg(receipt)}`;
1146
+ const hudGuard = `if-shell -F -t ${quoteShellArg(hudAuthority.paneId)} ${quoteShellArg(detachedHudAuthorityCondition(hudAuthority, requireMarker))} ${quoteShellArg(success)} ${quoteShellArg("display-message -p ''")}`;
1147
+ const output = execTmuxFileSync([
1148
+ "if-shell", "-F", "-t", leaderAuthority.paneId, detachedLeaderAuthorityCondition(leaderAuthority),
1149
+ hudGuard, "display-message -p ''",
1150
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1151
+ if (output !== receipt)
1152
+ throw new Error("detached leader or HUD authority changed before tmux mutation");
1153
+ }
1154
+ export function cleanupDetachedHudPane(authority, ownerId) {
1155
+ if (ownerId) {
1156
+ execTmuxFileSync(["kill-pane", "-t", authority.paneId], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
1157
+ }
1158
+ else {
1159
+ execTmuxFileSync([
1160
+ "if-shell", "-F", "-t", authority.paneId, detachedHudAuthorityCondition(authority, false),
1161
+ `kill-pane -t ${quoteShellArg(authority.paneId)}`, "",
1162
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
1163
+ }
1164
+ for (let attempt = 0; attempt < 50; attempt += 1) {
1165
+ const panes = execTmuxFileSync(["list-panes", "-a", "-F", "#{pane_id}"], {
1166
+ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"],
1167
+ }).split("\n").map((value) => value.trim()).filter(Boolean);
1168
+ if (!panes.includes(authority.paneId))
1169
+ return;
1170
+ blockMs(20);
1171
+ }
1172
+ throw new Error("detached HUD topology changed before teardown");
1173
+ }
1174
+ function tagDetachedHudPane(leaderAuthority, authority, ownerId) {
1175
+ runDetachedHudMutation(leaderAuthority, authority, [
1176
+ "set-option", "-pq", "-t", authority.paneId, "@omx_hud_owner", ownerId,
1177
+ ], false);
1178
+ }
1179
+ export function discoverDetachedHudAuthority(sessionName, ownerId) {
1180
+ const rows = execTmuxFileSync([
1181
+ "list-panes", "-a", "-F",
1182
+ "#{pane_id}\t#{pane_dead}\t#{pane_pid}\t#{session_name}\t#{session_id}\t#{session_created}\t#{window_id}\t#{@omx_hud_owner}",
1183
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
1184
+ const matches = rows.split("\n").map((line) => line.trim()).filter(Boolean).flatMap((line) => {
1185
+ const [paneId, paneDead, panePidRaw, discoveredSessionName, sessionId, sessionCreated, windowId, owner] = line.split("\t");
1186
+ const panePid = Number(panePidRaw);
1187
+ if (paneDead !== "0" || discoveredSessionName !== sessionName || owner !== ownerId || !/^%\d+$/.test(paneId ?? "") || !/^\$\d+$/.test(sessionId ?? "") || !/^\d+$/.test(sessionCreated ?? "") || !/^@\d+$/.test(windowId ?? "") || !Number.isSafeInteger(panePid) || panePid <= 0)
1188
+ return [];
1189
+ return [{ paneId: paneId, panePid, sessionId: sessionId, sessionCreated: sessionCreated, windowId: windowId, operationMarker: randomUUID() }];
1190
+ });
1191
+ if (matches.length > 1)
1192
+ throw new Error("multiple detached HUD ownership tags found");
1193
+ return matches[0];
1194
+ }
1195
+ function buildDeferredDetachedHudGuard(leaderAuthority, hudAuthority, command) {
1196
+ const deferredReceipt = detachedAuthorityReceipt();
1197
+ let guardedSinkCount = 0;
1198
+ const guardedResize = (_match, sink) => {
1199
+ guardedSinkCount += 1;
1200
+ // The stored hook payload is consumed by one outer tmux format pass when
1201
+ // run-shell executes it. The injected authority conditions must survive that
1202
+ // pass so they are evaluated at execution time against the guarded panes;
1203
+ // unescaped, tmux pre-expands them against the hook's own pane and collapses
1204
+ // them to constants, denying the resize before the sink is parsed (#3292).
1205
+ const outerFormatEscaped = (condition) => condition.replaceAll("#{", "##{");
1206
+ const success = `${sink} ; display-message -p ${quoteShellArg(deferredReceipt)}`;
1207
+ const hudGuard = `if-shell -F -t ${quoteShellArg(hudAuthority.paneId)} ${quoteShellArg(outerFormatEscaped(detachedHudAuthorityCondition(hudAuthority)))} ${quoteShellArg(success)} ${quoteShellArg("")}`;
1208
+ return `tmux if-shell -F -t ${quoteShellArg(leaderAuthority.paneId)} ${quoteShellArg(outerFormatEscaped(detachedLeaderAuthorityCondition(leaderAuthority)))} ${quoteShellArg(hudGuard)} ${quoteShellArg("")}`;
1209
+ };
1210
+ const outerFormatEscapedCommand = command
1211
+ .replaceAll('#{pane_id}', '##{pane_id}')
1212
+ .replaceAll('#{pane_dead}', '##{pane_dead}')
1213
+ .replaceAll('#{pane_pid}', '##{pane_pid}');
1214
+ const guardedCommand = outerFormatEscapedCommand.replace(/tmux (resize-pane -t %[0-9]+ -y [1-9][0-9]*)/g, guardedResize);
1215
+ if (guardedSinkCount === 0) {
1216
+ throw new Error("detached deferred HUD mutation lacks a recognized resize sink");
1217
+ }
1218
+ return guardedCommand;
1219
+ }
1220
+ export function guardDetachedHudDeferredMutation(leaderAuthority, hudAuthority, args) {
1221
+ if (args[0] === "set-hook" && typeof args.at(-1) === "string") {
1222
+ const hookCommand = args.at(-1);
1223
+ const runShellPrefix = "run-shell -b ";
1224
+ if (!hookCommand.startsWith(runShellPrefix)) {
1225
+ throw new Error("detached deferred HUD hook lacks a run-shell command");
1226
+ }
1227
+ const quotedPayload = hookCommand.slice(runShellPrefix.length);
1228
+ if (!quotedPayload.startsWith("'") || !quotedPayload.endsWith("'")) {
1229
+ throw new Error("detached deferred HUD hook has an invalid run-shell payload");
1230
+ }
1231
+ const payload = quotedPayload
1232
+ .slice(1, -1)
1233
+ .replaceAll(`'"'"'`, "'")
1234
+ .replaceAll("'\\''", "'");
1235
+ const guardedPayload = buildDeferredDetachedHudGuard(leaderAuthority, hudAuthority, payload);
1236
+ return [...args.slice(0, -1), `${runShellPrefix}${quoteShellArg(guardedPayload)}`];
1237
+ }
1238
+ if (args[0] === "run-shell") {
1239
+ const commandIndex = args.length - 1;
1240
+ return [...args.slice(0, commandIndex), buildDeferredDetachedHudGuard(leaderAuthority, hudAuthority, args[commandIndex])];
1241
+ }
1242
+ return args;
1243
+ }
1244
+ function runDetachedLeaderSplit(authority, args) {
1245
+ const receipt = detachedAuthorityReceipt();
1246
+ const operationMarker = randomUUID();
1247
+ const splitArgs = [...args];
1248
+ const targetIndex = splitArgs.indexOf("-t");
1249
+ if (targetIndex < 0 || splitArgs[targetIndex + 1] !== authority.sessionName)
1250
+ throw new Error("invalid detached HUD split target");
1251
+ splitArgs[targetIndex + 1] = authority.paneId;
1252
+ const formatIndex = splitArgs.indexOf("-F");
1253
+ if (formatIndex < 0 || splitArgs[formatIndex + 1] !== "#{pane_id}")
1254
+ throw new Error("invalid detached HUD split receipt format");
1255
+ const commandIndex = splitArgs.length - 1;
1256
+ splitArgs[commandIndex] = `env OMX_DETACHED_HUD_OPERATION=${operationMarker} ${splitArgs[commandIndex]}`;
1257
+ splitArgs[formatIndex + 1] = `#{pane_id}\t#{pane_pid}\t#{session_id}\t#{window_id}\t${receipt}`;
1258
+ const output = execTmuxFileSync([
1259
+ "if-shell", "-F", "-t", authority.paneId, detachedLeaderAuthorityCondition(authority),
1260
+ splitArgs.map(quoteShellArg).join(" "), "display-message -p ''",
1261
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1262
+ const [paneId, panePid, sessionId, windowId, observedReceipt, ...extra] = output.split("\t");
1263
+ if (!/^%[0-9]+$/.test(paneId) || !/^[1-9][0-9]*$/.test(panePid) || !/^\$[0-9]+$/.test(sessionId)
1264
+ || !/^@[0-9]+$/.test(windowId) || observedReceipt !== receipt || extra.length !== 0) {
1265
+ throw new Error("detached leader authority changed before HUD split");
1266
+ }
1267
+ const parsedPid = Number(panePid);
1268
+ if (!Number.isSafeInteger(parsedPid) || parsedPid <= 0)
1269
+ throw new Error("malformed detached HUD authority");
1270
+ return { paneId, panePid: parsedPid, sessionId, windowId, operationMarker };
1271
+ }
1045
1272
  function readTmuxEnvValueForTarget(targetPaneId) {
1046
1273
  if (!targetPaneId.startsWith("%"))
1047
1274
  return undefined;
@@ -1129,29 +1356,6 @@ function clearDetachedTmuxSessionHistoryIfUnattached(sessionName, leaderPaneId)
1129
1356
  logCliOperationFailure(err);
1130
1357
  }
1131
1358
  }
1132
- function readTmuxSessionInstanceId(sessionName) {
1133
- try {
1134
- return execTmuxFileSync(["show-options", "-qv", "-t", sessionName, OMX_INSTANCE_OPTION], {
1135
- stdio: ["ignore", "pipe", "ignore"],
1136
- encoding: "utf-8",
1137
- }).trim();
1138
- }
1139
- catch {
1140
- return null;
1141
- }
1142
- }
1143
- function tmuxPaneBelongsToSession(paneId, sessionName) {
1144
- try {
1145
- const paneSessionName = execTmuxFileSync(["display-message", "-p", "-t", paneId, "#{session_name}"], {
1146
- stdio: ["ignore", "pipe", "ignore"],
1147
- encoding: "utf-8",
1148
- }).trim();
1149
- return paneSessionName === sessionName;
1150
- }
1151
- catch {
1152
- return false;
1153
- }
1154
- }
1155
1359
  function buildDetachedHistoryPruneHookCommand(leaderPaneId) {
1156
1360
  // The leader pane can be gone by the time the hook fires (e.g. crashed
1157
1361
  // leader with a lingering session); suppress errors so tmux does not queue
@@ -1193,6 +1397,12 @@ function logCliOperationFailure(error) {
1193
1397
  process.stderr.write(`[cli/index] operation failed: ${error}
1194
1398
  `);
1195
1399
  }
1400
+ function logEstablishmentCleanupEvidence(error) {
1401
+ const cleanup = error?.establishmentCleanup;
1402
+ if (!cleanup)
1403
+ return;
1404
+ process.stderr.write(`[omx] establishment cleanup evidence: ${JSON.stringify(cleanup)}\n`);
1405
+ }
1196
1406
  function tmuxFailureMessage(error) {
1197
1407
  if (!error || typeof error !== "object")
1198
1408
  return String(error);
@@ -1207,77 +1417,18 @@ function isUnsupportedTmuxExtendedKeysFailure(error) {
1207
1417
  return (message.includes("extended-keys") &&
1208
1418
  /(?:invalid|unknown|unsupported) (?:option|flag|argument)|no such option|unknown option/.test(message));
1209
1419
  }
1210
- function isBenignMissingTmuxServerMessage(message) {
1211
- return (/no server running/i.test(message) ||
1212
- /error connecting to .*\(No such file or directory\)/i.test(message));
1213
- }
1214
- export function checkDetachedTmuxLaunchHealth() {
1215
- try {
1216
- execTmuxFileSync(["list-sessions"], {
1217
- stdio: ["ignore", "pipe", "pipe"],
1218
- encoding: "utf-8",
1219
- });
1220
- return { usable: true };
1221
- }
1222
- catch (err) {
1223
- const reason = tmuxFailureMessage(err);
1224
- if (isBenignMissingTmuxServerMessage(reason)) {
1225
- return { usable: true };
1226
- }
1227
- return { usable: false, reason };
1228
- }
1229
- }
1230
- function warnDetachedTmuxFallback(reason) {
1231
- const suffix = reason ? ` (${reason})` : "";
1232
- console.warn(`[omx] warning: tmux is installed but its server/socket is unusable${suffix}. Falling back to direct Codex launch.`);
1233
- }
1234
- const QUICK_ATTACH_NOOP_THRESHOLD_MS = 2_000;
1235
- function isWslWindowsTerminalEnvironment(env) {
1236
- return Boolean(env.WT_SESSION?.trim() &&
1237
- (env.WSL_INTEROP?.trim() ||
1238
- env.WSL_DISTRO_NAME?.trim() ||
1239
- env.WSLENV?.trim()));
1240
- }
1241
- function readDetachedSessionAttachedClientCount(sessionName) {
1242
- try {
1243
- const output = execTmuxFileSync(["display-message", "-p", "-t", sessionName, "#{session_attached}"], {
1244
- stdio: ["ignore", "pipe", "pipe"],
1245
- encoding: "utf-8",
1246
- }).trim();
1247
- const parsed = Number.parseInt(output, 10);
1248
- return Number.isFinite(parsed) ? parsed : null;
1249
- }
1250
- catch (err) {
1251
- logCliOperationFailure(err);
1252
- return null;
1420
+ function probeDetachedTmuxTransport() {
1421
+ const { result } = spawnPlatformCommandSync("tmux", ["-V"], {
1422
+ encoding: "utf-8",
1423
+ stdio: ["ignore", "pipe", "pipe"],
1424
+ });
1425
+ if (result.error || result.status !== 0) {
1426
+ throw new DetachedTransportUnavailable();
1253
1427
  }
1254
1428
  }
1255
- function assertDetachedAttachDidNotNoop(sessionName, elapsedMs, env) {
1256
- if (!isWslWindowsTerminalEnvironment(env))
1257
- return;
1258
- if (elapsedMs >= QUICK_ATTACH_NOOP_THRESHOLD_MS)
1259
- return;
1260
- const attachedClients = readDetachedSessionAttachedClientCount(sessionName);
1261
- if (attachedClients === null || attachedClients > 0)
1262
- return;
1263
- throw new Error([
1264
- "tmux attach-session returned immediately without attaching a client",
1265
- `(session=${sessionName}).`,
1266
- "This can happen on WSL2 under Windows Terminal.",
1267
- "Falling back to direct Codex launch.",
1268
- ].join(" "));
1269
- }
1270
1429
  function resolveTmuxAwareLaunchPolicy(explicitLaunchPolicy, nativeWindows) {
1271
- const launchPolicy = resolveCodexLaunchPolicy(process.env, process.platform, undefined, nativeWindows, undefined, undefined, explicitLaunchPolicy);
1272
- if (launchPolicy !== "detached-tmux") {
1273
- return { launchPolicy, effectiveExplicitLaunchPolicy: explicitLaunchPolicy };
1274
- }
1275
- const tmuxHealth = checkDetachedTmuxLaunchHealth();
1276
- if (tmuxHealth.usable) {
1277
- return { launchPolicy, effectiveExplicitLaunchPolicy: explicitLaunchPolicy };
1278
- }
1279
- warnDetachedTmuxFallback(tmuxHealth.reason);
1280
- return { launchPolicy: "direct", effectiveExplicitLaunchPolicy: "direct" };
1430
+ const launchPolicy = resolveCodexLaunchPolicy(process.env, process.platform, Boolean(resolveTmuxBinaryForPlatform()), nativeWindows, undefined, undefined, explicitLaunchPolicy);
1431
+ return { launchPolicy, effectiveExplicitLaunchPolicy: explicitLaunchPolicy };
1281
1432
  }
1282
1433
  export function resolveSignalExitCode(signal) {
1283
1434
  if (!signal)
@@ -1507,15 +1658,6 @@ export function resolveOmxRootForLaunch(cwd, env = process.env) {
1507
1658
  function resolveLaunchPath(cwd, raw) {
1508
1659
  return isCrossPlatformAbsolutePath(raw) ? raw : join(cwd, raw);
1509
1660
  }
1510
- function resolveHudRuntimeRootSource(omxRootOverride, env = process.env) {
1511
- if (env.OMX_TEAM_STATE_ROOT?.trim())
1512
- return 'team-env';
1513
- if (env.OMX_ROOT?.trim() || omxRootOverride)
1514
- return 'omx-root-env';
1515
- if (env.OMX_STATE_ROOT?.trim())
1516
- return 'omx-state-root-env';
1517
- return 'cwd-default';
1518
- }
1519
1661
  export function resolveHudRuntimeRootForLaunch(cwd, env = process.env) {
1520
1662
  const omxTeamStateRoot = env.OMX_TEAM_STATE_ROOT?.trim();
1521
1663
  if (omxTeamStateRoot) {
@@ -1574,6 +1716,8 @@ export function captureMadmaxWorktreeRuntimeContext(options) {
1574
1716
  const inheritedRoot = resolveInheritedMadmaxRoot(env);
1575
1717
  if (!inheritedRoot)
1576
1718
  return undefined;
1719
+ if (!madmaxInheritedContextMatchesLaunch(options.sourceCwd, options.originalLaunchArgs, env))
1720
+ return undefined;
1577
1721
  const sourceCwd = env.OMX_SOURCE_CWD?.trim() || options.sourceCwd;
1578
1722
  const worktreeCwd = options.worktreeCwd?.trim();
1579
1723
  const omxStateRoot = env.OMX_STATE_ROOT?.trim();
@@ -1761,31 +1905,77 @@ function readMadmaxDetachedActiveRecord(recordPath) {
1761
1905
  tmux_session_name: parsed.tmux_session_name,
1762
1906
  ...(typeof parsed.session_id === "string" ? { session_id: parsed.session_id } : {}),
1763
1907
  ...(typeof parsed.tmux_pane_id === "string" ? { tmux_pane_id: parsed.tmux_pane_id } : {}),
1908
+ ...(typeof parsed.tmux_internal_session_id === "string" ? { tmux_internal_session_id: parsed.tmux_internal_session_id } : {}),
1909
+ ...(typeof parsed.tmux_session_created === "string" ? { tmux_session_created: parsed.tmux_session_created } : {}),
1910
+ ...(typeof parsed.tmux_pane_pid === "number" ? { tmux_pane_pid: parsed.tmux_pane_pid } : {}),
1764
1911
  ...(typeof parsed.worktree_cwd === "string" ? { worktree_cwd: parsed.worktree_cwd } : {}),
1912
+ ...(typeof parsed.launch_nonce === "string" ? { launch_nonce: parsed.launch_nonce } : {}),
1913
+ ...(typeof parsed.leader_pid === "number" && Number.isSafeInteger(parsed.leader_pid) && parsed.leader_pid > 0 ? { leader_pid: parsed.leader_pid } : {}),
1914
+ ...(typeof parsed.base_state_root === "string" ? { base_state_root: parsed.base_state_root } : {}),
1915
+ ...(parsed.lifecycle_phase === "ready" || parsed.lifecycle_phase === "terminal" ? { lifecycle_phase: parsed.lifecycle_phase } : {}),
1765
1916
  };
1766
1917
  }
1767
1918
  catch {
1768
1919
  return null;
1769
1920
  }
1770
1921
  }
1922
+ function queryMadmaxDetachedRuntimeBinding(record) {
1923
+ if (!record.tmux_pane_id || !record.session_id)
1924
+ return null;
1925
+ try {
1926
+ const output = execTmuxFileSync([
1927
+ "list-panes", "-a", "-F",
1928
+ "#{pane_id}\t#{pane_dead}\t#{pane_pid}\t#{session_name}\t#{session_id}\t#{session_created}\t#{@omx_instance_id}",
1929
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
1930
+ let binding = null;
1931
+ const seen = new Set();
1932
+ for (const line of output.split("\n").filter(Boolean)) {
1933
+ const fields = line.split("\t");
1934
+ if (fields.length !== 7)
1935
+ return null;
1936
+ const [paneId, dead, panePidRaw, sessionName, internalSessionId, sessionCreated, omxInstanceId] = fields;
1937
+ const panePid = Number(panePidRaw);
1938
+ if (!/^%[0-9]+$/.test(paneId) || seen.has(paneId) || (dead !== "0" && dead !== "1")
1939
+ || !Number.isSafeInteger(panePid) || panePid <= 0 || !sessionName
1940
+ || !/^\$[0-9]+$/.test(internalSessionId) || !/^[0-9]+$/.test(sessionCreated))
1941
+ return null;
1942
+ seen.add(paneId);
1943
+ if (paneId !== record.tmux_pane_id)
1944
+ continue;
1945
+ if (dead !== "0" || sessionName !== record.tmux_session_name || omxInstanceId !== record.session_id)
1946
+ return null;
1947
+ binding = { paneId, panePid, sessionName, internalSessionId, sessionCreated, omxInstanceId };
1948
+ }
1949
+ return binding;
1950
+ }
1951
+ catch {
1952
+ return null;
1953
+ }
1954
+ }
1955
+ function hasMatchingMadmaxDetachedRuntimeBinding(record) {
1956
+ const binding = queryMadmaxDetachedRuntimeBinding(record);
1957
+ return Boolean(binding
1958
+ && binding.panePid === record.tmux_pane_pid
1959
+ && binding.internalSessionId === record.tmux_internal_session_id
1960
+ && binding.sessionCreated === record.tmux_session_created);
1961
+ }
1771
1962
  function isReusableMadmaxDetachedActiveRecord(record) {
1772
- if (!detachedTmuxSessionExists(record.tmux_session_name))
1773
- return false;
1774
- if (!record.session_id || !record.tmux_pane_id)
1963
+ if (record.lifecycle_phase !== "ready" || !record.leader_pid || !record.base_state_root || !record.launch_nonce)
1775
1964
  return false;
1776
- if (readTmuxSessionInstanceId(record.tmux_session_name) !== record.session_id) {
1965
+ try {
1966
+ process.kill(record.leader_pid, 0);
1967
+ }
1968
+ catch {
1777
1969
  return false;
1778
1970
  }
1779
- return tmuxPaneBelongsToSession(record.tmux_pane_id, record.tmux_session_name);
1780
- }
1781
- function detachedTmuxSessionExists(sessionName) {
1782
1971
  try {
1783
- execTmuxFileSync(["has-session", "-t", sessionName], { stdio: "ignore" });
1784
- return true;
1972
+ if (realpathSync(record.base_state_root) !== record.base_state_root)
1973
+ return false;
1785
1974
  }
1786
1975
  catch {
1787
1976
  return false;
1788
1977
  }
1978
+ return hasMatchingMadmaxDetachedRuntimeBinding(record);
1789
1979
  }
1790
1980
  function readMadmaxDetachedLockOwner(lockPath) {
1791
1981
  try {
@@ -1854,7 +2044,7 @@ function inspectMadmaxDetachedContextLock(lockPath) {
1854
2044
  diagnostic: `lock has no readable owner pid yet; lock age ${Math.round(ageMs)}ms`,
1855
2045
  };
1856
2046
  }
1857
- export function withMadmaxDetachedContextLock(runsRoot, contextKey, run, options = {}) {
2047
+ export async function withMadmaxDetachedContextLock(runsRoot, contextKey, run, options = {}) {
1858
2048
  const lockPath = join(runsRoot, MADMAX_DETACHED_ACTIVE_DIR, `${contextKey}.lock`);
1859
2049
  const maxAttempts = options.maxAttempts ?? MADMAX_DETACHED_LOCK_MAX_ATTEMPTS;
1860
2050
  const retryMs = options.retryMs ?? MADMAX_DETACHED_LOCK_RETRY_MS;
@@ -1872,7 +2062,7 @@ export function withMadmaxDetachedContextLock(runsRoot, contextKey, run, options
1872
2062
  };
1873
2063
  writeFileSync(join(lockPath, "owner.json"), `${JSON.stringify(owner, null, 2)}\n`, { mode: 0o600 });
1874
2064
  writeFileSync(join(lockPath, "pid"), String(process.pid));
1875
- return run();
2065
+ return await run();
1876
2066
  }
1877
2067
  finally {
1878
2068
  rmSync(lockPath, { recursive: true, force: true });
@@ -1900,6 +2090,7 @@ function readMadmaxRunMetadata(runRoot) {
1900
2090
  const parsed = JSON.parse(readFileSync(join(runRoot, ".omxbox-run.json"), "utf-8"));
1901
2091
  return {
1902
2092
  ...(typeof parsed.cwd === "string" ? { cwd: parsed.cwd } : {}),
2093
+ ...(typeof parsed.source_cwd === "string" ? { source_cwd: parsed.source_cwd } : {}),
1903
2094
  ...(typeof parsed.detached_launch_context === "string"
1904
2095
  ? { detached_launch_context: parsed.detached_launch_context }
1905
2096
  : {}),
@@ -1927,13 +2118,27 @@ function madmaxInheritedContextMatchesLaunch(cwd, launchArgs, env) {
1927
2118
  return false;
1928
2119
  if (metadata.cwd && metadata.cwd !== inheritedRoot)
1929
2120
  return false;
2121
+ const sourceCwd = env.OMX_SOURCE_CWD?.trim();
2122
+ if (!sourceCwd || !metadata.source_cwd || metadata.source_cwd !== sourceCwd)
2123
+ return false;
1930
2124
  if (metadata.detached_launch_context !== context)
1931
2125
  return false;
1932
2126
  const expectedContext = buildMadmaxDetachedLaunchContextKey(cwd, [...launchArgs], inheritedRoot);
1933
2127
  return expectedContext === context;
1934
2128
  }
1935
- function isMadmaxDetachedGuardEnabled(env) {
1936
- return env.OMXBOX_ACTIVE === "1" && typeof env[OMX_MADMAX_DETACHED_CONTEXT_ENV] === "string";
2129
+ function resolveVerifiedMadmaxDetachedContext(cwd, launchArgs, env) {
2130
+ if (!madmaxInheritedContextMatchesLaunch(cwd, launchArgs, env))
2131
+ return undefined;
2132
+ const inheritedRoot = resolveInheritedMadmaxRoot(env);
2133
+ const context = env[OMX_MADMAX_DETACHED_CONTEXT_ENV]?.trim();
2134
+ if (!inheritedRoot || !context)
2135
+ return undefined;
2136
+ return {
2137
+ root: resolveLaunchPath(cwd, inheritedRoot),
2138
+ sourceCwd: env.OMX_SOURCE_CWD?.trim() || cwd,
2139
+ context,
2140
+ runsRoot: resolveMadmaxRunsRoot(env),
2141
+ };
1937
2142
  }
1938
2143
  function cleanupCurrentMadmaxReuseRunRoot(env, runsRoot) {
1939
2144
  const runRoot = env.OMX_ROOT;
@@ -1945,15 +2150,236 @@ function cleanupCurrentMadmaxReuseRunRoot(env, runsRoot) {
1945
2150
  rmSync(runRoot, { recursive: true, force: true });
1946
2151
  }
1947
2152
  function writeMadmaxDetachedActiveRecord(recordPath, record) {
1948
- mkdirSync(dirname(recordPath), { recursive: true });
1949
- writeFileSync(recordPath, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
2153
+ const bytes = `${JSON.stringify(record, null, 2)}\n`;
2154
+ const tempPath = `${recordPath}.${process.pid}.${randomUUID()}.tmp`;
2155
+ let fd;
2156
+ mkdirSync(dirname(recordPath), { recursive: true, mode: 0o700 });
2157
+ try {
2158
+ fd = openSync(tempPath, "wx", 0o600);
2159
+ writeFileSync(fd, bytes, "utf-8");
2160
+ try {
2161
+ fsyncSync(fd);
2162
+ }
2163
+ catch (error) {
2164
+ if (!(process.platform === "win32" && error.code === "EPERM"))
2165
+ throw error;
2166
+ }
2167
+ closeSync(fd);
2168
+ fd = undefined;
2169
+ renameSync(tempPath, recordPath);
2170
+ const directoryFd = openSync(dirname(recordPath), "r");
2171
+ try {
2172
+ try {
2173
+ fsyncSync(directoryFd);
2174
+ }
2175
+ catch (error) {
2176
+ if (!(process.platform === "win32" && error.code === "EPERM"))
2177
+ throw error;
2178
+ }
2179
+ }
2180
+ finally {
2181
+ closeSync(directoryFd);
2182
+ }
2183
+ return {
2184
+ bytes,
2185
+ digest: createHash("sha256").update(bytes).digest("hex"),
2186
+ nonce: record.launch_nonce ?? "",
2187
+ };
2188
+ }
2189
+ finally {
2190
+ if (fd !== undefined)
2191
+ closeSync(fd);
2192
+ try {
2193
+ unlinkSync(tempPath);
2194
+ }
2195
+ catch { }
2196
+ }
1950
2197
  }
1951
- class MadmaxDetachedReuseError extends Error {
1952
- failClosed = true;
2198
+ function detachedFailureCode(error) {
2199
+ const code = error && typeof error === "object" && "code" in error
2200
+ ? error.code
2201
+ : undefined;
2202
+ return typeof code === "string" && /^(?:EACCES|EAGAIN|EBUSY|ECANCELED|ECONNREFUSED|EEXIST|EINTR|EINVAL|EIO|EISDIR|EMFILE|ENFILE|ENOENT|ENOSPC|ENOTDIR|EPERM|EPIPE|EROFS|ESRCH|ETIMEDOUT)$/.test(code) ? code : "unknown";
2203
+ }
2204
+ export function describeDetachedLeaderFailure(error) {
2205
+ const describe = (value, depth) => {
2206
+ if (depth > 4)
2207
+ return "nested failure";
2208
+ if (value instanceof AggregateError) {
2209
+ return [value.message, ...[...value.errors].map((child) => describe(child, depth + 1))]
2210
+ .filter(Boolean)
2211
+ .join(": ");
2212
+ }
2213
+ if (value instanceof Error)
2214
+ return value.message;
2215
+ return String(value);
2216
+ };
2217
+ return describe(error, 0).replace(/[\r\n\t]+/g, " ").slice(0, 1_024);
2218
+ }
2219
+ export class DetachedLaunchSafetyError extends Error {
2220
+ phase;
2221
+ cause;
2222
+ report;
2223
+ name = "DetachedLaunchSafetyError";
2224
+ constructor(phase, cause, report) {
2225
+ super(`detached launch safety failure during ${phase}${phase === "completion" && cause instanceof Error ? `: ${cause.message}` : ""}`);
2226
+ this.phase = phase;
2227
+ this.cause = cause;
2228
+ this.report = report;
2229
+ }
2230
+ }
2231
+ export class DetachedTransportUnavailable extends Error {
2232
+ reason;
2233
+ name = "DetachedTransportUnavailable";
2234
+ noSideEffects = true;
2235
+ constructor(reason) {
2236
+ super("detached transport is unavailable before launch side effects");
2237
+ this.reason = reason;
2238
+ }
2239
+ }
2240
+ /** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
2241
+ export async function executeDetachedLaunchStateMachine(input, deps) {
2242
+ const report = input.preflight.report;
2243
+ const transition = (name) => { report.transitions.push(name); };
2244
+ let binding;
2245
+ let ownedRecord;
2246
+ let released = false;
2247
+ let retainedAuthority = false;
2248
+ let rollbackAuthorized = false;
2249
+ try {
2250
+ transition("D1");
2251
+ binding = await deps.establish();
2252
+ retainedAuthority = true;
2253
+ transition("D2");
2254
+ const completion = await deps.complete(binding);
2255
+ if (completion.kind === "failure") {
2256
+ const failures = [completion.error];
2257
+ try {
2258
+ report.rollback.attempted.push("setup-finalization");
2259
+ await deps.finalizeSetupFailure(binding);
2260
+ }
2261
+ catch (finalizationError) {
2262
+ report.rollback.failures.push({ step: "setup-finalization", status: "failed", code: detachedFailureCode(finalizationError) });
2263
+ failures.push(finalizationError);
2264
+ }
2265
+ throw new DetachedLaunchSafetyError("completion", new AggregateError(failures, `preLaunch ${completion.operation} failed: ${describeDetachedLeaderFailure(completion.error)}`), report);
2266
+ }
2267
+ transition("D3");
2268
+ const inertSession = await deps.createInertSession();
2269
+ transition("D4");
2270
+ const pane = await deps.capturePane(inertSession);
2271
+ transition("D5");
2272
+ if (await deps.updateNameMetadata(binding, inertSession) !== "committed-released") {
2273
+ throw new DetachedLaunchSafetyError("name-metadata", new Error("detached name metadata did not release"), report);
2274
+ }
2275
+ transition("D6");
2276
+ if (await deps.updatePaneMetadata(binding, pane) !== "committed-released") {
2277
+ throw new DetachedLaunchSafetyError("pane-metadata", new Error("detached pane metadata did not release"), report);
2278
+ }
2279
+ transition("D7");
2280
+ ownedRecord = await deps.publishActiveRecord(inertSession, pane);
2281
+ transition("D9");
2282
+ try {
2283
+ await deps.releaseBarrier();
2284
+ released = true;
2285
+ }
2286
+ catch (releaseError) {
2287
+ // A failed publication must not let the outer launcher destroy the
2288
+ // leader's finalization capability. Request a nonce-bound abort and
2289
+ // preserve all artifacts on timeout, malformed, or foreign reports.
2290
+ let resolution;
2291
+ try {
2292
+ resolution = await deps.abortAndAwaitFinalization?.(releaseError);
2293
+ }
2294
+ catch (abortError) {
2295
+ released = true;
2296
+ throw new AggregateError([releaseError, abortError], "detached release and abort publication both failed");
2297
+ }
2298
+ rollbackAuthorized = resolution?.acknowledged === true && Boolean(resolution.nonce && resolution.sessionId && resolution.sessionName &&
2299
+ Number.isSafeInteger(resolution.leaderPid) && resolution.kind);
2300
+ if (!rollbackAuthorized)
2301
+ released = true;
2302
+ throw releaseError;
2303
+ }
2304
+ transition("D10");
2305
+ await deps.attachOrReturn(inertSession, input.preflight.shouldAttach);
2306
+ return { kind: input.preflight.shouldAttach ? "attached" : "returned", released: true, report };
2307
+ }
2308
+ catch (error) {
2309
+ deps.diagnostic?.(error);
2310
+ if (retainedAuthority && !rollbackAuthorized && !released) {
2311
+ try {
2312
+ const resolution = await deps.abortAndAwaitFinalization?.(error);
2313
+ rollbackAuthorized = resolution?.rollbackAuthorized === true || (resolution?.acknowledged === true && Boolean(resolution.nonce && resolution.sessionId && resolution.sessionName &&
2314
+ Number.isSafeInteger(resolution.leaderPid) && resolution.kind));
2315
+ if (!rollbackAuthorized)
2316
+ released = true;
2317
+ }
2318
+ catch (abortError) {
2319
+ released = true;
2320
+ deps.diagnostic?.(abortError);
2321
+ }
2322
+ }
2323
+ if (!released) {
2324
+ try {
2325
+ report.rollback.attempted.push("rollback");
2326
+ await deps.rollback(ownedRecord, report);
2327
+ }
2328
+ catch (rollbackError) {
2329
+ report.rollback.failures.push({ step: "rollback", status: "failed", code: detachedFailureCode(rollbackError) });
2330
+ deps.diagnostic?.(rollbackError);
2331
+ }
2332
+ }
2333
+ if (error instanceof DetachedLaunchSafetyError) {
2334
+ throw new DetachedLaunchSafetyError(error.phase, error.cause, report);
2335
+ }
2336
+ const phase = report.transitions.at(-1);
2337
+ const phaseName = phase === "D1" ? "establishment"
2338
+ : phase === "D2" ? "completion"
2339
+ : phase === "D3" ? "inert-session"
2340
+ : phase === "D4" ? "pane-id"
2341
+ : phase === "D5" ? "name-metadata"
2342
+ : phase === "D6" ? "pane-metadata"
2343
+ : phase === "D7" ? "active-record"
2344
+ : phase === "D9" ? "barrier-release"
2345
+ : "post-release-attach";
2346
+ throw new DetachedLaunchSafetyError(phaseName, error, report);
2347
+ }
1953
2348
  }
1954
2349
  class MadmaxDetachedGuardError extends Error {
1955
2350
  failClosed = true;
1956
2351
  }
2352
+ function preflightDetachedLaunch() {
2353
+ // D0 must remain transport-only: `tmux -V` neither connects to nor creates a server.
2354
+ probeDetachedTmuxTransport();
2355
+ const report = { transitions: ["D0"], rollback: { attempted: [], failures: [] } };
2356
+ return Object.freeze({
2357
+ kind: "available",
2358
+ shouldAttach: shouldAttachDetachedTmuxSession(process.env),
2359
+ report,
2360
+ });
2361
+ }
2362
+ function handleMadmaxDetachedReuse(runtimeContext, preflight) {
2363
+ const contextKey = runtimeContext?.madmaxDetachedContext ?? process.env[OMX_MADMAX_DETACHED_CONTEXT_ENV]?.trim();
2364
+ const activeRecordPath = contextKey ? madmaxDetachedActiveRecordPath(resolveMadmaxRunsRoot(process.env), contextKey) : null;
2365
+ const activeRecord = activeRecordPath ? readMadmaxDetachedActiveRecord(activeRecordPath) : null;
2366
+ if (!activeRecord || activeRecord.context_key !== contextKey || !isReusableMadmaxDetachedActiveRecord(activeRecord))
2367
+ return false;
2368
+ cleanupCurrentMadmaxReuseRunRoot(process.env, resolveMadmaxRunsRoot(process.env));
2369
+ setDetachedTmuxSessionHistoryLimit(activeRecord.tmux_session_name, activeRecord.tmux_pane_id);
2370
+ if (!preflight.shouldAttach) {
2371
+ clearDetachedTmuxSessionHistoryIfUnattached(activeRecord.tmux_session_name, activeRecord.tmux_pane_id);
2372
+ return true;
2373
+ }
2374
+ process.stderr.write(`[omx] madmax detached launch already active for this context; attaching ${activeRecord.tmux_session_name} instead of starting a duplicate.\n`);
2375
+ try {
2376
+ execTmuxFileSync(["attach-session", "-t", activeRecord.tmux_session_name], { stdio: "inherit" });
2377
+ }
2378
+ catch (error) {
2379
+ throw new DetachedLaunchSafetyError("post-release-attach", error, preflight.report);
2380
+ }
2381
+ return true;
2382
+ }
1957
2383
  export function createMadmaxIsolatedRoot(sourceCwd, argv, env = process.env) {
1958
2384
  const runsRoot = resolveMadmaxRunsRoot(env);
1959
2385
  mkdirSync(runsRoot, { recursive: true });
@@ -1984,6 +2410,23 @@ function activateMadmaxIsolationIfNeeded(command, launchArgs, cwd, env = process
1984
2410
  env.OMX_SOURCE_CWD = cwd;
1985
2411
  process.stderr.write(`[omx] madmax isolated state: ${runDir} (source: ${cwd})\n`);
1986
2412
  }
2413
+ function renderDetachedLaunchSafetyReport(error) {
2414
+ const establishmentCleanup = error.report.establishmentCleanup?.capability.map((entry) => ({
2415
+ role: entry.role,
2416
+ phase: entry.phase,
2417
+ status: entry.status,
2418
+ ...(entry.error ? { failureCode: detachedFailureCode(entry.error) } : {}),
2419
+ }));
2420
+ return JSON.stringify({
2421
+ phase: error.phase,
2422
+ transitions: error.report.transitions,
2423
+ ...(establishmentCleanup ? { establishmentCleanup } : {}),
2424
+ rollback: {
2425
+ attempted: error.report.rollback.attempted,
2426
+ failures: error.report.rollback.failures.map(({ step, status, code }) => ({ step, status, code })),
2427
+ },
2428
+ });
2429
+ }
1987
2430
  export async function main(args) {
1988
2431
  const knownCommands = new Set([
1989
2432
  "launch",
@@ -1992,6 +2435,7 @@ export async function main(args) {
1992
2435
  "imagegen",
1993
2436
  "capabilities",
1994
2437
  "setup",
2438
+ "__detached-session-leader",
1995
2439
  "update",
1996
2440
  "list",
1997
2441
  "agents",
@@ -2043,6 +2487,9 @@ export async function main(args) {
2043
2487
  console.log(HELP);
2044
2488
  return;
2045
2489
  }
2490
+ if (command !== "launch" && command !== "resume") {
2491
+ activateMadmaxIsolationIfNeeded(command, launchArgs, process.cwd(), process.env);
2492
+ }
2046
2493
  try {
2047
2494
  if (command === "session") {
2048
2495
  await sessionCommand(args.slice(1));
@@ -2052,8 +2499,13 @@ export async function main(args) {
2052
2499
  console.log(RESUME_HELP);
2053
2500
  return;
2054
2501
  }
2055
- activateMadmaxIsolationIfNeeded(command, launchArgs, process.cwd(), process.env);
2056
2502
  switch (command) {
2503
+ case "__detached-session-leader": {
2504
+ const payload = decodeDetachedLeaderPayload(launchArgs[0]);
2505
+ await runDetachedSessionLeader(payload);
2506
+ process.exit(typeof process.exitCode === "number" ? process.exitCode : 0);
2507
+ break;
2508
+ }
2057
2509
  case "launch":
2058
2510
  if (launchArgsRequestAuthHotswap(launchArgs)) {
2059
2511
  await launchWithAuthHotswap(launchArgs);
@@ -2249,7 +2701,13 @@ export async function main(args) {
2249
2701
  }
2250
2702
  }
2251
2703
  catch (err) {
2252
- console.error(`Error: ${err instanceof Error ? err.message : err}`);
2704
+ if (err instanceof DetachedLaunchSafetyError) {
2705
+ console.error(`Error: ${err.message}`);
2706
+ console.error(`[omx] detached launch safety report: ${renderDetachedLaunchSafetyReport(err)}`);
2707
+ }
2708
+ else {
2709
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
2710
+ }
2253
2711
  process.exit(1);
2254
2712
  }
2255
2713
  }
@@ -2480,38 +2938,29 @@ export async function launchWithAuthHotswap(args) {
2480
2938
  process.exitCode = status;
2481
2939
  }
2482
2940
  export async function launchWithHud(args) {
2483
- if (isNativeWindows()) {
2484
- const { result } = spawnPlatformCommandSync("tmux", ["-V"], {
2485
- encoding: "utf-8",
2486
- stdio: ["pipe", "pipe", "pipe"],
2487
- });
2488
- if (result.error) {
2489
- const errno = result.error;
2490
- const kind = classifySpawnError(errno);
2491
- if (kind === "missing") {
2492
- console.warn("[omx] warning: tmux was not found on native Windows. Continuing without tmux/HUD.\n" +
2493
- "[omx] To enable tmux-backed features, install psmux:\n" +
2494
- "[omx] winget install psmux\n" +
2495
- "[omx] See: https://github.com/marlocarlo/psmux");
2496
- }
2497
- else {
2498
- console.warn(`[omx] warning: tmux probe failed on native Windows (${errno.code || errno.message}). Continuing without tmux/HUD.`);
2499
- }
2500
- }
2501
- else if (result.status !== 0 && !isTmuxAvailable()) {
2502
- const stderr = (result.stderr || "").trim();
2503
- console.warn(`[omx] warning: tmux reported an error on native Windows${stderr ? ` (${stderr})` : ""}. Continuing without tmux/HUD.`);
2504
- }
2505
- }
2506
2941
  const launchCwd = process.cwd();
2507
2942
  const { omxArgs, suffix } = splitOmxArgsAtEndOfOptions(args);
2508
2943
  const parsedWorktree = parseWorktreeMode(omxArgs);
2509
2944
  const notifyTempResult = resolveNotifyTempContract(parsedWorktree.remainingArgs, process.env);
2510
2945
  const passthroughArgs = [...notifyTempResult.passthroughArgs, ...suffix];
2511
2946
  const explicitLaunchPolicy = resolveEffectiveLeaderLaunchPolicyOverride(passthroughArgs, process.env);
2512
- const persistentCodexHomeForLaunch = resolveCodexHomeForLaunch(launchCwd, process.env);
2513
- const { launchPolicy, effectiveExplicitLaunchPolicy } = resolveTmuxAwareLaunchPolicy(explicitLaunchPolicy, isNativeWindows());
2947
+ let { launchPolicy } = resolveTmuxAwareLaunchPolicy(explicitLaunchPolicy, isNativeWindows());
2948
+ let detachedPreflight;
2949
+ if (launchPolicy === "detached-tmux") {
2950
+ try {
2951
+ detachedPreflight = preflightDetachedLaunch();
2952
+ }
2953
+ catch (error) {
2954
+ if (!(error instanceof DetachedTransportUnavailable))
2955
+ throw error;
2956
+ console.warn("[omx] warning: detached tmux transport is unavailable. Falling back to direct Codex launch.");
2957
+ launchPolicy = "direct";
2958
+ }
2959
+ }
2514
2960
  const enableNotifyFallbackAuthority = launchPolicy === "direct";
2961
+ activateMadmaxIsolationIfNeeded("launch", args, launchCwd, process.env);
2962
+ const verifiedMadmaxLaunchContext = resolveVerifiedMadmaxDetachedContext(launchCwd, args, process.env);
2963
+ const persistentCodexHomeForLaunch = resolveCodexHomeForLaunch(launchCwd, process.env);
2515
2964
  const workerSparkModel = resolveWorkerSparkModel(passthroughArgs, persistentCodexHomeForLaunch);
2516
2965
  let normalizedArgs = normalizeCodexLaunchArgs(passthroughArgs);
2517
2966
  let cwd = launchCwd;
@@ -2551,6 +3000,8 @@ export async function launchWithHud(args) {
2551
3000
  clearInheritedMadmaxRootForDisposableWorktreeLaunch(parsedWorktree.remainingArgs);
2552
3001
  applyDisposableWorktreeOmxRootForLaunch(ensuredLaunchWorktree);
2553
3002
  applyWorktreeToolContextForLaunch(cwd, ensuredLaunchWorktree);
3003
+ if (launchPolicy === "detached-tmux" && detachedPreflight && handleMadmaxDetachedReuse(madmaxWorktreeRuntimeContext, detachedPreflight))
3004
+ return;
2554
3005
  const sessionId = `omx-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2555
3006
  try {
2556
3007
  await maybeCheckAndPromptUpdate(cwd);
@@ -2592,37 +3043,77 @@ export async function launchWithHud(args) {
2592
3043
  const codexHomeOverride = preparedCodexHome.codexHomeOverride;
2593
3044
  const sqliteHomeOverride = preparedCodexHome.sqliteHomeOverride;
2594
3045
  const projectLocalCodexHomeForCleanup = preparedCodexHome.projectLocalCodexHomeForCleanup;
2595
- // ── Phase 1: preLaunch ──────────────────────────────────────────────────
2596
- try {
2597
- await preLaunch(cwd, sessionId, notifyTempResult.contract, codexHomeOverride, enableNotifyFallbackAuthority, worktreeDirty);
3046
+ let insideTmuxControlPlane;
3047
+ if (launchPolicy === "inside-tmux") {
3048
+ let verifiedTeamContext;
3049
+ try {
3050
+ verifiedTeamContext = await resolveVerifiedDetachedTeamContext(cwd, process.env);
3051
+ }
3052
+ catch {
3053
+ verifiedTeamContext = undefined;
3054
+ }
3055
+ insideTmuxControlPlane = buildDetachedLaunchControlPlane({
3056
+ cwd,
3057
+ sessionId,
3058
+ env: process.env,
3059
+ omxRootOverride: resolveOmxRootForLaunch(cwd, process.env),
3060
+ runtimeContext: madmaxWorktreeRuntimeContext,
3061
+ verifiedMadmaxContext: verifiedMadmaxLaunchContext,
3062
+ leaderPaneId: process.env.TMUX_PANE,
3063
+ verifiedTeamContext,
3064
+ });
2598
3065
  }
2599
- catch (err) {
2600
- if (isSessionPointerLaunchAbort(err)) {
2601
- console.error(`[omx] session pointer launch aborted: ${err.code}`);
2602
- reportOrdinaryLaunchRootConflict(err, cwd, cwd === launchCwd && !parsedWorktree.mode.enabled && !isResumeCodexLaunch(normalizedArgs));
3066
+ const restoreInsideTmuxControlPlane = insideTmuxControlPlane
3067
+ ? applyLaunchOwnedControlPlane(insideTmuxControlPlane)
3068
+ : undefined;
3069
+ let launch;
3070
+ if (launchPolicy !== "detached-tmux") {
3071
+ try {
3072
+ launch = await preLaunch(cwd, sessionId, notifyTempResult.contract, codexHomeOverride, enableNotifyFallbackAuthority, worktreeDirty);
3073
+ }
3074
+ catch (err) {
3075
+ logEstablishmentCleanupEvidence(err);
3076
+ if (isSessionPointerLaunchAbort(err)) {
3077
+ console.error(`[omx] session pointer launch aborted: ${err.code}`);
3078
+ reportOrdinaryLaunchRootConflict(err, cwd, cwd === launchCwd && !parsedWorktree.mode.enabled && !isResumeCodexLaunch(normalizedArgs));
3079
+ }
3080
+ restoreInsideTmuxControlPlane?.();
2603
3081
  await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch((cleanupErr) => {
2604
3082
  console.error(`[omx] preLaunch abort cleanup warning: ${cleanupErr instanceof Error ? cleanupErr.message : cleanupErr}`);
2605
3083
  });
2606
3084
  throw err;
2607
3085
  }
2608
- // preLaunch errors after pointer commit must not prevent Codex from starting.
2609
- console.error(`[omx] preLaunch warning: ${err instanceof Error ? err.message : err}`);
2610
3086
  }
2611
- // ── Phase 2: run ────────────────────────────────────────────────────────
2612
3087
  let postLaunchHandledExternally = false;
2613
3088
  try {
2614
3089
  const notifyTempContractRaw = notifyTempResult.contract.active
2615
3090
  ? serializeNotifyTempContract(notifyTempResult.contract)
2616
3091
  : null;
2617
- const launchResult = runCodex(cwd, normalizedArgs, sessionId, workerSparkModel, codexHomeOverride, sqliteHomeOverride, notifyTempContractRaw, effectiveExplicitLaunchPolicy, projectLocalCodexHomeForCleanup, preparedCodexHome.runtimeCodexHomeForCleanup, madmaxWorktreeRuntimeContext);
3092
+ const launchResult = await runCodex(cwd, normalizedArgs, sessionId, workerSparkModel, codexHomeOverride, sqliteHomeOverride, notifyTempContractRaw, {
3093
+ notifyTempContract: notifyTempResult.contract,
3094
+ enableNotifyFallbackAuthority,
3095
+ worktreeDirty,
3096
+ }, launchPolicy, detachedPreflight, projectLocalCodexHomeForCleanup, preparedCodexHome.runtimeCodexHomeForCleanup, madmaxWorktreeRuntimeContext, verifiedMadmaxLaunchContext, insideTmuxControlPlane);
2618
3097
  postLaunchHandledExternally = launchResult.postLaunchHandledExternally;
2619
3098
  }
3099
+ catch (error) {
3100
+ if (error instanceof DetachedLaunchSafetyError)
3101
+ postLaunchHandledExternally = true;
3102
+ throw error;
3103
+ }
2620
3104
  finally {
2621
- // ── Phase 3: postLaunch ─────────────────────────────────────────────
2622
- if (!postLaunchHandledExternally) {
2623
- await postLaunch(cwd, sessionId, codexHomeOverride, enableNotifyFallbackAuthority, projectLocalCodexHomeForCleanup);
3105
+ if (!postLaunchHandledExternally && launch) {
3106
+ try {
3107
+ await postLaunch(cwd, sessionId, launch.binding, codexHomeOverride, enableNotifyFallbackAuthority, projectLocalCodexHomeForCleanup);
3108
+ }
3109
+ finally {
3110
+ restoreInsideTmuxControlPlane?.();
3111
+ }
2624
3112
  await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch(logCliOperationFailure);
2625
3113
  }
3114
+ else {
3115
+ restoreInsideTmuxControlPlane?.();
3116
+ }
2626
3117
  }
2627
3118
  }
2628
3119
  export async function execWithOverlay(args) {
@@ -2689,18 +3180,19 @@ export async function execWithOverlay(args) {
2689
3180
  const codexHomeOverride = preparedCodexHome.codexHomeOverride;
2690
3181
  const sqliteHomeOverride = preparedCodexHome.sqliteHomeOverride;
2691
3182
  const projectLocalCodexHomeForCleanup = preparedCodexHome.projectLocalCodexHomeForCleanup;
3183
+ let launch;
2692
3184
  try {
2693
- await preLaunch(cwd, sessionId, notifyTempResult.contract, codexHomeOverride, true, worktreeDirty);
3185
+ launch = await preLaunch(cwd, sessionId, notifyTempResult.contract, codexHomeOverride, true, worktreeDirty);
2694
3186
  }
2695
3187
  catch (err) {
3188
+ logEstablishmentCleanupEvidence(err);
2696
3189
  if (isSessionPointerLaunchAbort(err)) {
2697
3190
  console.error(`[omx] session pointer launch aborted: ${err.code}`);
2698
- await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch((cleanupErr) => {
2699
- console.error(`[omx] preLaunch abort cleanup warning: ${cleanupErr instanceof Error ? cleanupErr.message : cleanupErr}`);
2700
- });
2701
- throw err;
2702
3191
  }
2703
- console.error(`[omx] preLaunch warning: ${err instanceof Error ? err.message : err}`);
3192
+ await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch((cleanupErr) => {
3193
+ console.error(`[omx] preLaunch abort cleanup warning: ${cleanupErr instanceof Error ? cleanupErr.message : cleanupErr}`);
3194
+ });
3195
+ throw err;
2704
3196
  }
2705
3197
  try {
2706
3198
  const notifyTempContractRaw = notifyTempResult.contract.active
@@ -2708,22 +3200,28 @@ export async function execWithOverlay(args) {
2708
3200
  : null;
2709
3201
  const codexArgs = injectModelInstructionsBypassArgs(cwd, ["exec", ...normalizedArgs], process.env, sessionModelInstructionsPath(cwd, sessionId));
2710
3202
  const omxRootOverride = resolveOmxRootForLaunch(cwd, process.env);
2711
- const codexEnvBase = {
2712
- ...process.env,
3203
+ const omxBin = resolveOmxCliEntryPath({ argv1: process.argv[1], cwd, env: process.env });
3204
+ if (!omxBin)
3205
+ throw new Error("Unable to resolve OMX launcher path for exec");
3206
+ const hudRuntimeRoot = resolveHudRuntimeRootForLaunch(cwd, process.env);
3207
+ const codexEnvBase = prependOmxRuntimeCommandShimToEnv(cwd, {
3208
+ ...stripHermesMcpBridgeEnv(process.env),
2713
3209
  ...(codexHomeOverride ? { CODEX_HOME: codexHomeOverride } : {}),
2714
3210
  ...(sqliteHomeOverride ? { [CODEX_SQLITE_HOME_ENV]: sqliteHomeOverride } : {}),
2715
3211
  ...(omxRootOverride ? { OMX_ROOT: omxRootOverride } : {}),
3212
+ }, omxBin);
3213
+ // Correlates plugin hook routing only; it is not an authority token.
3214
+ const pluginHookRoutingLaunchId = randomUUID();
3215
+ const codexEnv = {
3216
+ ...codexEnvBase,
3217
+ OMX_CODEX_LAUNCH_ID: pluginHookRoutingLaunchId,
3218
+ ...buildHudRuntimeEnv({ sessionId, ...hudRuntimeRoot }).env,
3219
+ ...(notifyTempContractRaw ? { [OMX_NOTIFY_TEMP_CONTRACT_ENV]: notifyTempContractRaw } : {}),
2716
3220
  };
2717
- const codexEnv = notifyTempContractRaw
2718
- ? {
2719
- ...codexEnvBase,
2720
- [OMX_NOTIFY_TEMP_CONTRACT_ENV]: notifyTempContractRaw,
2721
- }
2722
- : codexEnvBase;
2723
3221
  runCodexBlocking(cwd, codexArgs, codexEnv);
2724
3222
  }
2725
3223
  finally {
2726
- await postLaunch(cwd, sessionId, codexHomeOverride, true, projectLocalCodexHomeForCleanup);
3224
+ await postLaunch(cwd, sessionId, launch.binding, codexHomeOverride, true, projectLocalCodexHomeForCleanup);
2727
3225
  await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch(logCliOperationFailure);
2728
3226
  }
2729
3227
  }
@@ -3005,9 +3503,6 @@ export function detectDetachedSessionWindowIndex(sessionName) {
3005
3503
  return null;
3006
3504
  }
3007
3505
  }
3008
- function escapeShellDoubleQuotedValue(value) {
3009
- return value.replace(/["\\$`]/g, "\\$&");
3010
- }
3011
3506
  function sanitizeTmuxLeaseKey(value) {
3012
3507
  const cleaned = value
3013
3508
  .toLowerCase()
@@ -3193,121 +3688,188 @@ function withTmuxExtendedKeysLeaseLock(cwd, socketPath, run) {
3193
3688
  }
3194
3689
  throw new Error(`timed out waiting for tmux extended-keys lease lock: ${lockPath}`);
3195
3690
  }
3196
- function buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath) {
3197
- const detachedPostLaunchHelper = sessionId
3198
- ? `${buildDetachedSessionPostLaunchHelperCommand(cwd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup)} >/dev/null 2>&1 || true;`
3199
- : "";
3200
- const parentEnvSource = parentEnvFilePath && parentEnvFilePath.trim()
3201
- ? `if [ -r ${quoteShellArg(parentEnvFilePath)} ]; then . ${quoteShellArg(parentEnvFilePath)}; rm -f ${quoteShellArg(parentEnvFilePath)}; fi;`
3202
- : "";
3203
- const parentEnvCleanup = parentEnvFilePath && parentEnvFilePath.trim()
3204
- ? `rm -f ${quoteShellArg(parentEnvFilePath)} 2>/dev/null || true;`
3205
- : "";
3206
- const wrapped = [
3207
- buildTmuxExtendedKeysAcquireShellSnippet(cwd),
3208
- 'exec 3<&0;',
3209
- 'omx_codex_pid="";',
3210
- "omx_detached_session_cleanup() {",
3211
- "status=$?;",
3212
- "trap - 0 INT TERM HUP;",
3213
- 'if [ -n "$omx_codex_pid" ] && kill -0 "$omx_codex_pid" 2>/dev/null; then',
3214
- 'kill -TERM "$omx_codex_pid" 2>/dev/null || true;',
3215
- 'wait "$omx_codex_pid" 2>/dev/null || true;',
3216
- "fi;",
3217
- 'exec 3<&- 2>/dev/null || true;',
3218
- buildTmuxExtendedKeysReleaseShellSnippet(cwd),
3219
- parentEnvCleanup,
3220
- detachedPostLaunchHelper,
3221
- 'if [ "$status" -eq 0 ]; then',
3222
- `tmux kill-session -t "${escapeShellDoubleQuotedValue(sessionName)}" >/dev/null 2>&1 || true;`,
3223
- "fi;",
3224
- "exit $status;",
3225
- "};",
3226
- "trap omx_detached_session_cleanup 0 INT TERM HUP;",
3227
- parentEnvSource,
3228
- "unset OMX_HERMES_MCP_BRIDGE;",
3229
- "omx_codex_started_at=$(date +%s 2>/dev/null || printf 0);",
3230
- `${codexCmd} <&3 &`,
3231
- "omx_codex_pid=$!;",
3232
- 'wait "$omx_codex_pid";',
3233
- "omx_codex_status=$?;",
3234
- "omx_codex_finished_at=$(date +%s 2>/dev/null || printf 0);",
3235
- 'omx_codex_elapsed=$((omx_codex_finished_at - omx_codex_started_at));',
3236
- 'if [ "$omx_codex_status" -eq 0 ] && [ "$omx_codex_elapsed" -le 2 ]; then',
3237
- 'printf "\\n[omx] codex exited immediately with code 0 during startup. The detached tmux session is being kept open so any output above remains visible. Press Enter to close this OMX session.\\n" >&2;',
3238
- 'IFS= read -r _omx_close || true;',
3239
- 'elif [ "$omx_codex_status" -gt 0 ] && [ "$omx_codex_status" -lt 128 ] && [ "$omx_codex_elapsed" -le 2 ]; then',
3240
- 'printf "\\n[omx] codex exited with code %s during startup. The detached tmux session is being kept open so the error above remains visible. Press Enter to close this OMX session.\\n" "$omx_codex_status" >&2;',
3241
- 'IFS= read -r _omx_close || true;',
3242
- 'elif [ "$omx_codex_status" -gt 0 ] && [ "$omx_codex_status" -lt 128 ]; then',
3243
- 'printf "\\n[omx] codex exited with code %s. The detached tmux session is being kept open so the error above remains visible. Press Enter to close this OMX session.\\n" "$omx_codex_status" >&2;',
3244
- 'IFS= read -r _omx_close || true;',
3245
- "fi;",
3246
- 'exit "$omx_codex_status";',
3247
- ].join(" ");
3248
- return `/bin/sh -c ${quoteShellArg(wrapped)}`;
3249
- }
3250
- function buildDetachedSessionPostLaunchHelperCommand(cwd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup) {
3251
- const cwdLiteral = JSON.stringify(cwd);
3252
- const sessionIdLiteral = JSON.stringify(sessionId);
3253
- const codexHomeLiteral = typeof codexHomeOverride === "string" && codexHomeOverride.length > 0
3254
- ? JSON.stringify(codexHomeOverride)
3255
- : "undefined";
3256
- const projectLocalCleanupLiteral = typeof projectLocalCodexHomeForCleanup === "string" &&
3257
- projectLocalCodexHomeForCleanup.length > 0
3258
- ? JSON.stringify(projectLocalCodexHomeForCleanup)
3259
- : "undefined";
3260
- const runtimeCodexHomeCleanupLiteral = typeof runtimeCodexHomeForCleanup === "string" &&
3261
- runtimeCodexHomeForCleanup.length > 0
3262
- ? JSON.stringify(runtimeCodexHomeForCleanup)
3263
- : "undefined";
3264
- const moduleUrlLiteral = JSON.stringify(import.meta.url);
3265
- const script = [
3266
- `const mod = await import(${moduleUrlLiteral});`,
3267
- `await mod.runDetachedSessionPostLaunch(${cwdLiteral}, ${sessionIdLiteral}, ${codexHomeLiteral}, ${projectLocalCleanupLiteral}, ${runtimeCodexHomeCleanupLiteral});`,
3268
- ].join(" ");
3269
- return `${quoteShellArg(process.execPath)} --input-type=module -e ${quoteShellArg(script)}`;
3270
- }
3271
- function execTmuxSync(args, execFileSyncImpl = (file, tmuxArgs) => execFileSync(file, tmuxArgs, {
3272
- encoding: "utf-8",
3273
- ...(process.platform === "win32" ? { windowsHide: true } : {}),
3274
- })) {
3275
- return execFileSyncImpl(resolveTmuxExecutableForLaunch(), [...args]).trim();
3691
+ function buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath, readyPath, preLaunchOptions, omxBin = process.argv[1]) {
3692
+ const payload = Buffer.from(JSON.stringify({
3693
+ cwd, sessionName, sessionId, codexCmd, codexHomeOverride,
3694
+ projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath,
3695
+ readyPath, preLaunchOptions,
3696
+ })).toString("base64url");
3697
+ return `/bin/sh -c ${quoteShellArg(`${parentEnvFilePath ? `if [ -r ${quoteShellArg(parentEnvFilePath)} ]; then . ${quoteShellArg(parentEnvFilePath)}; rm -f ${quoteShellArg(parentEnvFilePath)}; fi; ` : ""}exec ${quoteShellArg(process.execPath)} ${quoteShellArg(omxBin)} __detached-session-leader ${quoteShellArg(payload)}`)}`;
3698
+ }
3699
+ function buildDetachedWindowsLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnv, readyPath, preLaunchOptions, omxBin = process.argv[1]) {
3700
+ const payload = Buffer.from(JSON.stringify({
3701
+ cwd, sessionName, sessionId, codexCmd, codexHomeOverride,
3702
+ projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup,
3703
+ ...(parentEnv ? { parentEnv: detachedSessionParentEnvironment(parentEnv, true) } : {}),
3704
+ readyPath, preLaunchOptions,
3705
+ })).toString("base64url");
3706
+ const invocation = `& ${quotePowerShellArg(process.execPath)} ${quotePowerShellArg(omxBin)} __detached-session-leader ${quotePowerShellArg(payload)}`;
3707
+ return `powershell.exe -NoLogo -NoProfile -NonInteractive -Command ${quotePowerShellArg(invocation)}`;
3708
+ }
3709
+ export function isDetachedReadyReportAuthorized(report, expected) {
3710
+ if (report?.kind !== "ready" || report.nonce !== expected.nonce
3711
+ || report.sessionId !== expected.sessionId || report.sessionName !== expected.sessionName
3712
+ || typeof report.leaderPid !== "number" || report.leaderPid <= 0)
3713
+ return false;
3714
+ if (expected.shouldAttach && expected.leaderPanePid !== null) {
3715
+ return report.leaderPid === expected.leaderPanePid;
3716
+ }
3717
+ return report.paneId === expected.leaderPaneId;
3276
3718
  }
3277
- export function acquireTmuxExtendedKeysLease(cwd, execFileSyncImpl = (file, tmuxArgs) => execFileSync(file, tmuxArgs, {
3278
- encoding: "utf-8",
3279
- ...(process.platform === "win32" ? { windowsHide: true } : {}),
3280
- }), ownerPid = process.pid) {
3719
+ function writeDetachedLeaderReport(path, report) {
3720
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
3721
+ const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
3722
+ let fd;
3281
3723
  try {
3282
- const socketPath = resolveTmuxSocketPath(execFileSyncImpl);
3283
- const leasePath = tmuxExtendedKeysLeasePath(cwd, socketPath);
3284
- const holderPid = Number.isSafeInteger(ownerPid) && ownerPid > 0 ? ownerPid : process.pid;
3285
- const leaseId = `${holderPid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
3286
- withTmuxExtendedKeysLeaseLock(cwd, socketPath, () => {
3287
- const stateRaw = readTmuxExtendedKeysLeaseState(leasePath);
3288
- const state = stateRaw ? reapDeadTmuxExtendedKeysLeaseHolders(stateRaw) : null;
3289
- if (stateRaw && state?.holders.length === 0) {
3290
- execTmuxSync(["set-option", "-sq", "extended-keys", state.originalMode], execFileSyncImpl);
3291
- rmSync(leasePath, { force: true });
3724
+ fd = openSync(tempPath, "wx", 0o600);
3725
+ writeFileSync(fd, `${JSON.stringify(report)}\n`, "utf-8");
3726
+ try {
3727
+ fsyncSync(fd);
3728
+ }
3729
+ catch (error) {
3730
+ if (!(process.platform === "win32" && error.code === "EPERM"))
3731
+ throw error;
3732
+ }
3733
+ closeSync(fd);
3734
+ fd = undefined;
3735
+ renameSync(tempPath, path);
3736
+ const directoryFd = openSync(dirname(path), "r");
3737
+ try {
3738
+ try {
3739
+ fsyncSync(directoryFd);
3292
3740
  }
3293
- if (!state || state.holders.length === 0) {
3294
- const previousMode = execTmuxSync(["show-options", "-sv", "extended-keys"], execFileSyncImpl) ||
3295
- TMUX_EXTENDED_KEYS_FALLBACK_MODE;
3296
- execTmuxSync(["set-option", "-sq", "extended-keys", TMUX_EXTENDED_KEYS_MODE], execFileSyncImpl);
3297
- writeTmuxExtendedKeysLeaseState(leasePath, {
3298
- originalMode: previousMode,
3299
- holders: [createTmuxExtendedKeysLeaseHolder(leaseId, holderPid)],
3300
- });
3301
- return;
3741
+ catch (error) {
3742
+ if (!(process.platform === "win32" && error.code === "EPERM"))
3743
+ throw error;
3302
3744
  }
3303
- state.holders.push(createTmuxExtendedKeysLeaseHolder(leaseId, holderPid));
3304
- writeTmuxExtendedKeysLeaseState(leasePath, state);
3305
- });
3306
- return `${socketPath}\t${leaseId}`;
3307
- }
3308
- catch (err) {
3309
- if (!isUnsupportedTmuxExtendedKeysFailure(err)) {
3310
- logCliOperationFailure(err);
3745
+ }
3746
+ finally {
3747
+ closeSync(directoryFd);
3748
+ }
3749
+ }
3750
+ finally {
3751
+ if (fd !== undefined)
3752
+ closeSync(fd);
3753
+ try {
3754
+ unlinkSync(tempPath);
3755
+ }
3756
+ catch { }
3757
+ }
3758
+ }
3759
+ function readDetachedLeaderReport(path) {
3760
+ try {
3761
+ const value = JSON.parse(readFileSync(path, "utf8"));
3762
+ if (!value || typeof value !== "object")
3763
+ return undefined;
3764
+ const report = value;
3765
+ if (report.version !== 1 || !["ready", "failed", "terminal", "release", "abort"].includes(String(report.kind)) ||
3766
+ typeof report.nonce !== "string" || typeof report.sessionId !== "string" || typeof report.sessionName !== "string")
3767
+ return undefined;
3768
+ if (report.paneId !== undefined && typeof report.paneId !== "string")
3769
+ return undefined;
3770
+ if (report.leaderPid !== undefined && (!Number.isSafeInteger(report.leaderPid) || Number(report.leaderPid) <= 0))
3771
+ return undefined;
3772
+ if (report.exitStatus !== undefined && (!Number.isSafeInteger(report.exitStatus) || Number(report.exitStatus) < 0 || Number(report.exitStatus) > 255))
3773
+ return undefined;
3774
+ return report;
3775
+ }
3776
+ catch {
3777
+ return undefined;
3778
+ }
3779
+ }
3780
+ /** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
3781
+ export function publishDetachedReleaseMarker(releaseMarkerPath, nonce, sessionId, sessionName, leaderPid, hud) {
3782
+ writeDetachedLeaderReport(`${releaseMarkerPath}.release`, {
3783
+ version: 1, kind: "release", nonce, sessionId, sessionName, leaderPid, ...(hud ? { hud } : {}),
3784
+ });
3785
+ }
3786
+ /** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
3787
+ export function applyDetachedTerminalExitStatus(releaseMarkerPath, expected) {
3788
+ const report = readDetachedLeaderReport(releaseMarkerPath);
3789
+ if (!report || report.kind !== "terminal" || report.finalized !== true)
3790
+ return false;
3791
+ if (report.nonce !== expected.nonce || report.sessionId !== expected.sessionId ||
3792
+ report.sessionName !== expected.sessionName || report.leaderPid !== expected.leaderPid)
3793
+ return false;
3794
+ if (typeof report.exitStatus !== "number")
3795
+ return false;
3796
+ process.exitCode = report.exitStatus;
3797
+ return true;
3798
+ }
3799
+ /** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
3800
+ export function probeExactDetachedSessionExists(authority) {
3801
+ if (!authority)
3802
+ return undefined;
3803
+ try {
3804
+ // A retained dead pane (remain-on-exit) keeps its pid/topology, so prove
3805
+ // liveness first: a dead leader with no valid terminal report is a failure,
3806
+ // never a manual detach.
3807
+ const paneDead = execTmuxFileSync(["display-message", "-p", "-t", authority.paneId, "#{pane_dead}"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
3808
+ if (paneDead === "1")
3809
+ return false;
3810
+ if (paneDead !== "0")
3811
+ return undefined;
3812
+ // Re-capture the exact leader-pane topology the bootstrap authority recorded.
3813
+ // A matching recapture proves the owned session and its leader survive (manual
3814
+ // detach); a destroyed session, recycled pane id, or tmux error fails closed.
3815
+ const current = captureDetachedLeaderAuthority(authority.paneId, authority.sessionName, authority.ownerId);
3816
+ return current.panePid === authority.panePid && current.sessionId === authority.sessionId
3817
+ && current.sessionCreated === authority.sessionCreated && current.windowId === authority.windowId
3818
+ && current.windowIndex === authority.windowIndex;
3819
+ }
3820
+ catch {
3821
+ return undefined;
3822
+ }
3823
+ }
3824
+ /** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
3825
+ export function resolveDetachedAttachExitStatus(releaseMarkerPath, expected, probe) {
3826
+ if (applyDetachedTerminalExitStatus(releaseMarkerPath, expected))
3827
+ return "applied";
3828
+ return probe.exactSessionExists() === true ? "manual-detach" : "protocol-failure";
3829
+ }
3830
+ function publishDetachedAbortMarker(releaseMarkerPath, nonce, sessionId, sessionName, leaderPid) {
3831
+ writeDetachedLeaderReport(`${releaseMarkerPath}.abort`, { version: 1, kind: "abort", nonce, sessionId, sessionName, leaderPid });
3832
+ }
3833
+ function execTmuxSync(args, execFileSyncImpl = (file, tmuxArgs) => execFileSync(file, tmuxArgs, {
3834
+ encoding: "utf-8",
3835
+ ...(process.platform === "win32" ? { windowsHide: true } : {}),
3836
+ })) {
3837
+ return execFileSyncImpl(resolveTmuxExecutableForLaunch(), [...args]).trim();
3838
+ }
3839
+ export function acquireTmuxExtendedKeysLease(cwd, execFileSyncImpl = (file, tmuxArgs) => execFileSync(file, tmuxArgs, {
3840
+ encoding: "utf-8",
3841
+ ...(process.platform === "win32" ? { windowsHide: true } : {}),
3842
+ }), ownerPid = process.pid) {
3843
+ try {
3844
+ const socketPath = resolveTmuxSocketPath(execFileSyncImpl);
3845
+ const leasePath = tmuxExtendedKeysLeasePath(cwd, socketPath);
3846
+ const holderPid = Number.isSafeInteger(ownerPid) && ownerPid > 0 ? ownerPid : process.pid;
3847
+ const leaseId = `${holderPid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
3848
+ withTmuxExtendedKeysLeaseLock(cwd, socketPath, () => {
3849
+ const stateRaw = readTmuxExtendedKeysLeaseState(leasePath);
3850
+ const state = stateRaw ? reapDeadTmuxExtendedKeysLeaseHolders(stateRaw) : null;
3851
+ if (stateRaw && state?.holders.length === 0) {
3852
+ execTmuxSync(["set-option", "-sq", "extended-keys", state.originalMode], execFileSyncImpl);
3853
+ rmSync(leasePath, { force: true });
3854
+ }
3855
+ if (!state || state.holders.length === 0) {
3856
+ const previousMode = execTmuxSync(["show-options", "-sv", "extended-keys"], execFileSyncImpl) ||
3857
+ TMUX_EXTENDED_KEYS_FALLBACK_MODE;
3858
+ execTmuxSync(["set-option", "-sq", "extended-keys", TMUX_EXTENDED_KEYS_MODE], execFileSyncImpl);
3859
+ writeTmuxExtendedKeysLeaseState(leasePath, {
3860
+ originalMode: previousMode,
3861
+ holders: [createTmuxExtendedKeysLeaseHolder(leaseId, holderPid)],
3862
+ });
3863
+ return;
3864
+ }
3865
+ state.holders.push(createTmuxExtendedKeysLeaseHolder(leaseId, holderPid));
3866
+ writeTmuxExtendedKeysLeaseState(leasePath, state);
3867
+ });
3868
+ return `${socketPath}\t${leaseId}`;
3869
+ }
3870
+ catch (err) {
3871
+ if (!isUnsupportedTmuxExtendedKeysFailure(err)) {
3872
+ logCliOperationFailure(err);
3311
3873
  }
3312
3874
  return null;
3313
3875
  }
@@ -3352,19 +3914,123 @@ export function releaseTmuxExtendedKeysLease(cwd, leaseHandle, execFileSyncImpl
3352
3914
  }
3353
3915
  }
3354
3916
  }
3355
- function buildTmuxExtendedKeysHelperCommand(cwd, operation) {
3356
- const cwdLiteral = JSON.stringify(cwd);
3357
- const moduleUrlLiteral = JSON.stringify(import.meta.url);
3358
- const script = operation === "acquire"
3359
- ? `const mod = await import(${moduleUrlLiteral}); const ownerPid = Number.parseInt(process.argv[1] ?? "", 10); const lease = mod.acquireTmuxExtendedKeysLease(${cwdLiteral}, undefined, Number.isSafeInteger(ownerPid) && ownerPid > 0 ? ownerPid : undefined); if (lease) process.stdout.write(lease);`
3360
- : `const mod = await import(${moduleUrlLiteral}); mod.releaseTmuxExtendedKeysLease(${cwdLiteral}, process.argv[1] ?? "");`;
3361
- return `${quoteShellArg(process.execPath)} --input-type=module -e ${quoteShellArg(script)}`;
3362
- }
3363
- function buildTmuxExtendedKeysAcquireShellSnippet(cwd) {
3364
- return `OMX_TMUX_EXTENDED_KEYS_LEASE=$(${buildTmuxExtendedKeysHelperCommand(cwd, "acquire")} "$$" 2>/dev/null || true);`;
3917
+ export const DETACHED_LAUNCH_CONTROL_PLANE_KEYS = [
3918
+ "OMX_SESSION_ID",
3919
+ "CODEX_SESSION_ID",
3920
+ "SESSION_ID",
3921
+ "OMX_TMUX_HUD_OWNER",
3922
+ "OMX_TMUX_HUD_LEADER_PANE",
3923
+ "OMX_ROOT",
3924
+ "OMX_STATE_ROOT",
3925
+ "OMX_TEAM_STATE_ROOT",
3926
+ "OMXBOX_ACTIVE",
3927
+ "OMX_SOURCE_CWD",
3928
+ "OMX_MADMAX_DETACHED_CONTEXT",
3929
+ "OMX_RUNS_DIR",
3930
+ "OMX_TEAM_LEADER_CWD",
3931
+ "OMX_TEAM_WORKER",
3932
+ "OMX_TEAM_INTERNAL_WORKER",
3933
+ ];
3934
+ const DETACHED_LAUNCH_CONTROL_PLANE_KEY_SET = new Set(DETACHED_LAUNCH_CONTROL_PLANE_KEYS);
3935
+ function isDetachedLaunchControlPlaneKey(key, caseInsensitive = false) {
3936
+ return caseInsensitive
3937
+ ? DETACHED_LAUNCH_CONTROL_PLANE_KEY_SET.has(key.toUpperCase())
3938
+ : DETACHED_LAUNCH_CONTROL_PLANE_KEY_SET.has(key);
3939
+ }
3940
+ function emptyDetachedLaunchControlPlaneValues() {
3941
+ return Object.fromEntries(DETACHED_LAUNCH_CONTROL_PLANE_KEYS.map((key) => [key, ""]));
3942
+ }
3943
+ function resolveDetachedRootValue(cwd, raw) {
3944
+ return isCrossPlatformAbsolutePath(raw) ? raw : resolve(cwd, raw);
3945
+ }
3946
+ export function buildDetachedLaunchControlPlane(options) {
3947
+ const env = options.env ?? process.env;
3948
+ const values = emptyDetachedLaunchControlPlaneValues();
3949
+ values.OMX_SESSION_ID = options.sessionId;
3950
+ values.OMX_TMUX_HUD_OWNER = "1";
3951
+ values.OMX_TMUX_HUD_LEADER_PANE = options.leaderPaneId ?? "";
3952
+ const runtimeContext = options.runtimeContext;
3953
+ const verifiedMadmaxContext = options.verifiedMadmaxContext;
3954
+ if (runtimeContext && verifiedMadmaxContext) {
3955
+ values.OMX_ROOT = verifiedMadmaxContext.root;
3956
+ values.OMXBOX_ACTIVE = "1";
3957
+ values.OMX_SOURCE_CWD = verifiedMadmaxContext.sourceCwd;
3958
+ values.OMX_MADMAX_DETACHED_CONTEXT = verifiedMadmaxContext.context;
3959
+ values.OMX_RUNS_DIR = verifiedMadmaxContext.runsRoot;
3960
+ }
3961
+ else {
3962
+ const madmaxGuardPresent = env.OMXBOX_ACTIVE === "1";
3963
+ const teamRoot = env.OMX_TEAM_STATE_ROOT?.trim();
3964
+ const root = env.OMX_ROOT?.trim();
3965
+ const stateRoot = env.OMX_STATE_ROOT?.trim();
3966
+ const resolvedStateRoot = stateRoot ? resolveDetachedRootValue(options.cwd, stateRoot) : undefined;
3967
+ const explicitOverrideWins = Boolean(options.omxRootOverride?.trim()
3968
+ && !teamRoot
3969
+ && !root
3970
+ && (!resolvedStateRoot || resolveDetachedRootValue(options.cwd, options.omxRootOverride.trim()) !== resolvedStateRoot));
3971
+ if (teamRoot)
3972
+ values.OMX_TEAM_STATE_ROOT = resolveDetachedRootValue(options.cwd, teamRoot);
3973
+ if (!madmaxGuardPresent) {
3974
+ if (explicitOverrideWins)
3975
+ values.OMX_ROOT = resolveDetachedRootValue(options.cwd, options.omxRootOverride.trim());
3976
+ else if (!teamRoot && root)
3977
+ values.OMX_ROOT = resolveDetachedRootValue(options.cwd, root);
3978
+ else if (!teamRoot && stateRoot)
3979
+ values.OMX_STATE_ROOT = resolvedStateRoot;
3980
+ else if (!teamRoot && options.omxRootOverride?.trim())
3981
+ values.OMX_ROOT = resolveDetachedRootValue(options.cwd, options.omxRootOverride.trim());
3982
+ }
3983
+ if (verifiedMadmaxContext) {
3984
+ values.OMX_ROOT = verifiedMadmaxContext.root;
3985
+ values.OMX_STATE_ROOT = "";
3986
+ values.OMX_TEAM_STATE_ROOT = "";
3987
+ values.OMXBOX_ACTIVE = "1";
3988
+ values.OMX_SOURCE_CWD = verifiedMadmaxContext.sourceCwd;
3989
+ values.OMX_MADMAX_DETACHED_CONTEXT = verifiedMadmaxContext.context;
3990
+ values.OMX_RUNS_DIR = verifiedMadmaxContext.runsRoot;
3991
+ }
3992
+ }
3993
+ const verifiedTeam = options.verifiedTeamContext;
3994
+ if (verifiedTeam && !verifiedMadmaxContext) {
3995
+ values.OMX_ROOT = "";
3996
+ values.OMX_STATE_ROOT = "";
3997
+ values.OMXBOX_ACTIVE = "";
3998
+ values.OMX_SOURCE_CWD = "";
3999
+ values.OMX_MADMAX_DETACHED_CONTEXT = "";
4000
+ values.OMX_RUNS_DIR = "";
4001
+ values.OMX_TEAM_STATE_ROOT = verifiedTeam.stateRoot;
4002
+ values.OMX_TEAM_LEADER_CWD = verifiedTeam.leaderCwd;
4003
+ values.OMX_TEAM_WORKER = verifiedTeam.worker;
4004
+ values.OMX_TEAM_INTERNAL_WORKER = verifiedTeam.internalWorker;
4005
+ }
4006
+ return Object.freeze({
4007
+ values: Object.freeze(values),
4008
+ keys: DETACHED_LAUNCH_CONTROL_PLANE_KEYS,
4009
+ });
3365
4010
  }
3366
- function buildTmuxExtendedKeysReleaseShellSnippet(cwd) {
3367
- return `if [ -n "\${OMX_TMUX_EXTENDED_KEYS_LEASE:-}" ]; then ${buildTmuxExtendedKeysHelperCommand(cwd, "release")} "\${OMX_TMUX_EXTENDED_KEYS_LEASE}" >/dev/null 2>&1 || true; fi;`;
4011
+ export const resolveDetachedLaunchControlPlane = buildDetachedLaunchControlPlane;
4012
+ function detachedLaunchControlPlaneEnv(controlPlane) {
4013
+ return Object.fromEntries(DETACHED_LAUNCH_CONTROL_PLANE_KEYS.map((key) => [key, controlPlane.values[key]]));
4014
+ }
4015
+ function applyLaunchOwnedControlPlane(controlPlane) {
4016
+ const previous = new Map();
4017
+ for (const key of DETACHED_LAUNCH_CONTROL_PLANE_KEYS) {
4018
+ previous.set(key, process.env[key]);
4019
+ const value = controlPlane.values[key];
4020
+ if (value === "")
4021
+ delete process.env[key];
4022
+ else
4023
+ process.env[key] = value;
4024
+ }
4025
+ return () => {
4026
+ for (const key of DETACHED_LAUNCH_CONTROL_PLANE_KEYS) {
4027
+ const value = previous.get(key);
4028
+ if (typeof value === "string")
4029
+ process.env[key] = value;
4030
+ else
4031
+ delete process.env[key];
4032
+ }
4033
+ };
3368
4034
  }
3369
4035
  const SHELL_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
3370
4036
  const DETACHED_SESSION_PANE_ENV_KEYS = new Set([
@@ -3376,21 +4042,23 @@ const DETACHED_SESSION_PANE_ENV_KEYS = new Set([
3376
4042
  "COLUMNS",
3377
4043
  "LINES",
3378
4044
  ]);
3379
- export function serializeDetachedSessionParentEnv(env) {
3380
- const lines = [];
4045
+ function detachedSessionParentEnvironment(env, caseInsensitive = false) {
4046
+ const values = {};
3381
4047
  for (const key of Object.keys(env).sort()) {
3382
- if (!SHELL_ENV_NAME_PATTERN.test(key))
3383
- continue;
3384
- if (DETACHED_SESSION_PANE_ENV_KEYS.has(key))
4048
+ if (!SHELL_ENV_NAME_PATTERN.test(key)
4049
+ || DETACHED_SESSION_PANE_ENV_KEYS.has(key)
4050
+ || isDetachedLaunchControlPlaneKey(key, caseInsensitive))
3385
4051
  continue;
3386
4052
  const value = env[key];
3387
- if (typeof value !== "string")
3388
- continue;
3389
- if (value.includes("\0"))
3390
- continue;
3391
- lines.push(`export ${key}=${quoteShellArg(value)}`);
4053
+ if (typeof value === "string" && !value.includes("\0"))
4054
+ values[key] = value;
3392
4055
  }
3393
- return `${lines.join("\n")}\n`;
4056
+ return values;
4057
+ }
4058
+ export function serializeDetachedSessionParentEnv(env) {
4059
+ return `${Object.entries(detachedSessionParentEnvironment(env))
4060
+ .map(([key, value]) => `export ${key}=${quoteShellArg(value)}`)
4061
+ .join("\n")}\n`;
3394
4062
  }
3395
4063
  export function detachedSessionParentEnvFilePath(cwd, sessionId) {
3396
4064
  const safeSessionId = sessionId.replace(/[^A-Za-z0-9_.-]/g, "_");
@@ -3418,27 +4086,21 @@ export function withTmuxExtendedKeys(cwd, run, execFileSyncImpl = (file, tmuxArg
3418
4086
  releaseTmuxExtendedKeysLease(cwd, leaseHandle, execFileSyncImpl);
3419
4087
  }
3420
4088
  }
3421
- export function buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, hudCmd, workerLaunchArgs, codexHomeOverride, notifyTempContractRaw, nativeWindows = false, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, omxRootOverride, env = process.env, sqliteHomeOverride, parentEnvFilePath, inheritedWorkerModel) {
4089
+ export function buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, hudCmd, workerLaunchArgs, codexHomeOverride, notifyTempContractRaw, nativeWindows = false, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, omxRootOverride, env = process.env, sqliteHomeOverride, parentEnvFilePath, inheritedWorkerModel, releaseMarkerPath, omxBin = process.argv[1], preLaunchOptions = {
4090
+ enableNotifyFallbackAuthority: false,
4091
+ worktreeDirty: false,
4092
+ shouldAttach: true,
4093
+ }, controlPlane) {
3422
4094
  const detachedLeaderCmd = nativeWindows
3423
- ? "powershell.exe"
3424
- : buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath);
3425
- const resolvedEnvStateRoot = env.OMX_STATE_ROOT?.trim()
3426
- ? resolveLaunchPath(cwd, env.OMX_STATE_ROOT.trim())
3427
- : undefined;
3428
- const hasExplicitRootOverride = Boolean(env.OMX_ROOT?.trim()
3429
- || (omxRootOverride && omxRootOverride !== resolvedEnvStateRoot));
3430
- const hudRuntimeRoot = env.OMX_TEAM_STATE_ROOT?.trim()
3431
- ? resolveHudRuntimeRootForLaunch(cwd, env)
3432
- : hasExplicitRootOverride
3433
- ? {
3434
- omxRoot: omxRootOverride,
3435
- rootSource: resolveHudRuntimeRootSource(omxRootOverride, env),
3436
- }
3437
- : resolveHudRuntimeRootForLaunch(cwd, env);
3438
- const hudRuntimeEnv = buildHudRuntimeEnv({
3439
- sessionId,
3440
- ...hudRuntimeRoot,
3441
- }).env;
4095
+ ? buildDetachedWindowsLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, env, releaseMarkerPath, preLaunchOptions, omxBin)
4096
+ : buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath, releaseMarkerPath, preLaunchOptions, omxBin);
4097
+ const launchControlPlane = controlPlane ?? buildDetachedLaunchControlPlane({
4098
+ cwd,
4099
+ sessionId: sessionId ?? "",
4100
+ env,
4101
+ omxRootOverride,
4102
+ });
4103
+ const controlPlaneEnv = detachedLaunchControlPlaneEnv(launchControlPlane);
3442
4104
  const newSessionArgs = [
3443
4105
  "new-session",
3444
4106
  "-d",
@@ -3449,15 +4111,10 @@ export function buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, h
3449
4111
  sessionName,
3450
4112
  "-c",
3451
4113
  cwd,
4114
+ ...DETACHED_LAUNCH_CONTROL_PLANE_KEYS.flatMap((key) => ["-e", `${key}=${controlPlaneEnv[key]}`]),
3452
4115
  ...(workerLaunchArgs ? ["-e", `${TEAM_WORKER_LAUNCH_ARGS_ENV}=${workerLaunchArgs}`] : []),
3453
- ...Object.entries(hudRuntimeEnv).map(([key, value]) => ["-e", `${key}=${value}`]).flat(),
3454
4116
  ...(codexHomeOverride ? ["-e", `CODEX_HOME=${codexHomeOverride}`] : []),
3455
4117
  ...(sqliteHomeOverride ? ["-e", `${CODEX_SQLITE_HOME_ENV}=${sqliteHomeOverride}`] : []),
3456
- ...(env.OMXBOX_ACTIVE ? ["-e", `OMXBOX_ACTIVE=${env.OMXBOX_ACTIVE}`] : []),
3457
- ...(env.OMX_SOURCE_CWD ? ["-e", `OMX_SOURCE_CWD=${env.OMX_SOURCE_CWD}`] : []),
3458
- ...(env[OMX_MADMAX_DETACHED_CONTEXT_ENV]
3459
- ? ["-e", `${OMX_MADMAX_DETACHED_CONTEXT_ENV}=${env[OMX_MADMAX_DETACHED_CONTEXT_ENV]}`]
3460
- : []),
3461
4118
  ...(notifyTempContractRaw
3462
4119
  ? ["-e", `${OMX_NOTIFY_TEMP_CONTRACT_ENV}=${notifyTempContractRaw}`]
3463
4120
  : []),
@@ -3717,41 +4374,40 @@ function isAutopilotReviewPendingPostLaunchState(state) {
3717
4374
  function postLaunchUniqueStrings(values) {
3718
4375
  return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
3719
4376
  }
3720
- async function scrubPostLaunchRootSkillActiveForSession(stateDir, sessionId, nowIso, writeFileFn, rootStateBeforeCleanup) {
4377
+ async function scrubPostLaunchRootSkillActiveForSession(stateDir, sessionId, nowIso, writeRootStateFn) {
3721
4378
  const normalizedSessionId = cleanPostLaunchString(sessionId);
3722
4379
  if (!normalizedSessionId)
3723
4380
  return;
3724
- const { rootPath } = getSkillActiveStatePathsForStateDir(stateDir);
3725
- const rootState = rootStateBeforeCleanup ?? await readSkillActiveState(rootPath);
3726
- if (!rootState)
3727
- return;
3728
- const rootSessionIds = postLaunchUniqueStrings([
3729
- cleanPostLaunchString(rootState.session_id),
3730
- cleanPostLaunchString(extractSessionIdFromInitializedStatePath(rootState.initialized_state_path)),
3731
- ]);
3732
- const rootBelongsToSession = rootSessionIds.includes(normalizedSessionId);
3733
- const entries = listActiveSkills(rootState);
3734
- const keptEntries = entries.filter((entry) => {
3735
- const entrySessionId = cleanPostLaunchString(entry.session_id);
3736
- if (entrySessionId)
3737
- return entrySessionId !== normalizedSessionId;
3738
- return !rootBelongsToSession;
4381
+ await writeRootStateFn(stateDir, (rootState) => {
4382
+ if (!rootState)
4383
+ return null;
4384
+ const rootSessionIds = postLaunchUniqueStrings([
4385
+ cleanPostLaunchString(rootState.session_id),
4386
+ cleanPostLaunchString(extractSessionIdFromInitializedStatePath(rootState.initialized_state_path)),
4387
+ ]);
4388
+ const rootBelongsToSession = rootSessionIds.includes(normalizedSessionId);
4389
+ const entries = listActiveSkills(rootState);
4390
+ const keptEntries = entries.filter((entry) => {
4391
+ const entrySessionId = cleanPostLaunchString(entry.session_id);
4392
+ if (entrySessionId)
4393
+ return entrySessionId !== normalizedSessionId;
4394
+ return !rootBelongsToSession;
4395
+ });
4396
+ if (keptEntries.length === entries.length && rootState.active !== true)
4397
+ return null;
4398
+ if (keptEntries.length === entries.length && !rootBelongsToSession)
4399
+ return null;
4400
+ return {
4401
+ ...rootState,
4402
+ active: keptEntries.length > 0,
4403
+ skill: keptEntries[0]?.skill ?? (keptEntries.length > 0 ? cleanPostLaunchString(rootState.skill) : ""),
4404
+ phase: keptEntries[0]?.phase ?? (keptEntries.length > 0 ? cleanPostLaunchString(rootState.phase) : "complete"),
4405
+ updated_at: nowIso,
4406
+ active_skills: keptEntries,
4407
+ post_launch_reconciled_at: nowIso,
4408
+ post_launch_reconciliation_reason: "terminal_session_cleanup",
4409
+ };
3739
4410
  });
3740
- if (keptEntries.length === entries.length && rootState.active !== true)
3741
- return;
3742
- if (keptEntries.length === entries.length && !rootBelongsToSession)
3743
- return;
3744
- const nextRoot = {
3745
- ...rootState,
3746
- active: keptEntries.length > 0,
3747
- skill: keptEntries[0]?.skill ?? (keptEntries.length > 0 ? cleanPostLaunchString(rootState.skill) : ""),
3748
- phase: keptEntries[0]?.phase ?? (keptEntries.length > 0 ? cleanPostLaunchString(rootState.phase) : "complete"),
3749
- updated_at: nowIso,
3750
- active_skills: keptEntries,
3751
- post_launch_reconciled_at: nowIso,
3752
- post_launch_reconciliation_reason: "terminal_session_cleanup",
3753
- };
3754
- await writeFileFn(rootPath, JSON.stringify(nextRoot, null, 2));
3755
4411
  }
3756
4412
  function buildRecoveredPostLaunchModeState(mode, completedAt) {
3757
4413
  return {
@@ -3798,9 +4454,14 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
3798
4454
  ? [getStateDir(cwd, sessionId)]
3799
4455
  : [getBaseStateDir(cwd)];
3800
4456
  const rootStateDir = getBaseStateDir(cwd);
3801
- const rootSkillActiveStateBeforeCleanup = sessionId
3802
- ? await readSkillActiveState(getSkillActiveStatePathsForStateDir(rootStateDir).rootPath)
3803
- : null;
4457
+ const writeRootState = dependencies.writeRootState ?? ((stateDir, update) => (updateRootSkillActiveStateForStateDir(stateDir, update)));
4458
+ const writeModeState = async (stateDir, mode, path, state) => {
4459
+ if (stateDir === rootStateDir && mode === SKILL_ACTIVE_STATE_MODE) {
4460
+ await writeRootState(stateDir, () => state);
4461
+ return;
4462
+ }
4463
+ await writeFile(path, JSON.stringify(state, null, 2));
4464
+ };
3804
4465
  let preserveSkillActiveForReviewPendingAutopilot = false;
3805
4466
  for (const stateDir of scopedDirs) {
3806
4467
  const files = await readdir(stateDir).catch(() => []);
@@ -3812,7 +4473,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
3812
4473
  && isAutopilotReviewPendingPostLaunchState(autopilotPrecheck.state);
3813
4474
  preserveSkillActiveForReviewPendingAutopilot ||= preserveReviewPendingAutopilot;
3814
4475
  for (const file of files) {
3815
- if (!file.endsWith("-state.json") || file === "session.json")
4476
+ if (!isModeStateFilename(file))
3816
4477
  continue;
3817
4478
  const path = join(stateDir, file);
3818
4479
  const mode = file.slice(0, -"-state.json".length);
@@ -3821,9 +4482,10 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
3821
4482
  if (result.kind === "recoverable") {
3822
4483
  try {
3823
4484
  const completedAt = now().toISOString();
3824
- await writeFile(path, JSON.stringify(mode === SKILL_ACTIVE_STATE_MODE
4485
+ const recoveredState = mode === SKILL_ACTIVE_STATE_MODE
3825
4486
  ? buildRecoveredPostLaunchSkillActiveState(completedAt)
3826
- : buildRecoveredPostLaunchModeState(mode, completedAt), null, 2));
4487
+ : buildRecoveredPostLaunchModeState(mode, completedAt);
4488
+ await writeModeState(stateDir, mode, path, recoveredState);
3827
4489
  if (isTrackedWorkflowMode(mode)) {
3828
4490
  await syncCanonicalSkillStateForMode({
3829
4491
  cwd,
@@ -3856,12 +4518,12 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
3856
4518
  : normalizeTerminalWorkflowState(result.state, { mode, nowIso: completedAt });
3857
4519
  if (normalized.changed) {
3858
4520
  result.state = normalized.state;
3859
- await writeFile(path, JSON.stringify(result.state, null, 2));
4521
+ await writeModeState(stateDir, mode, path, result.state);
3860
4522
  }
3861
4523
  if (mode === "ralph") {
3862
4524
  const completedAt = now().toISOString();
3863
4525
  if (markRalphCompletionAuditBlockedForPostLaunch(result.state, cwd, completedAt)) {
3864
- await writeFile(path, JSON.stringify(result.state, null, 2));
4526
+ await writeModeState(stateDir, mode, path, result.state);
3865
4527
  await syncCanonicalSkillStateForMode({
3866
4528
  cwd,
3867
4529
  baseStateDir: rootStateDir,
@@ -3887,7 +4549,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
3887
4549
  result.state.phase = "complete";
3888
4550
  result.state.updated_at = completedAt;
3889
4551
  result.state.active_skills = [];
3890
- await writeFile(path, JSON.stringify(result.state, null, 2));
4552
+ await writeModeState(stateDir, mode, path, result.state);
3891
4553
  continue;
3892
4554
  }
3893
4555
  result.state.active = false;
@@ -3898,7 +4560,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
3898
4560
  result.state.interrupted_at = completedAt;
3899
4561
  result.state.stop_reason = cleanPostLaunchString(result.state.stop_reason) || "session_exit";
3900
4562
  }
3901
- await writeFile(path, JSON.stringify(result.state, null, 2));
4563
+ await writeModeState(stateDir, mode, path, result.state);
3902
4564
  if (isTrackedWorkflowMode(mode)) {
3903
4565
  await syncCanonicalSkillStateForMode({
3904
4566
  cwd,
@@ -3920,7 +4582,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
3920
4582
  if (sessionId) {
3921
4583
  try {
3922
4584
  if (!preserveSkillActiveForReviewPendingAutopilot) {
3923
- await scrubPostLaunchRootSkillActiveForSession(rootStateDir, sessionId, now().toISOString(), writeFile, rootSkillActiveStateBeforeCleanup);
4585
+ await scrubPostLaunchRootSkillActiveForSession(rootStateDir, sessionId, now().toISOString(), writeRootState);
3924
4586
  }
3925
4587
  }
3926
4588
  catch (err) {
@@ -3946,115 +4608,140 @@ export async function reapPostLaunchOrphanedMcpProcesses(dependencies = {}) {
3946
4608
  writeError(`[cli/index] postLaunch MCP cleanup failed: ${err}\n`);
3947
4609
  }
3948
4610
  }
3949
- /**
3950
- * preLaunch: Prepare environment before Codex starts.
3951
- * 1. Best-effort launch-safe orphan cleanup for detached OMX MCP processes
3952
- * 2. Establish the canonical session pointer
3953
- * 3. Generate session-scoped launch artifacts and start best-effort helpers
3954
- *
3955
- * Automatic broad stale-session cleanup remains disabled here. Only detached
3956
- * OMX MCP processes without a live Codex ancestor are reaped so new launches
3957
- * do not accumulate stale processes from prior crashed/closed sessions.
3958
- */
3959
- export async function preLaunch(cwd, sessionId, notifyTempContract, codexHomeOverride, enableNotifyFallbackAuthority = false, worktreeDirty = false) {
3960
- // 1. Best-effort launch-safe orphan cleanup
4611
+ async function establishPreLaunchBinding(cwd, sessionId, metadata = {}) {
4612
+ const pointerOptions = await resolvePreLaunchSessionPointerOptions();
4613
+ const established = await establishLaunchSessionBinding(cwd, sessionId, { ...pointerOptions, ...metadata });
4614
+ if (established.kind === "committed-released")
4615
+ return { binding: established.binding, cleanup: established.cleanup };
4616
+ const error = established.kind === "precommit-aborted" ? established.abort : established.error;
4617
+ Object.assign(error, { establishmentCleanup: established.cleanup });
4618
+ throw error;
4619
+ }
4620
+ async function failPreLaunchSetup(binding) {
4621
+ const failures = [];
4622
+ try {
4623
+ await finalizeBoundOnce(binding, "setup-failure");
4624
+ }
4625
+ catch (error) {
4626
+ failures.push(error);
4627
+ }
4628
+ finally {
4629
+ try {
4630
+ const close = await closeLaunchSessionBindingOnce(binding);
4631
+ if (close.status === "failed")
4632
+ failures.push(close.error ?? new Error("bound session close failed"));
4633
+ }
4634
+ catch (error) {
4635
+ failures.push(error);
4636
+ }
4637
+ }
4638
+ if (failures.length > 0)
4639
+ throw new AggregateError(failures, "preLaunch setup finalization failed");
4640
+ }
4641
+ async function completePreLaunchSetup(cwd, sessionId, notifyTempContract, codexHomeOverride, enableNotifyFallbackAuthority, worktreeDirty) {
4642
+ const degraded = [];
3961
4643
  try {
3962
4644
  const cleanup = await cleanupLaunchOrphanedMcpProcesses();
3963
- if (cleanup.terminatedCount > 0) {
4645
+ if (cleanup.terminatedCount > 0)
3964
4646
  console.log(`[omx] Reaped ${cleanup.terminatedCount} orphaned OMX MCP process(es) before launch.`);
3965
- }
3966
- if (cleanup.failedPids.length > 0) {
4647
+ if (cleanup.failedPids.length > 0)
3967
4648
  console.warn(`[omx] Failed to reap ${cleanup.failedPids.length} orphaned OMX MCP process(es); continuing launch.`);
3968
- }
3969
4649
  }
3970
- catch (err) {
3971
- logCliOperationFailure(err);
3972
- // Non-fatal
4650
+ catch (error) {
4651
+ degraded.push("orphan-reaping");
4652
+ logCliOperationFailure(error);
4653
+ }
4654
+ let instructions;
4655
+ try {
4656
+ const orchestrationMode = await resolveSessionOrchestrationMode(cwd, sessionId);
4657
+ const overlay = await generateOverlay(cwd, sessionId, { orchestrationMode });
4658
+ const launchAppendix = await readLaunchAppendInstructions();
4659
+ const dirtyWorktreeGuidance = worktreeDirty
4660
+ ? `\n\n## Session start: dirty worktree detected\n\nThis worktree has uncommitted changes that were present when the session launched.\nBefore executing the requested task, resolve the dirty state first:\n1. Review uncommitted changes with \`git status\` and \`git diff\`.\n2. Commit, stash, or discard changes as appropriate.\n3. Then proceed with the original task.`
4661
+ : "";
4662
+ instructions = launchAppendix.trim().length > 0 ? `${overlay}\n\n${launchAppendix}${dirtyWorktreeGuidance}` : `${overlay}${dirtyWorktreeGuidance}`;
4663
+ }
4664
+ catch (error) {
4665
+ return { kind: "failure", operation: "overlay", error };
4666
+ }
4667
+ try {
4668
+ await writeSessionModelInstructionsFile(cwd, sessionId, instructions);
4669
+ }
4670
+ catch (error) {
4671
+ return { kind: "failure", operation: "session-instructions", error };
4672
+ }
4673
+ try {
4674
+ await resetSessionMetrics(cwd, sessionId);
4675
+ tagCurrentTmuxSessionWithInstance(sessionId);
4676
+ }
4677
+ catch (error) {
4678
+ return { kind: "failure", operation: "session-metrics", error };
3973
4679
  }
3974
- // 2. Establish the canonical pointer before any session-scoped launch artifact.
3975
- await writeSessionStart(cwd, sessionId, await resolvePreLaunchSessionPointerOptions());
3976
- // 3. Generate runtime overlay + write session-scoped model instructions file
3977
- const orchestrationMode = await resolveSessionOrchestrationMode(cwd, sessionId);
3978
- const overlay = await generateOverlay(cwd, sessionId, { orchestrationMode });
3979
- const launchAppendix = await readLaunchAppendInstructions();
3980
- const dirtyWorktreeGuidance = worktreeDirty
3981
- ? `\n\n## Session start: dirty worktree detected\n\nThis worktree has uncommitted changes that were present when the session launched.\nBefore executing the requested task, resolve the dirty state first:\n1. Review uncommitted changes with \`git status\` and \`git diff\`.\n2. Commit, stash, or discard changes as appropriate.\n3. Then proceed with the original task.`
3982
- : "";
3983
- const sessionInstructions = launchAppendix.trim().length > 0
3984
- ? `${overlay}
3985
-
3986
- ${launchAppendix}${dirtyWorktreeGuidance}`
3987
- : `${overlay}${dirtyWorktreeGuidance}`;
3988
- await writeSessionModelInstructionsFile(cwd, sessionId, sessionInstructions);
3989
- // 4. Reset session metrics and tag the established session.
3990
- await resetSessionMetrics(cwd, sessionId);
3991
- tagCurrentTmuxSessionWithInstance(sessionId);
3992
- // 5. Start notify fallback watcher (best effort)
3993
4680
  try {
3994
4681
  await startNotifyFallbackWatcher(cwd, { codexHomeOverride, enableAuthority: enableNotifyFallbackAuthority, sessionId });
3995
4682
  }
3996
- catch (err) {
3997
- logCliOperationFailure(err);
3998
- // Non-fatal
4683
+ catch (error) {
4684
+ degraded.push("notify-watcher");
4685
+ logCliOperationFailure(error);
3999
4686
  }
4000
- // 6. Start derived watcher (best effort, opt-in)
4001
4687
  try {
4002
4688
  await startHookDerivedWatcher(cwd);
4003
4689
  }
4004
- catch (err) {
4005
- logCliOperationFailure(err);
4006
- // Non-fatal
4690
+ catch (error) {
4691
+ degraded.push("derived-watcher");
4692
+ logCliOperationFailure(error);
4007
4693
  }
4008
- // 7. Emit temp notification startup summary + warnings, then send session-start lifecycle notification (best effort)
4009
4694
  try {
4010
4695
  if (notifyTempContract?.active) {
4011
- process.env[OMX_NOTIFY_TEMP_CONTRACT_ENV] =
4012
- serializeNotifyTempContract(notifyTempContract);
4696
+ process.env[OMX_NOTIFY_TEMP_CONTRACT_ENV] = serializeNotifyTempContract(notifyTempContract);
4013
4697
  const { getNotificationConfig } = await import("../notifications/config.js");
4014
- const resolved = getNotificationConfig();
4015
- const startup = buildNotifyTempStartupMessages(notifyTempContract, Boolean(resolved?.enabled));
4016
- for (const info of startup.infoLines) {
4698
+ const startup = buildNotifyTempStartupMessages(notifyTempContract, Boolean(getNotificationConfig()?.enabled));
4699
+ for (const info of startup.infoLines)
4017
4700
  console.log(`[omx] ${info}`);
4018
- }
4019
- for (const warning of startup.warningLines) {
4701
+ for (const warning of startup.warningLines)
4020
4702
  console.warn(`[omx] ${warning}`);
4021
- }
4022
4703
  }
4023
- else {
4704
+ else
4024
4705
  delete process.env[OMX_NOTIFY_TEMP_CONTRACT_ENV];
4025
- }
4026
4706
  const { notifyLifecycle } = await import("../notifications/index.js");
4027
- await notifyLifecycle("session-start", {
4028
- sessionId,
4029
- projectPath: cwd,
4030
- projectName: basename(cwd),
4031
- });
4707
+ await notifyLifecycle("session-start", { sessionId, projectPath: cwd, projectName: basename(cwd) });
4032
4708
  }
4033
- catch (err) {
4034
- logCliOperationFailure(err);
4035
- // Non-fatal: notification failures must never block launch
4709
+ catch (error) {
4710
+ degraded.push("notification");
4711
+ logCliOperationFailure(error);
4036
4712
  }
4037
- // 8. Dispatch native hook event (best effort)
4038
4713
  try {
4039
- await emitNativeHookEvent(cwd, "session-start", {
4040
- session_id: sessionId,
4041
- context: buildNativeHookBaseContext(cwd, sessionId, "started", {
4042
- project_path: cwd,
4043
- project_name: basename(cwd),
4044
- status: "started",
4045
- }),
4046
- });
4714
+ await emitNativeHookEvent(cwd, "session-start", { session_id: sessionId, context: buildNativeHookBaseContext(cwd, sessionId, "started", { project_path: cwd, project_name: basename(cwd), status: "started" }) });
4047
4715
  }
4048
- catch (err) {
4049
- logCliOperationFailure(err);
4050
- // Non-fatal
4716
+ catch (error) {
4717
+ degraded.push("native-lifecycle-event");
4718
+ logCliOperationFailure(error);
4719
+ }
4720
+ return degraded.length === 0 ? { kind: "success" } : { kind: "degraded-success", operations: degraded };
4721
+ }
4722
+ export async function preLaunch(cwd, sessionId, notifyTempContract, codexHomeOverride, enableNotifyFallbackAuthority = false, worktreeDirty = false) {
4723
+ const established = await establishPreLaunchBinding(cwd, sessionId);
4724
+ const completion = {
4725
+ ...await completePreLaunchSetup(cwd, sessionId, notifyTempContract, codexHomeOverride, enableNotifyFallbackAuthority, worktreeDirty),
4726
+ establishmentCleanup: established.cleanup,
4727
+ };
4728
+ if (completion.kind === "failure") {
4729
+ try {
4730
+ await failPreLaunchSetup(established.binding);
4731
+ }
4732
+ catch (cleanupError) {
4733
+ throw new AggregateError([completion.error, cleanupError], `preLaunch ${completion.operation} failed`);
4734
+ }
4735
+ throw completion.error;
4051
4736
  }
4737
+ return { binding: established.binding, completion };
4052
4738
  }
4053
4739
  /**
4054
4740
  * runCodex: Launch Codex CLI (blocks until exit).
4055
4741
  * All 3 paths (new tmux, existing tmux, no tmux) block via execSync/execFileSync.
4056
4742
  */
4057
- function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, sqliteHomeOverride, notifyTempContractRaw, explicitLaunchPolicy, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, runtimeContext) {
4743
+ async function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, sqliteHomeOverride, notifyTempContractRaw, preLaunchOptions, launchPolicy = "direct", detachedPreflight, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, runtimeContext, verifiedMadmaxContext, launchControlPlane) {
4744
+ void preLaunchOptions;
4058
4745
  const launchArgs = injectModelInstructionsBypassArgs(cwd, args, process.env, sessionModelInstructionsPath(cwd, sessionId));
4059
4746
  const nativeWindows = isNativeWindows();
4060
4747
  const omxBin = resolveOmxCliEntryPath({ argv1: process.argv[1], cwd, env: process.env });
@@ -4062,19 +4749,59 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
4062
4749
  throw new Error("Unable to resolve OMX launcher path for tmux HUD bootstrap");
4063
4750
  }
4064
4751
  const runtimeEnvOverlay = buildMadmaxWorktreeRuntimeEnvOverlay(runtimeContext);
4752
+ let verifiedTeamContext;
4753
+ if (launchPolicy === "detached-tmux") {
4754
+ try {
4755
+ verifiedTeamContext = await resolveVerifiedDetachedTeamContext(cwd, process.env);
4756
+ }
4757
+ catch {
4758
+ verifiedTeamContext = undefined;
4759
+ }
4760
+ }
4761
+ const verifiedMadmaxContextForLaunch = verifiedMadmaxContext ?? (runtimeContext?.madmaxDetachedContext
4762
+ ? {
4763
+ root: runtimeContext.omxRoot,
4764
+ sourceCwd: runtimeContext.sourceCwd,
4765
+ context: runtimeContext.madmaxDetachedContext,
4766
+ runsRoot: resolveMadmaxRunsRoot(process.env),
4767
+ }
4768
+ : resolveVerifiedMadmaxDetachedContext(cwd, launchArgs, process.env));
4769
+ const detachedControlPlane = launchPolicy === "detached-tmux"
4770
+ ? buildDetachedLaunchControlPlane({
4771
+ cwd,
4772
+ sessionId,
4773
+ env: process.env,
4774
+ omxRootOverride: runtimeContext?.omxRoot ?? resolveOmxRootForLaunch(cwd, process.env),
4775
+ runtimeContext,
4776
+ verifiedMadmaxContext: verifiedMadmaxContextForLaunch,
4777
+ verifiedTeamContext,
4778
+ })
4779
+ : undefined;
4780
+ const launchOwnedControlPlane = detachedControlPlane ?? launchControlPlane;
4781
+ const launchOwnedControlPlaneEnv = launchOwnedControlPlane
4782
+ ? detachedLaunchControlPlaneEnv(launchOwnedControlPlane)
4783
+ : {};
4784
+ const detachedSelectedStateEnv = detachedControlPlane
4785
+ ? { ...process.env, ...detachedLaunchControlPlaneEnv(detachedControlPlane) }
4786
+ : undefined;
4065
4787
  const omxRootOverride = runtimeContext?.omxRoot ?? resolveOmxRootForLaunch(cwd, process.env);
4788
+ const selectedOmxRootOverride = launchOwnedControlPlane
4789
+ ? launchOwnedControlPlane.values.OMX_ROOT || undefined
4790
+ : omxRootOverride;
4066
4791
  const currentPaneId = process.env.TMUX_PANE;
4067
4792
  const hudRuntimeRoot = runtimeContext
4068
4793
  ? { omxRoot: runtimeContext.omxRoot, rootSource: 'omx-root-env' }
4069
4794
  : resolveHudRuntimeRootForLaunch(cwd, process.env);
4070
- const hudRuntimeEnv = {
4071
- ...buildHudRuntimeEnv({
4072
- sessionId,
4073
- leaderPaneId: currentPaneId,
4074
- ...hudRuntimeRoot,
4075
- }).env,
4076
- ...runtimeEnvOverlay,
4077
- };
4795
+ const hudRuntimeEnv = launchOwnedControlPlane
4796
+ ? Object.fromEntries(Object.entries(launchOwnedControlPlaneEnv))
4797
+ : {
4798
+ ...buildHudRuntimeEnv({
4799
+ sessionId,
4800
+ leaderPaneId: currentPaneId,
4801
+ ...hudRuntimeRoot,
4802
+ }).env,
4803
+ ...runtimeEnvOverlay,
4804
+ };
4078
4805
  const hudEnvArgs = Object.entries(hudRuntimeEnv).map(([key, value]) => `${key}=${value}`);
4079
4806
  const hudCmd = nativeWindows
4080
4807
  ? buildWindowsPromptCommand("node", [omxBin, "hud", "--watch"])
@@ -4089,13 +4816,17 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
4089
4816
  ...stripHermesMcpBridgeEnv(process.env),
4090
4817
  ...(codexHomeOverride ? { CODEX_HOME: codexHomeOverride } : {}),
4091
4818
  ...(sqliteHomeOverride ? { [CODEX_SQLITE_HOME_ENV]: sqliteHomeOverride } : {}),
4092
- ...(omxRootOverride ? { OMX_ROOT: omxRootOverride } : {}),
4819
+ ...(!launchOwnedControlPlane && omxRootOverride ? { OMX_ROOT: omxRootOverride } : {}),
4093
4820
  ...runtimeEnvOverlay,
4821
+ ...launchOwnedControlPlaneEnv,
4094
4822
  }, omxBin);
4823
+ // Correlates plugin hook routing only; it is not an authority token.
4824
+ const pluginHookRoutingLaunchId = randomUUID();
4095
4825
  const codexEnvWithSession = {
4096
4826
  ...codexBaseEnv,
4097
- OMX_CODEX_LAUNCH_ID: randomUUID(),
4827
+ OMX_CODEX_LAUNCH_ID: pluginHookRoutingLaunchId,
4098
4828
  ...buildHudRuntimeEnv({ sessionId }).env,
4829
+ ...launchOwnedControlPlaneEnv,
4099
4830
  };
4100
4831
  const codexEnv = workerLaunchArgs
4101
4832
  ? {
@@ -4107,8 +4838,9 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
4107
4838
  const codexEnvWithNotify = notifyTempContractRaw
4108
4839
  ? { ...codexEnv, [OMX_NOTIFY_TEMP_CONTRACT_ENV]: notifyTempContractRaw }
4109
4840
  : codexEnv;
4110
- const runtimeHookEnv = { ...process.env, ...runtimeEnvOverlay };
4111
- const { launchPolicy } = resolveTmuxAwareLaunchPolicy(explicitLaunchPolicy, nativeWindows);
4841
+ const runtimeHookEnv = launchOwnedControlPlane
4842
+ ? { ...process.env, ...runtimeEnvOverlay, ...launchOwnedControlPlaneEnv }
4843
+ : { ...process.env, ...runtimeEnvOverlay };
4112
4844
  if (isCodexVersionRequest(launchArgs)) {
4113
4845
  runCodexBlocking(cwd, launchArgs, codexEnvWithNotify);
4114
4846
  return { postLaunchHandledExternally: false };
@@ -4144,7 +4876,7 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
4144
4876
  currentPaneId,
4145
4877
  cwd,
4146
4878
  sessionId,
4147
- omxRootOverride,
4879
+ omxRootOverride: selectedOmxRootOverride,
4148
4880
  baseEnv: runtimeHookEnv,
4149
4881
  });
4150
4882
  }
@@ -4171,7 +4903,7 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
4171
4903
  currentPaneId,
4172
4904
  cwd,
4173
4905
  sessionId,
4174
- omxRootOverride,
4906
+ omxRootOverride: selectedOmxRootOverride,
4175
4907
  baseEnv: runtimeHookEnv,
4176
4908
  });
4177
4909
  }
@@ -4232,214 +4964,247 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
4232
4964
  return { postLaunchHandledExternally: false };
4233
4965
  }
4234
4966
  else {
4235
- // Not in tmux: create a new tmux session with codex + HUD pane
4967
+ if (!detachedPreflight) {
4968
+ throw new DetachedLaunchSafetyError("establishment", new Error("detached launch requires a frozen available preflight"), { transitions: ["D0"], rollback: { attempted: [], failures: [] } });
4969
+ }
4236
4970
  const codexCmd = buildTmuxPaneCommand("codex", launchArgs);
4237
4971
  const detachedWindowsCodexCmd = nativeWindows
4238
- ? buildWindowsPromptCommand("codex", launchArgs)
4972
+ ? buildWindowsDetachedChildCommand("codex", launchArgs)
4239
4973
  : null;
4240
4974
  const sessionName = buildDetachedTmuxSessionName(cwd, sessionId);
4241
- const launchDetachedSession = () => {
4242
- const contextKey = runtimeContext?.madmaxDetachedContext ?? process.env[OMX_MADMAX_DETACHED_CONTEXT_ENV]?.trim();
4243
- const runsRoot = resolveMadmaxRunsRoot(process.env);
4244
- const activeRecordPath = contextKey
4245
- ? madmaxDetachedActiveRecordPath(runsRoot, contextKey)
4246
- : null;
4247
- const activeRecord = activeRecordPath
4248
- ? readMadmaxDetachedActiveRecord(activeRecordPath)
4249
- : null;
4250
- if (activeRecord &&
4251
- activeRecord.context_key === contextKey &&
4252
- isReusableMadmaxDetachedActiveRecord(activeRecord)) {
4253
- cleanupCurrentMadmaxReuseRunRoot(process.env, runsRoot);
4254
- setDetachedTmuxSessionHistoryLimit(activeRecord.tmux_session_name, activeRecord.tmux_pane_id);
4255
- if (!shouldAttachDetachedTmuxSession(process.env)) {
4256
- clearDetachedTmuxSessionHistoryIfUnattached(activeRecord.tmux_session_name, activeRecord.tmux_pane_id);
4257
- process.stderr.write(`[omx] madmax detached launch already active for this context; reusing ${activeRecord.tmux_session_name} without attaching because this launch is a Hermes MCP bridge.\n`);
4258
- return { postLaunchHandledExternally: true };
4259
- }
4260
- process.stderr.write(`[omx] madmax detached launch already active for this context; attaching ${activeRecord.tmux_session_name} instead of starting a duplicate.\n`);
4261
- try {
4262
- execTmuxFileSync(["attach-session", "-t", activeRecord.tmux_session_name], {
4263
- stdio: "inherit",
4264
- });
4265
- }
4266
- catch (err) {
4267
- logCliOperationFailure(err);
4268
- throw new MadmaxDetachedReuseError(`refusing duplicate madmax detached launch: existing session ${activeRecord.tmux_session_name} is active but attach failed`);
4269
- }
4270
- return { postLaunchHandledExternally: true };
4271
- }
4272
- if (activeRecordPath && activeRecord) {
4273
- rmSync(activeRecordPath, { force: true });
4274
- }
4275
- let detachedSessionBindingWrite = Promise.resolve();
4276
- const writeDetachedSessionBinding = (tmuxPaneId) => {
4277
- detachedSessionBindingWrite = detachedSessionBindingWrite
4278
- .catch((err) => {
4279
- logCliOperationFailure(err);
4280
- })
4281
- .then(() => writeSessionStart(cwd, sessionId, {
4282
- tmuxSessionName: sessionName,
4283
- ...(tmuxPaneId ? { tmuxPaneId } : {}),
4284
- }));
4285
- void detachedSessionBindingWrite.catch((err) => {
4286
- logCliOperationFailure(err);
4287
- // Non-fatal: managed tmux recovery can still use compatibility fallback.
4288
- });
4289
- };
4290
- writeDetachedSessionBinding();
4291
- let createdDetachedSession = false;
4292
- let registeredHookTarget = null;
4293
- let registeredHookName = null;
4294
- let registeredClientAttachedHookName = null;
4295
- let detachedParentEnvFilePath;
4296
- let detachedLeaderPaneId = null;
4297
- try {
4298
- // This path is the user-shell interactive launch: OMX creates a tmux
4299
- // session and immediately attaches the user's terminal to it. If a tmux
4300
- // server already exists, `new-session -e` only forwards explicit values,
4301
- // so provider-specific parent-shell keys would disappear. Source a
4302
- // private env file inside the leader shell instead of putting every
4303
- // parent env value on the tmux command line or in logs.
4304
- if (!nativeWindows) {
4975
+ const contextKey = detachedControlPlane?.values.OMX_MADMAX_DETACHED_CONTEXT.trim() || undefined;
4976
+ const runsRoot = detachedControlPlane?.values.OMX_RUNS_DIR || resolveMadmaxRunsRoot(process.env);
4977
+ const detachedLaunchNonce = randomUUID();
4978
+ const releaseMarkerPath = join(omxRoot(cwd), "runtime", "detached-release", `${sessionId}.${detachedLaunchNonce}.release`);
4979
+ let detachedParentEnvFilePath;
4980
+ let detachedLeaderPaneId = null;
4981
+ let detachedLeaderAuthority = null;
4982
+ let detachedHudAuthority = null;
4983
+ let detachedLeaderPid = null;
4984
+ let rollbackFromPreReportAuthority = false;
4985
+ let attachStep = null;
4986
+ const launchDetachedSession = async () => {
4987
+ let createdSession = false;
4988
+ const bootstrapLeader = async () => {
4989
+ if (!nativeWindows)
4305
4990
  detachedParentEnvFilePath = writeDetachedSessionParentEnvFile(cwd, sessionId, codexEnvWithNotify);
4306
- }
4307
- const bootstrapSteps = buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, hudCmd, workerLaunchArgs, codexHomeOverride, notifyTempContractRaw, nativeWindows, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, omxRootOverride, runtimeHookEnv, sqliteHomeOverride, detachedParentEnvFilePath, inheritedWorkerModel);
4991
+ const bootstrapSteps = buildDetachedSessionBootstrapSteps(sessionName, cwd, nativeWindows && detachedWindowsCodexCmd ? detachedWindowsCodexCmd : codexCmd, hudCmd, workerLaunchArgs, codexHomeOverride, notifyTempContractRaw, nativeWindows, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, omxRootOverride, codexEnvWithNotify, sqliteHomeOverride, detachedParentEnvFilePath, inheritedWorkerModel, releaseMarkerPath, omxBin, {
4992
+ ...(preLaunchOptions.notifyTempContract.active ? { notifyTempContract: preLaunchOptions.notifyTempContract } : {}),
4993
+ enableNotifyFallbackAuthority: preLaunchOptions.enableNotifyFallbackAuthority,
4994
+ worktreeDirty: preLaunchOptions.worktreeDirty,
4995
+ shouldAttach: detachedPreflight.shouldAttach,
4996
+ }, detachedControlPlane);
4308
4997
  for (const step of bootstrapSteps) {
4309
- const output = execTmuxFileSync(step.args, {
4310
- stdio: "pipe",
4311
- encoding: "utf-8",
4312
- });
4313
- if (step.name === "new-session") {
4314
- createdDetachedSession = true;
4315
- const leaderPaneId = parsePaneIdFromTmuxOutput(output || "");
4316
- if (leaderPaneId) {
4998
+ try {
4999
+ if (step.name === "new-session") {
5000
+ const output = execTmuxFileSync(step.args, { stdio: "pipe", encoding: "utf-8" });
5001
+ createdSession = true;
5002
+ const leaderPaneId = parsePaneIdFromTmuxOutput(output || "");
5003
+ if (!leaderPaneId)
5004
+ throw new Error("detached session did not report a leader pane id");
4317
5005
  detachedLeaderPaneId = leaderPaneId;
4318
- setDetachedTmuxSessionHistoryLimit(sessionName, leaderPaneId);
4319
- if (activeRecordPath && contextKey) {
4320
- writeMadmaxDetachedActiveRecord(activeRecordPath, {
4321
- version: 1,
4322
- context_key: contextKey,
4323
- created_at: new Date().toISOString(),
4324
- source_cwd: runtimeContext?.sourceCwd ?? process.env.OMX_SOURCE_CWD ?? cwd,
4325
- ...(runtimeContext?.worktreeCwd ? { worktree_cwd: runtimeContext.worktreeCwd } : {}),
4326
- argv: args,
4327
- run_dir: runtimeContext?.omxRoot ?? process.env.OMX_ROOT ?? cwd,
4328
- tmux_session_name: sessionName,
4329
- session_id: sessionId,
4330
- tmux_pane_id: leaderPaneId,
4331
- });
4332
- }
4333
- writeDetachedSessionBinding(leaderPaneId);
5006
+ detachedLeaderAuthority = captureDetachedLeaderAuthority(leaderPaneId, sessionName, sessionId);
5007
+ continue;
4334
5008
  }
4335
- }
4336
- if (step.name === "split-and-capture-hud-pane") {
4337
- const hudPaneId = parsePaneIdFromTmuxOutput(output || "");
4338
- const hookWindowIndex = hudPaneId
4339
- ? detectDetachedSessionWindowIndex(sessionName)
4340
- : null;
4341
- const hookTarget = hudPaneId && hookWindowIndex
4342
- ? buildResizeHookTarget(sessionName, hookWindowIndex)
4343
- : null;
4344
- const hookName = hudPaneId && hookWindowIndex
4345
- ? buildResizeHookName("launch", sessionName, hookWindowIndex, hudPaneId)
4346
- : null;
4347
- const clientAttachedHookName = hudPaneId && hookWindowIndex
4348
- ? buildClientAttachedReconcileHookName("launch", sessionName, hookWindowIndex, hudPaneId)
4349
- : null;
4350
- const finalizeSteps = buildDetachedSessionFinalizeSteps(sessionName, hudPaneId, hookWindowIndex, process.env.OMX_MOUSE !== "0", nativeWindows, shouldAttachDetachedTmuxSession(process.env), detachedLeaderPaneId);
4351
- if (nativeWindows && detachedWindowsCodexCmd) {
4352
- scheduleDetachedWindowsCodexLaunch(sessionName, detachedWindowsCodexCmd);
5009
+ const authority = detachedLeaderAuthority;
5010
+ if (!authority)
5011
+ throw new Error("detached leader authority missing before tmux mutation");
5012
+ if (step.name === "tag-session") {
5013
+ runDetachedLeaderMutation(authority, step.args, false);
5014
+ runDetachedLeaderMutation(authority, ["set-option", "-q", "-t", authority.sessionName, "history-limit", String(DETACHED_TMUX_HISTORY_LIMIT)]);
5015
+ runDetachedLeaderMutation(authority, ["set-option", "-pq", "-t", authority.paneId, "history-limit", String(DETACHED_TMUX_HISTORY_LIMIT)]);
5016
+ // #3266: the owned leader pane must close with its process so a normal
5017
+ // child exit destroys the session naturally even when the user's tmux
5018
+ // config inherits remain-on-exit=on/failed.
5019
+ runDetachedLeaderMutation(authority, ["set-option", "-pq", "-t", authority.paneId, "remain-on-exit", "off"]);
5020
+ continue;
4353
5021
  }
4354
- for (const finalizeStep of finalizeSteps) {
4355
- if (finalizeStep.name === "sanitize-copy-mode-style") {
4356
- try {
4357
- mitigateCopyModeUnderlineArtifacts(sessionName);
4358
- }
4359
- catch (err) {
4360
- logCliOperationFailure(err);
4361
- }
5022
+ if (step.name !== "split-and-capture-hud-pane") {
5023
+ throw new Error(`unexpected detached bootstrap mutation: ${step.name}`);
5024
+ }
5025
+ detachedHudAuthority = runDetachedLeaderSplit(authority, step.args);
5026
+ tagDetachedHudPane(authority, detachedHudAuthority, sessionId);
5027
+ for (const finalizeStep of buildDetachedSessionFinalizeSteps(authority.sessionName, detachedHudAuthority.paneId, authority.windowIndex, process.env.OMX_MOUSE !== "0", nativeWindows, detachedPreflight.shouldAttach, authority.paneId)) {
5028
+ if (finalizeStep.name === "attach-session") {
5029
+ attachStep = finalizeStep;
4362
5030
  continue;
4363
5031
  }
4364
- const stdio = finalizeStep.name === "attach-session" ? "inherit" : "ignore";
4365
- try {
4366
- const startedAtMs = Date.now();
4367
- execTmuxFileSync(finalizeStep.args, { stdio });
4368
- if (finalizeStep.name === "attach-session") {
4369
- assertDetachedAttachDidNotNoop(sessionName, Date.now() - startedAtMs, process.env);
4370
- }
4371
- }
4372
- catch (err) {
4373
- logCliOperationFailure(err);
4374
- if (finalizeStep.name === "attach-session")
4375
- throw new Error("failed to attach detached tmux session");
5032
+ if (finalizeStep.name === "sanitize-copy-mode-style")
4376
5033
  continue;
4377
- }
4378
- if (finalizeStep.name === "register-resize-hook" &&
4379
- hookTarget &&
4380
- hookName) {
4381
- registeredHookTarget = hookTarget;
4382
- registeredHookName = hookName;
4383
- }
4384
- if (finalizeStep.name === "register-client-attached-reconcile" &&
4385
- clientAttachedHookName) {
4386
- registeredClientAttachedHookName = clientAttachedHookName;
4387
- }
4388
- if (finalizeStep.name === "reconcile-hud-resize") {
4389
- registerDetachedHudLayoutReconcileHook({
4390
- hudPaneId,
4391
- detachedLeaderPaneId,
4392
- cwd,
4393
- sessionId,
4394
- omxBin,
4395
- omxRootOverride,
4396
- baseEnv: runtimeHookEnv,
4397
- });
4398
- }
5034
+ const targetsHudPane = new Set([
5035
+ "register-resize-hook",
5036
+ "register-client-attached-reconcile",
5037
+ "schedule-delayed-resize",
5038
+ "reconcile-hud-resize",
5039
+ ]).has(finalizeStep.name);
5040
+ if (targetsHudPane)
5041
+ runDetachedHudMutation(authority, detachedHudAuthority, guardDetachedHudDeferredMutation(authority, detachedHudAuthority, finalizeStep.args));
5042
+ else
5043
+ runDetachedLeaderMutation(authority, finalizeStep.args);
4399
5044
  }
4400
5045
  }
5046
+ catch (error) {
5047
+ throw new DetachedLaunchSafetyError(step.name === "new-session" ? "inert-session" : "pane-id", error, detachedPreflight.report);
5048
+ }
4401
5049
  }
4402
- return { postLaunchHandledExternally: !nativeWindows };
4403
- }
4404
- catch (err) {
4405
- if (detachedParentEnvFilePath) {
4406
- rmSync(detachedParentEnvFilePath, { force: true });
4407
- }
4408
- if (activeRecordPath) {
4409
- rmSync(activeRecordPath, { force: true });
4410
- }
4411
- if (createdDetachedSession) {
4412
- const rollbackSteps = buildDetachedSessionRollbackSteps(sessionName, registeredHookTarget, registeredHookName, registeredClientAttachedHookName);
4413
- for (const rollbackStep of rollbackSteps) {
5050
+ return undefined;
5051
+ };
5052
+ const launchTerminal = await executeDetachedLaunchStateMachine({ preflight: detachedPreflight }, {
5053
+ establish: bootstrapLeader,
5054
+ complete: async () => {
5055
+ const readyDeadline = Date.now() + DETACHED_LEADER_READY_TIMEOUT_MS;
5056
+ while (true) {
5057
+ const report = readDetachedLeaderReport(releaseMarkerPath);
5058
+ if (isDetachedReadyReportAuthorized(report, {
5059
+ nonce: detachedLaunchNonce,
5060
+ sessionId,
5061
+ sessionName,
5062
+ shouldAttach: detachedPreflight.shouldAttach,
5063
+ leaderPaneId: detachedLeaderPaneId,
5064
+ leaderPanePid: detachedLeaderAuthority?.panePid ?? null,
5065
+ })) {
5066
+ detachedLeaderPid = report.leaderPid;
5067
+ return { kind: "success" };
5068
+ }
5069
+ if (report?.nonce === detachedLaunchNonce && report.sessionId === sessionId && report.sessionName === sessionName && report.leaderPid && report.kind === "failed") {
5070
+ detachedLeaderPid = report.leaderPid;
5071
+ return { kind: "failure", operation: "session-instructions", error: new Error(report.error || "detached leader setup failed") };
5072
+ }
5073
+ if (Date.now() >= readyDeadline)
5074
+ return { kind: "failure", operation: "session-instructions", error: new Error("detached leader readiness timed out") };
5075
+ blockMs(20);
5076
+ }
5077
+ },
5078
+ createInertSession: async () => {
5079
+ const report = readDetachedLeaderReport(releaseMarkerPath);
5080
+ if (report?.kind !== "ready" || report.nonce !== detachedLaunchNonce || report.sessionId !== sessionId || report.sessionName !== sessionName || report.leaderPid !== detachedLeaderPid) {
5081
+ throw new Error("detached ready report does not bind the inert session");
5082
+ }
5083
+ return sessionName;
5084
+ },
5085
+ capturePane: async () => {
5086
+ const report = readDetachedLeaderReport(releaseMarkerPath);
5087
+ if (!detachedLeaderPaneId || report?.kind !== "ready" || report.paneId !== detachedLeaderPaneId || report.leaderPid !== detachedLeaderPid) {
5088
+ throw new Error("detached ready report does not bind the leader pane");
5089
+ }
5090
+ return detachedLeaderPaneId;
5091
+ },
5092
+ updateNameMetadata: async () => {
5093
+ const state = await readSessionState(cwd, detachedSelectedStateEnv);
5094
+ if (state?.session_id !== sessionId || state.tmux_session_name !== sessionName) {
5095
+ throw new Error("detached session-name metadata was not committed by the leader");
5096
+ }
5097
+ return "committed-released";
5098
+ },
5099
+ updatePaneMetadata: async (_binding, pane) => {
5100
+ const state = await readSessionState(cwd, detachedSelectedStateEnv);
5101
+ if (state?.session_id !== sessionId || state.tmux_pane_id !== pane) {
5102
+ throw new Error("detached pane metadata was not committed by the leader");
5103
+ }
5104
+ return "committed-released";
5105
+ },
5106
+ publishActiveRecord: async () => {
5107
+ const activeRecordPath = contextKey ? madmaxDetachedActiveRecordPath(runsRoot, contextKey) : join(omxRoot(cwd), "state", "detached-active-record.json");
5108
+ const record = readMadmaxDetachedActiveRecord(activeRecordPath);
5109
+ if (!record || record.launch_nonce !== detachedLaunchNonce || record.leader_pid !== detachedLeaderPid || record.session_id !== sessionId || record.tmux_session_name !== sessionName || record.tmux_pane_id !== detachedLeaderPaneId)
5110
+ throw new Error("detached active record does not bind the ready leader");
5111
+ const bytes = readFileSync(activeRecordPath, "utf-8");
5112
+ return { bytes, digest: createHash("sha256").update(bytes).digest("hex"), nonce: detachedLaunchNonce };
5113
+ },
5114
+ finalizeSetupFailure: async () => { },
5115
+ releaseBarrier: async () => {
5116
+ if (!detachedLeaderPid)
5117
+ throw new Error("detached leader PID missing before release");
5118
+ publishDetachedReleaseMarker(releaseMarkerPath, detachedLaunchNonce, sessionId, sessionName, detachedLeaderPid, detachedHudAuthority ?? undefined);
5119
+ },
5120
+ abortAndAwaitFinalization: async () => {
5121
+ // The leader has the retained binding. Publishing abort is the only
5122
+ // outer action permitted after a D9 write failure; a mismatched or
5123
+ // missing report leaves every artifact and the tmux session intact.
5124
+ if (!detachedLeaderPid) {
5125
+ rollbackFromPreReportAuthority = detachedLeaderAuthority !== null;
5126
+ return { acknowledged: false, rollbackAuthorized: rollbackFromPreReportAuthority };
5127
+ }
5128
+ const current = readDetachedLeaderReport(releaseMarkerPath);
5129
+ if (current?.nonce === detachedLaunchNonce && current.sessionId === sessionId &&
5130
+ current.sessionName === sessionName && current.leaderPid === detachedLeaderPid &&
5131
+ current.finalized === true && (current.kind === "failed" || current.kind === "terminal"))
5132
+ return {
5133
+ acknowledged: true,
5134
+ nonce: current.nonce,
5135
+ sessionId: current.sessionId,
5136
+ sessionName: current.sessionName,
5137
+ leaderPid: current.leaderPid,
5138
+ kind: current.kind,
5139
+ };
5140
+ publishDetachedAbortMarker(releaseMarkerPath, detachedLaunchNonce, sessionId, sessionName, detachedLeaderPid);
5141
+ const deadline = Date.now() + 30_000;
5142
+ while (Date.now() < deadline) {
5143
+ const report = readDetachedLeaderReport(releaseMarkerPath);
5144
+ if (report?.nonce === detachedLaunchNonce && report.sessionId === sessionId &&
5145
+ report.sessionName === sessionName && report.leaderPid === detachedLeaderPid &&
5146
+ report.finalized === true && (report.kind === "failed" || report.kind === "terminal"))
5147
+ return {
5148
+ acknowledged: true,
5149
+ nonce: report.nonce,
5150
+ sessionId: report.sessionId,
5151
+ sessionName: report.sessionName,
5152
+ leaderPid: report.leaderPid,
5153
+ kind: report.kind,
5154
+ };
5155
+ blockMs(20);
5156
+ }
5157
+ return { acknowledged: false };
5158
+ },
5159
+ attachOrReturn: async () => { if (attachStep)
5160
+ execTmuxFileSync(attachStep.args, { stdio: "inherit" }); },
5161
+ rollback: async (_ownedRecord, report) => {
5162
+ const attempt = async (step, operation) => {
5163
+ report.rollback.attempted.push(step);
4414
5164
  try {
4415
- execTmuxFileSync(rollbackStep.args, { stdio: "ignore" });
5165
+ await operation();
4416
5166
  }
4417
- catch (rollbackErr) {
4418
- logCliOperationFailure(rollbackErr);
4419
- // best-effort rollback only
5167
+ catch (error) {
5168
+ report.rollback.failures.push({ step, status: "failed", code: detachedFailureCode(error) });
5169
+ }
5170
+ };
5171
+ await attempt("parent-env", async () => { if (detachedParentEnvFilePath)
5172
+ await rm(detachedParentEnvFilePath, { force: true }); });
5173
+ await attempt("runtime-codex-home", () => cleanupRuntimeCodexHome(runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup));
5174
+ await attempt("rollback", () => { rmSync(releaseMarkerPath, { force: true }); rmSync(`${releaseMarkerPath}.release`, { force: true }); rmSync(`${releaseMarkerPath}.abort`, { force: true }); });
5175
+ if (createdSession) {
5176
+ for (const step of buildDetachedSessionRollbackSteps(sessionName, null, null, null)) {
5177
+ await attempt(`session:${step.name}`, () => {
5178
+ if (!detachedLeaderAuthority)
5179
+ throw new Error("detached leader authority missing before rollback");
5180
+ if (rollbackFromPreReportAuthority && step.name === "kill-session") {
5181
+ cleanupDetachedPreReportSession(detachedLeaderAuthority);
5182
+ return;
5183
+ }
5184
+ runDetachedLeaderMutation(detachedLeaderAuthority, step.args);
5185
+ });
4420
5186
  }
4421
5187
  }
5188
+ },
5189
+ });
5190
+ if (launchTerminal.kind === "attached" && detachedLeaderPid) {
5191
+ // #3266: propagate the finalized detached child's exit status to the invoking shell.
5192
+ // A rejected status protocol is only benign when the exact owned session
5193
+ // provably survives (manual detach); a destroyed or ambiguous session with
5194
+ // no authenticated finalized status is a protocol failure, never silent success.
5195
+ const attachResolution = resolveDetachedAttachExitStatus(releaseMarkerPath, { nonce: detachedLaunchNonce, sessionId, sessionName, leaderPid: detachedLeaderPid }, { exactSessionExists: () => probeExactDetachedSessionExists(detachedLeaderAuthority) });
5196
+ if (attachResolution === "protocol-failure") {
5197
+ process.stderr.write("[omx] detached session ended without an authenticated finalized terminal status; treating the launch as failed.\n");
5198
+ if (!process.exitCode)
5199
+ process.exitCode = 1;
4422
5200
  }
4423
- throw err;
4424
5201
  }
5202
+ return { postLaunchHandledExternally: true };
4425
5203
  };
4426
- const contextKey = process.env[OMX_MADMAX_DETACHED_CONTEXT_ENV]?.trim();
4427
- const runsRoot = resolveMadmaxRunsRoot(process.env);
4428
- try {
4429
- if (isMadmaxDetachedGuardEnabled(process.env) && contextKey) {
4430
- return withMadmaxDetachedContextLock(runsRoot, contextKey, launchDetachedSession);
4431
- }
4432
- return launchDetachedSession();
4433
- }
4434
- catch (err) {
4435
- if (err instanceof MadmaxDetachedReuseError || err instanceof MadmaxDetachedGuardError) {
4436
- throw err;
4437
- }
4438
- logCliOperationFailure(err);
4439
- // tmux not available or failed, just run codex directly
4440
- runCodexBlocking(cwd, launchArgs, codexEnvWithNotify);
4441
- return { postLaunchHandledExternally: false };
5204
+ if (contextKey) {
5205
+ return await withMadmaxDetachedContextLock(runsRoot, contextKey, launchDetachedSession);
4442
5206
  }
5207
+ return await launchDetachedSession();
4443
5208
  }
4444
5209
  }
4445
5210
  function listHudWatchPaneIdsInCurrentWindow(currentPaneId, owner = {}) {
@@ -4486,7 +5251,24 @@ export function isCodexVersionRequest(args) {
4486
5251
  const { omxArgs } = splitOmxArgsAtEndOfOptions(args);
4487
5252
  return omxArgs.some((arg) => CODEX_VERSION_FLAGS.has(arg));
4488
5253
  }
4489
- export function buildWindowsPromptCommand(command, args) {
5254
+ export function buildWindowsPromptCommand(command, args) {
5255
+ const invocation = [
5256
+ "&",
5257
+ quotePowerShellArg(command),
5258
+ ...args.map(quotePowerShellArg),
5259
+ ].join(" ");
5260
+ const wrappedCommand = [
5261
+ "$ErrorActionPreference = 'Stop'",
5262
+ `& { ${invocation} }`,
5263
+ ].join("; ");
5264
+ return `powershell.exe -NoLogo -NoExit -EncodedCommand ${encodePowerShellCommand(wrappedCommand)}`;
5265
+ }
5266
+ /**
5267
+ * Build a Windows child command for detached ownership. Unlike the interactive
5268
+ * prompt wrapper, this process exits as soon as Codex does so the outer barrier
5269
+ * can run its post-launch cleanup.
5270
+ */
5271
+ export function buildWindowsDetachedChildCommand(command, args) {
4490
5272
  const invocation = [
4491
5273
  "&",
4492
5274
  quotePowerShellArg(command),
@@ -4494,9 +5276,10 @@ export function buildWindowsPromptCommand(command, args) {
4494
5276
  ].join(" ");
4495
5277
  const wrappedCommand = [
4496
5278
  "$ErrorActionPreference = 'Stop'",
4497
- `& { ${invocation} }`,
5279
+ invocation,
5280
+ "exit $LASTEXITCODE",
4498
5281
  ].join("; ");
4499
- return `powershell.exe -NoLogo -NoExit -EncodedCommand ${encodePowerShellCommand(wrappedCommand)}`;
5282
+ return `powershell.exe -NoLogo -NoProfile -NonInteractive -EncodedCommand ${encodePowerShellCommand(wrappedCommand)}`;
4500
5283
  }
4501
5284
  /**
4502
5285
  * Wrap a command for tmux pane execution while preserving the tmux pane cwd.
@@ -4534,45 +5317,323 @@ function quoteShellArg(value) {
4534
5317
  function quotePowerShellArg(value) {
4535
5318
  return `'${value.replace(/'/g, "''")}'`;
4536
5319
  }
4537
- export function buildDetachedWindowsBootstrapScript(sessionName, commandText, delayMs = WINDOWS_DETACHED_BOOTSTRAP_DELAY_MS, tmuxCommand = resolveTmuxExecutableForLaunch()) {
4538
- const delay = Number.isFinite(delayMs) && delayMs > 0
4539
- ? Math.floor(delayMs)
4540
- : WINDOWS_DETACHED_BOOTSTRAP_DELAY_MS;
4541
- const targetLiteral = JSON.stringify(`${sessionName}:0.0`);
4542
- const commandLiteral = JSON.stringify(commandText);
4543
- const tmuxCommandLiteral = JSON.stringify(tmuxCommand);
4544
- return [
4545
- "const { execFileSync } = require('child_process');",
4546
- `const tmuxCommand = ${tmuxCommandLiteral};`,
4547
- `setTimeout(() => {`,
4548
- `try { execFileSync(tmuxCommand, ['send-keys', '-t', ${targetLiteral}, '-l', '--', ${commandLiteral}], { stdio: 'ignore' }); } catch {}`,
4549
- `try { execFileSync(tmuxCommand, ['send-keys', '-t', ${targetLiteral}, 'C-m'], { stdio: 'ignore' }); } catch {}`,
4550
- `}, ${delay});`,
4551
- ].join("");
5320
+ export function decodeDetachedLeaderPayload(encoded) {
5321
+ if (!encoded || !/^[A-Za-z0-9_-]+$/.test(encoded))
5322
+ throw new Error("invalid detached leader payload");
5323
+ let value;
5324
+ try {
5325
+ value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
5326
+ }
5327
+ catch {
5328
+ throw new Error("invalid detached leader payload");
5329
+ }
5330
+ if (!value || typeof value !== "object")
5331
+ throw new Error("invalid detached leader payload");
5332
+ const payload = value;
5333
+ const options = payload.preLaunchOptions;
5334
+ if (typeof payload.cwd !== "string" || typeof payload.sessionName !== "string" || typeof payload.sessionId !== "string" ||
5335
+ typeof payload.codexCmd !== "string" || !options || typeof options !== "object" ||
5336
+ typeof options.enableNotifyFallbackAuthority !== "boolean" ||
5337
+ typeof options.worktreeDirty !== "boolean" ||
5338
+ typeof options.shouldAttach !== "boolean")
5339
+ throw new Error("invalid detached leader payload");
5340
+ const notifyTempContract = options.notifyTempContract;
5341
+ if (notifyTempContract !== undefined && (!notifyTempContract || typeof notifyTempContract !== "object"))
5342
+ throw new Error("invalid detached leader payload");
5343
+ const parentEnv = payload.parentEnv;
5344
+ if (parentEnv !== undefined && (!parentEnv || typeof parentEnv !== "object" ||
5345
+ Object.entries(parentEnv).some(([key, value]) => !SHELL_ENV_NAME_PATTERN.test(key) || DETACHED_SESSION_PANE_ENV_KEYS.has(key) || typeof value !== "string" || value.includes("\0")))) {
5346
+ throw new Error("invalid detached leader parent environment");
5347
+ }
5348
+ return {
5349
+ cwd: payload.cwd, sessionName: payload.sessionName, sessionId: payload.sessionId, codexCmd: payload.codexCmd,
5350
+ ...(typeof payload.codexHomeOverride === "string" ? { codexHomeOverride: payload.codexHomeOverride } : {}),
5351
+ ...(typeof payload.projectLocalCodexHomeForCleanup === "string" ? { projectLocalCodexHomeForCleanup: payload.projectLocalCodexHomeForCleanup } : {}),
5352
+ ...(typeof payload.runtimeCodexHomeForCleanup === "string" ? { runtimeCodexHomeForCleanup: payload.runtimeCodexHomeForCleanup } : {}),
5353
+ ...(parentEnv ? { parentEnv: parentEnv } : {}),
5354
+ ...(typeof payload.readyPath === "string" ? { readyPath: payload.readyPath } : {}),
5355
+ preLaunchOptions: {
5356
+ ...(notifyTempContract ? { notifyTempContract: notifyTempContract } : {}),
5357
+ enableNotifyFallbackAuthority: options.enableNotifyFallbackAuthority,
5358
+ worktreeDirty: options.worktreeDirty,
5359
+ shouldAttach: options.shouldAttach,
5360
+ },
5361
+ };
4552
5362
  }
4553
- function scheduleDetachedWindowsCodexLaunch(sessionName, commandText) {
4554
- const child = spawn(process.execPath, ["-e", buildDetachedWindowsBootstrapScript(sessionName, commandText)], {
4555
- detached: true,
4556
- stdio: "ignore",
4557
- windowsHide: true,
5363
+ function parseDetachedHudAuthorityProof(value) {
5364
+ if (!value || typeof value !== "object")
5365
+ return undefined;
5366
+ const proof = value;
5367
+ if (typeof proof.paneId !== "string" || !/^%[0-9]+$/.test(proof.paneId))
5368
+ return undefined;
5369
+ if (typeof proof.panePid !== "number" || !Number.isSafeInteger(proof.panePid) || proof.panePid <= 0)
5370
+ return undefined;
5371
+ if (typeof proof.sessionId !== "string" || !/^\$[0-9]+$/.test(proof.sessionId))
5372
+ return undefined;
5373
+ if (typeof proof.windowId !== "string" || !/^@[0-9]+$/.test(proof.windowId))
5374
+ return undefined;
5375
+ if (typeof proof.operationMarker !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(proof.operationMarker))
5376
+ return undefined;
5377
+ return { paneId: proof.paneId, panePid: proof.panePid, sessionId: proof.sessionId, windowId: proof.windowId, operationMarker: proof.operationMarker };
5378
+ }
5379
+ function teardownDetachedOwnedHudPane(leaderPaneId, payload, hudProof) {
5380
+ // #3266: on a normal child exit, kill exactly the bootstrap-proven OMX HUD pane so
5381
+ // the leader pane becomes the last live pane and the owned session closes naturally,
5382
+ // returning the attached client to its shell. Fail-closed: any doubt means zero mutations.
5383
+ // Contract: only the proven HUD is removed. A pane the user added to the detached
5384
+ // session is deliberately preserved, which intentionally keeps that session (and any
5385
+ // attached client) alive; natural closure and shell return happen only when no other
5386
+ // panes remain.
5387
+ const discovered = discoverDetachedHudAuthority(payload.sessionName, payload.sessionId);
5388
+ const proof = discovered ?? hudProof;
5389
+ if (!proof)
5390
+ return;
5391
+ try {
5392
+ cleanupDetachedHudPane(proof, discovered ? payload.sessionId : undefined);
5393
+ execTmuxFileSync(["kill-pane", "-t", leaderPaneId], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
5394
+ }
5395
+ catch (error) {
5396
+ traceDetachedLeaderPhase(`hud-teardown-error:${describeDetachedLeaderFailure(error)}`);
5397
+ logCliOperationFailure(error);
5398
+ }
5399
+ }
5400
+ function traceDetachedLeaderPhase(phase) {
5401
+ if (process.env.OMX_TEST_DETACHED_TRACE !== "1")
5402
+ return;
5403
+ const line = `[omx-test-detached] ${phase}\n`;
5404
+ process.stderr.write(line);
5405
+ const tracePath = process.env.OMX_TEST_DETACHED_TRACE_PATH;
5406
+ if (tracePath)
5407
+ appendFileSync(tracePath, line);
5408
+ }
5409
+ async function runDetachedSessionLeader(payload) {
5410
+ for (const [key, value] of Object.entries(payload.parentEnv ?? {})) {
5411
+ if (isDetachedLaunchControlPlaneKey(key, process.platform === "win32"))
5412
+ continue;
5413
+ process.env[key] = value;
5414
+ }
5415
+ let pane;
5416
+ const inheritedPane = process.env.TMUX_PANE?.trim();
5417
+ if (payload.preLaunchOptions.shouldAttach === false) {
5418
+ if (!inheritedPane || !/^%[0-9]+$/.test(inheritedPane)) {
5419
+ throw new Error("detached Hermes leader has no tmux pane identity");
5420
+ }
5421
+ pane = inheritedPane;
5422
+ }
5423
+ else {
5424
+ const paneSnapshot = execTmuxFileSync(["list-panes", "-t", payload.sessionName, "-F", "#{pane_id}\t#{pane_dead}\t#{pane_pid}"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
5425
+ pane = parseDetachedLeaderPaneIdByPid(paneSnapshot, process.pid);
5426
+ }
5427
+ process.env.TMUX_PANE = pane;
5428
+ const established = await establishPreLaunchBinding(payload.cwd, payload.sessionId, {
5429
+ tmuxSessionName: payload.sessionName,
5430
+ tmuxPaneId: pane,
4558
5431
  });
4559
- child.unref();
5432
+ const binding = established.binding;
5433
+ let ownedRecord;
5434
+ let detachedHudProof;
5435
+ const nonce = payload.readyPath?.split(".").at(-2) || payload.sessionId;
5436
+ const contextKey = process.env[OMX_MADMAX_DETACHED_CONTEXT_ENV]?.trim();
5437
+ const activeRecordPath = contextKey
5438
+ ? madmaxDetachedActiveRecordPath(resolveMadmaxRunsRoot(process.env), contextKey)
5439
+ : join(omxRoot(payload.cwd), "state", "detached-active-record.json");
5440
+ let externalInterrupt;
5441
+ try {
5442
+ const completion = await completePreLaunchSetup(payload.cwd, payload.sessionId, payload.preLaunchOptions.notifyTempContract, payload.codexHomeOverride, payload.preLaunchOptions.enableNotifyFallbackAuthority, payload.preLaunchOptions.worktreeDirty);
5443
+ if (completion.kind === "failure") {
5444
+ const finalization = await finalizeBoundOnce(binding, "setup-failure", payload.cwd);
5445
+ if (!finalization.finalized)
5446
+ throw new Error(`bound setup finalization denied: ${finalization.cleanup.comparison?.reason ?? "unknown reason"}`);
5447
+ throw completion.error;
5448
+ }
5449
+ const record = {
5450
+ version: 1, context_key: contextKey ?? payload.sessionId, created_at: new Date().toISOString(),
5451
+ source_cwd: payload.cwd, argv: [payload.codexCmd], run_dir: process.env.OMX_ROOT ?? payload.cwd,
5452
+ tmux_session_name: payload.sessionName, session_id: payload.sessionId, tmux_pane_id: pane,
5453
+ launch_nonce: nonce, leader_pid: process.pid, base_state_root: binding.context.baseStateDir,
5454
+ lifecycle_phase: "ready",
5455
+ };
5456
+ const runtimeBinding = queryMadmaxDetachedRuntimeBinding(record);
5457
+ ownedRecord = writeMadmaxDetachedActiveRecord(activeRecordPath, {
5458
+ ...record,
5459
+ ...(runtimeBinding ? {
5460
+ tmux_internal_session_id: runtimeBinding.internalSessionId,
5461
+ tmux_session_created: runtimeBinding.sessionCreated,
5462
+ tmux_pane_pid: runtimeBinding.panePid,
5463
+ } : {}),
5464
+ });
5465
+ if (payload.readyPath) {
5466
+ writeDetachedLeaderReport(payload.readyPath, { version: 1, kind: "ready", nonce, sessionId: payload.sessionId, sessionName: payload.sessionName, paneId: pane, leaderPid: process.pid });
5467
+ const releasePath = `${payload.readyPath}.release`;
5468
+ const abortPath = `${payload.readyPath}.abort`;
5469
+ const releaseDeadline = Date.now() + 30_000;
5470
+ let interrupted;
5471
+ const interrupt = (signal) => { interrupted = signal; };
5472
+ process.once("SIGINT", () => interrupt("SIGINT"));
5473
+ process.once("SIGTERM", () => interrupt("SIGTERM"));
5474
+ process.once("SIGHUP", () => interrupt("SIGHUP"));
5475
+ while (true) {
5476
+ if (interrupted)
5477
+ throw new Error(`detached leader interrupted before release: ${interrupted}`);
5478
+ const release = readDetachedLeaderReport(releasePath);
5479
+ if (release?.kind === "release" && release.nonce === nonce && release.sessionId === payload.sessionId && release.sessionName === payload.sessionName && release.leaderPid === process.pid) {
5480
+ detachedHudProof = parseDetachedHudAuthorityProof(release.hud);
5481
+ break;
5482
+ }
5483
+ const abort = readDetachedLeaderReport(abortPath);
5484
+ if (abort?.kind === "abort" && abort.nonce === nonce && abort.sessionId === payload.sessionId && abort.sessionName === payload.sessionName && abort.leaderPid === process.pid) {
5485
+ throw new Error("detached launch aborted before release");
5486
+ }
5487
+ if (Date.now() >= releaseDeadline)
5488
+ throw new Error("detached leader release timed out");
5489
+ blockMs(20);
5490
+ }
5491
+ rmSync(releasePath, { force: true });
5492
+ rmSync(abortPath, { force: true });
5493
+ }
5494
+ const child = spawn(process.platform === "win32" ? "powershell.exe" : "/bin/sh", process.platform === "win32" ? ["-NoProfile", "-Command", payload.codexCmd] : ["-c", payload.codexCmd], {
5495
+ cwd: payload.cwd,
5496
+ stdio: "inherit",
5497
+ ...(process.platform === "win32" ? {} : { detached: true }),
5498
+ });
5499
+ let escalation;
5500
+ const signalChildTree = (signal, force = false) => {
5501
+ if (!child.pid)
5502
+ return;
5503
+ try {
5504
+ if (process.platform === "win32")
5505
+ execFileSync("taskkill", ["/PID", String(child.pid), "/T", ...(force ? ["/F"] : [])], { stdio: "ignore" });
5506
+ else
5507
+ process.kill(-child.pid, signal);
5508
+ }
5509
+ catch { }
5510
+ };
5511
+ const forward = (signal) => {
5512
+ externalInterrupt ??= signal;
5513
+ signalChildTree(signal);
5514
+ if (!escalation) {
5515
+ escalation = setTimeout(() => signalChildTree("SIGKILL", true), 5_000);
5516
+ escalation.unref();
5517
+ }
5518
+ };
5519
+ process.once("SIGINT", () => forward("SIGINT"));
5520
+ process.once("SIGTERM", () => forward("SIGTERM"));
5521
+ process.once("SIGHUP", () => forward("SIGHUP"));
5522
+ traceDetachedLeaderPhase("child-wait-start");
5523
+ const outcome = await new Promise((resolveChild, rejectChild) => {
5524
+ child.once("error", rejectChild);
5525
+ child.once("exit", (code, signal) => resolveChild({ code, signal }));
5526
+ });
5527
+ traceDetachedLeaderPhase(`child-exit:${String(outcome.code)}:${String(outcome.signal)}`);
5528
+ if (escalation)
5529
+ clearTimeout(escalation);
5530
+ if (process.platform !== "win32" && child.pid) {
5531
+ const waitForGroupExit = async (deadline) => {
5532
+ while (Date.now() < deadline) {
5533
+ try {
5534
+ process.kill(-child.pid, 0);
5535
+ await new Promise((resolveWait) => setTimeout(resolveWait, 20));
5536
+ }
5537
+ catch {
5538
+ return true;
5539
+ }
5540
+ }
5541
+ return false;
5542
+ };
5543
+ let groupGone = await waitForGroupExit(Date.now() + 5_000);
5544
+ if (!groupGone) {
5545
+ signalChildTree("SIGKILL", true);
5546
+ groupGone = await waitForGroupExit(Date.now() + 5_000);
5547
+ }
5548
+ if (!groupGone)
5549
+ throw new Error("detached child process group did not disappear after forced termination");
5550
+ }
5551
+ const signalExitCodes = { SIGHUP: 129, SIGINT: 130, SIGTERM: 143, SIGKILL: 137 };
5552
+ process.exitCode = externalInterrupt
5553
+ ? signalExitCodes[externalInterrupt] ?? 1
5554
+ : outcome.code ?? (outcome.signal ? signalExitCodes[outcome.signal] ?? 1 : 1);
5555
+ traceDetachedLeaderPhase("post-launch-start");
5556
+ await postLaunch(payload.cwd, payload.sessionId, binding, payload.codexHomeOverride, payload.preLaunchOptions.enableNotifyFallbackAuthority, payload.projectLocalCodexHomeForCleanup, async () => {
5557
+ if (payload.readyPath)
5558
+ writeDetachedLeaderReport(payload.readyPath, {
5559
+ version: 1, kind: "terminal", nonce, sessionId: payload.sessionId, sessionName: payload.sessionName,
5560
+ paneId: pane, leaderPid: process.pid, finalized: true,
5561
+ ...(typeof process.exitCode === "number" ? { exitStatus: process.exitCode } : {}),
5562
+ });
5563
+ traceDetachedLeaderPhase("terminal-report-written");
5564
+ if (ownedRecord) {
5565
+ const bytes = readFileSync(activeRecordPath, "utf-8");
5566
+ const digest = createHash("sha256").update(bytes).digest("hex");
5567
+ if (bytes === ownedRecord.bytes && digest === ownedRecord.digest)
5568
+ rmSync(activeRecordPath, { force: true });
5569
+ }
5570
+ if (!externalInterrupt && outcome.code !== null) {
5571
+ traceDetachedLeaderPhase("hud-teardown-start");
5572
+ teardownDetachedOwnedHudPane(pane, payload, detachedHudProof);
5573
+ traceDetachedLeaderPhase("hud-teardown-done");
5574
+ }
5575
+ });
5576
+ traceDetachedLeaderPhase("post-launch-done");
5577
+ await cleanupRuntimeCodexHome(payload.runtimeCodexHomeForCleanup, payload.projectLocalCodexHomeForCleanup);
5578
+ traceDetachedLeaderPhase("leader-return");
5579
+ }
5580
+ catch (error) {
5581
+ let failure = error;
5582
+ let finalized = false;
5583
+ try {
5584
+ await postLaunch(payload.cwd, payload.sessionId, binding, payload.codexHomeOverride, payload.preLaunchOptions.enableNotifyFallbackAuthority, payload.projectLocalCodexHomeForCleanup);
5585
+ finalized = true;
5586
+ await cleanupRuntimeCodexHome(payload.runtimeCodexHomeForCleanup, payload.projectLocalCodexHomeForCleanup);
5587
+ }
5588
+ catch (lifecycleError) {
5589
+ failure = new AggregateError([error, lifecycleError], `detached leader failure finalization did not complete: launch=${describeDetachedLeaderFailure(error)}; lifecycle=${describeDetachedLeaderFailure(lifecycleError)}`);
5590
+ }
5591
+ if (finalized && ownedRecord) {
5592
+ try {
5593
+ const bytes = readFileSync(activeRecordPath, "utf-8");
5594
+ const digest = createHash("sha256").update(bytes).digest("hex");
5595
+ if (bytes === ownedRecord.bytes && digest === ownedRecord.digest)
5596
+ rmSync(activeRecordPath, { force: true });
5597
+ }
5598
+ catch { }
5599
+ }
5600
+ if (payload.readyPath)
5601
+ writeDetachedLeaderReport(payload.readyPath, {
5602
+ version: 1, kind: "failed", nonce, sessionId: payload.sessionId, sessionName: payload.sessionName,
5603
+ leaderPid: process.pid, finalized, error: failure instanceof Error ? failure.message : String(failure),
5604
+ });
5605
+ throw failure;
5606
+ }
5607
+ finally {
5608
+ await closeLaunchSessionBindingOnce(binding);
5609
+ }
4560
5610
  }
4561
5611
  /**
4562
5612
  * postLaunch: Clean up after Codex exits.
4563
5613
  * Each step is independently fault-tolerant (try/catch per step).
4564
5614
  */
4565
- export async function postLaunch(cwd, sessionId, codexHomeOverride, enableNotifyFallbackAuthority = false, projectLocalCodexHomeForCleanup) {
4566
- // Capture session start time before cleanup (writeSessionEnd deletes session.json)
4567
- let sessionStartedAt;
5615
+ export async function postLaunch(cwd, sessionId, binding, codexHomeOverride, enableNotifyFallbackAuthority = false, projectLocalCodexHomeForCleanup, onFinalized) {
5616
+ const sessionStartedAt = binding.startedAt;
5617
+ // Pointer authority is consumed before any terminal cleanup. A denied or
5618
+ // unproven binding must leave every cleanup target untouched.
5619
+ let terminalError;
4568
5620
  try {
4569
- const sessionState = await readSessionState(cwd);
4570
- sessionStartedAt = sessionState?.started_at;
5621
+ const finalization = await finalizeBoundOnce(binding, "post-launch", cwd);
5622
+ if (!finalization.finalized)
5623
+ terminalError = new Error(`bound session finalization denied: ${finalization.cleanup.comparison?.reason ?? "unknown reason"}`);
5624
+ const closeFailure = finalization.cleanup.capability.find((evidence) => evidence.status === "failed");
5625
+ if (closeFailure)
5626
+ terminalError = new Error(`bound session retained close failed: ${closeFailure.error?.message ?? "unknown error"}`);
4571
5627
  }
4572
- catch (err) {
4573
- logCliOperationFailure(err);
4574
- // Non-fatal
5628
+ catch (error) {
5629
+ terminalError = error;
4575
5630
  }
5631
+ if (terminalError) {
5632
+ // Denial diagnostics are complete; do not mutate ordinary lifecycle state.
5633
+ await closeLaunchSessionBindingOnce(binding);
5634
+ throw terminalError;
5635
+ }
5636
+ await onFinalized?.();
4576
5637
  // 0. Reap MCP orphans left behind by the session that just exited.
4577
5638
  await reapPostLaunchOrphanedMcpProcesses();
4578
5639
  // 0. Flush fallback watcher once to reduce race with fast codex exit.
@@ -4623,13 +5684,6 @@ export async function postLaunch(cwd, sessionId, codexHomeOverride, enableNotify
4623
5684
  catch (err) {
4624
5685
  console.error(`[omx] postLaunch: model instructions cleanup failed: ${err instanceof Error ? err.message : err}`);
4625
5686
  }
4626
- // 2. Archive session (write history, delete session.json)
4627
- try {
4628
- await writeSessionEnd(cwd, sessionId);
4629
- }
4630
- catch (err) {
4631
- console.error(`[omx] postLaunch: session archive failed: ${err instanceof Error ? err.message : err}`);
4632
- }
4633
5687
  // 2.5. Best-effort wiki session capture
4634
5688
  try {
4635
5689
  const { onSessionEnd } = await import("../wiki/lifecycle.js");
@@ -4701,10 +5755,59 @@ export async function postLaunch(cwd, sessionId, codexHomeOverride, enableNotify
4701
5755
  logCliOperationFailure(err);
4702
5756
  // Non-fatal
4703
5757
  }
5758
+ const closeEvidence = await closeLaunchSessionBindingOnce(binding);
5759
+ if (closeEvidence.status === "failed") {
5760
+ throw new Error(`bound session retained close failed: ${closeEvidence.error?.message ?? "unknown error"}`);
5761
+ }
4704
5762
  }
4705
- export async function runDetachedSessionPostLaunch(cwd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup) {
4706
- await postLaunch(cwd, sessionId, codexHomeOverride, false, projectLocalCodexHomeForCleanup);
4707
- await cleanupRuntimeCodexHome(runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch(logCliOperationFailure);
5763
+ export async function runDetachedSessionPostLaunch(cwd, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, releaseMarkerPath) {
5764
+ // The detached child is intentionally unbound: it must never perform parent
5765
+ // pointer finalization or cleanup after the release barrier. It still owns
5766
+ // every non-pointer terminal lifecycle responsibility.
5767
+ const failures = [];
5768
+ const strict = async (operation) => {
5769
+ try {
5770
+ await operation();
5771
+ }
5772
+ catch (error) {
5773
+ failures.push(error);
5774
+ }
5775
+ };
5776
+ await strict(() => reapPostLaunchOrphanedMcpProcesses());
5777
+ await strict(() => flushNotifyFallbackOnce(cwd, { sessionId }));
5778
+ await strict(() => stopNotifyFallbackWatcher(cwd));
5779
+ await strict(() => flushHookDerivedWatcherOnce(cwd));
5780
+ await strict(() => stopHookDerivedWatcher(cwd));
5781
+ if (projectLocalCodexHomeForCleanup) {
5782
+ await strict(() => cleanCodexModelAvailabilityNuxIfNeeded(join(projectLocalCodexHomeForCleanup, "config.toml")));
5783
+ }
5784
+ await strict(() => removeSessionModelInstructionsFile(cwd, sessionId));
5785
+ await strict(async () => {
5786
+ const { onSessionEnd } = await import("../wiki/lifecycle.js");
5787
+ onSessionEnd({ cwd, session_id: sessionId });
5788
+ });
5789
+ await strict(() => cleanupPostLaunchModeStateFiles(cwd, sessionId));
5790
+ await strict(async () => {
5791
+ const { notifyLifecycle } = await import("../notifications/index.js");
5792
+ await notifyLifecycle("session-end", {
5793
+ sessionId, projectPath: cwd, projectName: basename(cwd), reason: "session_exit",
5794
+ });
5795
+ });
5796
+ await strict(async () => {
5797
+ const { markOwnedTeamsLeaderSessionStopped } = await import("../team/state.js");
5798
+ await markOwnedTeamsLeaderSessionStopped(cwd, sessionId);
5799
+ });
5800
+ await strict(() => emitNativeHookEvent(cwd, "session-end", {
5801
+ session_id: sessionId,
5802
+ context: buildNativeHookBaseContext(cwd, sessionId, "finished", {
5803
+ project_path: cwd, project_name: basename(cwd), reason: "session_exit", status: "finished",
5804
+ }),
5805
+ }));
5806
+ await strict(() => cleanupRuntimeCodexHome(runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup));
5807
+ if (releaseMarkerPath)
5808
+ await strict(() => rm(releaseMarkerPath, { force: true }));
5809
+ if (failures.length > 0)
5810
+ throw new AggregateError(failures, "detached post-launch lifecycle cleanup failed");
4708
5811
  }
4709
5812
  async function emitNativeHookEvent(cwd, event, opts = {}) {
4710
5813
  const payload = buildHookEvent(event, {
@@ -5143,14 +6246,13 @@ async function listHookVisibleRunDirStateRefs(cwd) {
5143
6246
  if (!sourceCwd || !runDir)
5144
6247
  return;
5145
6248
  try {
5146
- if (canonicalizePathForRunDirMatch(sourceCwd) !== canonicalCwd &&
5147
- (!worktreeCwd || canonicalizePathForRunDirMatch(worktreeCwd) !== canonicalCwd))
6249
+ if (canonicalizePathForRunDirMatch(sourceCwd) !== canonicalCwd
6250
+ && (!worktreeCwd || canonicalizePathForRunDirMatch(worktreeCwd) !== canonicalCwd))
5148
6251
  return;
5149
6252
  const resolvedRunDir = resolve(runDir);
5150
6253
  if (resolvedRunDir !== canonicalRunsRoot
5151
- && !resolvedRunDir.startsWith(`${canonicalRunsRoot}/`)) {
6254
+ && !resolvedRunDir.startsWith(`${canonicalRunsRoot}/`))
5152
6255
  return;
5153
- }
5154
6256
  runDirs.add(resolvedRunDir);
5155
6257
  }
5156
6258
  catch {
@@ -5203,7 +6305,7 @@ async function listHookVisibleRunDirStateRefs(cwd) {
5203
6305
  for (const dir of candidateDirs) {
5204
6306
  const files = await readdir(dir).catch(() => []);
5205
6307
  for (const file of files) {
5206
- if (!file.endsWith("-state.json") || file === "session.json")
6308
+ if (!isModeStateFilename(file))
5207
6309
  continue;
5208
6310
  const path = join(dir, file);
5209
6311
  if (seenPaths.has(path))
@@ -5219,41 +6321,362 @@ async function listHookVisibleRunDirStateRefs(cwd) {
5219
6321
  }
5220
6322
  return refs.sort((a, b) => a.mode.localeCompare(b.mode));
5221
6323
  }
5222
- async function cancelModes(args = []) {
5223
- const { writeFile, readFile } = await import("fs/promises");
5224
- const cwd = process.cwd();
6324
+ function isCanonicalPathWithin(parent, child, allowEqual = false) {
6325
+ const rel = relative(parent, child);
6326
+ if (rel === "")
6327
+ return allowEqual;
6328
+ return rel !== ".." && !rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(rel);
6329
+ }
6330
+ async function selectAuthorizedHookVisibleRunDirState(cwd) {
6331
+ const runsRoot = resolveMadmaxRunsRoot(process.env);
6332
+ const activeDir = join(runsRoot, MADMAX_DETACHED_ACTIVE_DIR);
6333
+ const canonicalCwd = canonicalizePathForRunDirMatch(cwd);
6334
+ let canonicalRunsRoot;
6335
+ try {
6336
+ canonicalRunsRoot = realpathSync(resolve(runsRoot));
6337
+ }
6338
+ catch {
6339
+ return null;
6340
+ }
6341
+ const candidates = [];
6342
+ const files = await readdir(activeDir).catch(() => []);
6343
+ for (const file of files) {
6344
+ if (!file.endsWith(".json"))
6345
+ continue;
6346
+ const record = readMadmaxDetachedActiveRecord(join(activeDir, file));
6347
+ if (!record || file !== `${record.context_key}.json`)
6348
+ continue;
6349
+ if (!record.session_id || normalizeSessionId(record.session_id) !== record.session_id)
6350
+ continue;
6351
+ if (!hasMatchingMadmaxDetachedRuntimeBinding(record))
6352
+ continue;
6353
+ if (canonicalizePathForRunDirMatch(record.source_cwd) !== canonicalCwd
6354
+ && (!record.worktree_cwd || canonicalizePathForRunDirMatch(record.worktree_cwd) !== canonicalCwd))
6355
+ continue;
6356
+ try {
6357
+ const canonicalRunDir = realpathSync(resolve(record.run_dir));
6358
+ if (!isCanonicalPathWithin(canonicalRunsRoot, canonicalRunDir)) {
6359
+ throw new Error("run directory escapes the authorized runs root");
6360
+ }
6361
+ const stateDir = realpathSync(join(canonicalRunDir, ".omx", "state"));
6362
+ if (!isCanonicalPathWithin(canonicalRunDir, stateDir)) {
6363
+ throw new Error("state directory escapes the authorized run directory");
6364
+ }
6365
+ const session = JSON.parse(await readFile(join(stateDir, "session.json"), "utf-8"));
6366
+ if (session.session_id !== record.session_id)
6367
+ throw new Error("run session pointer changed");
6368
+ const sessionDir = realpathSync(join(stateDir, "sessions", record.session_id));
6369
+ if (!isCanonicalPathWithin(stateDir, sessionDir)) {
6370
+ throw new Error("session directory escapes the authorized state directory");
6371
+ }
6372
+ candidates.push({ sessionDir, sessionId: record.session_id, record });
6373
+ }
6374
+ catch (err) {
6375
+ throw new Error(`Refusing cancellation because detached run authority is invalid: ${record.run_dir}.`, { cause: err });
6376
+ }
6377
+ }
6378
+ if (candidates.length > 1)
6379
+ throw new Error("Refusing cancellation because multiple detached run authorities match.");
6380
+ if (candidates.length === 0)
6381
+ return null;
6382
+ const [{ sessionDir, sessionId, record }] = candidates;
6383
+ const refs = [];
6384
+ const stateFiles = await readdir(sessionDir).catch(() => []);
6385
+ for (const file of stateFiles) {
6386
+ if (!isModeStateFilename(file))
6387
+ continue;
6388
+ const path = join(sessionDir, file);
6389
+ try {
6390
+ const fileStat = lstatSync(path);
6391
+ if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
6392
+ throw new Error(`Refusing cancellation through non-regular run state target: ${path}.`);
6393
+ }
6394
+ const canonicalFile = realpathSync(path);
6395
+ if (!isCanonicalPathWithin(sessionDir, canonicalFile)) {
6396
+ throw new Error(`Refusing cancellation outside authorized run session: ${path}.`);
6397
+ }
6398
+ refs.push({
6399
+ mode: file.slice(0, -"-state.json".length),
6400
+ path: canonicalFile,
6401
+ scope: "session",
6402
+ });
6403
+ }
6404
+ catch (err) {
6405
+ throw new Error(`Refusing cancellation because detached run state authority is invalid: ${path}.`, { cause: err });
6406
+ }
6407
+ }
6408
+ return { refs: refs.sort((a, b) => a.mode.localeCompare(b.mode)), sessionId, sessionDir, record };
6409
+ }
6410
+ function assertCancellationAuthorityPath(baseStateDir, authorityRoot) {
6411
+ const resolvedBase = resolve(baseStateDir);
6412
+ const resolvedAuthority = resolve(authorityRoot);
6413
+ const baseStat = lstatSync(resolvedBase);
6414
+ if (!baseStat.isDirectory() || baseStat.isSymbolicLink() || realpathSync(resolvedBase) !== resolvedBase) {
6415
+ throw new Error(`Refusing cancellation through symlinked state authority root: ${baseStateDir}.`);
6416
+ }
6417
+ if (!isCanonicalPathWithin(resolvedBase, resolvedAuthority, true)) {
6418
+ throw new Error(`Refusing cancellation outside base state authority: ${authorityRoot}.`);
6419
+ }
6420
+ let current = resolvedBase;
6421
+ const rel = relative(resolvedBase, resolvedAuthority);
6422
+ for (const component of rel ? rel.split(/[\\/]+/) : []) {
6423
+ current = join(current, component);
6424
+ const componentStat = lstatSync(current);
6425
+ if (!componentStat.isDirectory() || componentStat.isSymbolicLink() || realpathSync(current) !== current) {
6426
+ throw new Error(`Refusing cancellation through symlinked authority component: ${current}.`);
6427
+ }
6428
+ }
6429
+ return resolvedAuthority;
6430
+ }
6431
+ function collectCancellationOwnerIds(baseStateDir, sessionId) {
6432
+ const ownerIds = new Set();
6433
+ if (sessionId)
6434
+ ownerIds.add(sessionId);
6435
+ try {
6436
+ const pointer = JSON.parse(readFileSync(join(baseStateDir, "session.json"), "utf-8"));
6437
+ for (const field of [
6438
+ "session_id",
6439
+ "native_session_id",
6440
+ "codex_session_id",
6441
+ "owner_omx_session_id",
6442
+ "owner_codex_session_id",
6443
+ ]) {
6444
+ const normalized = normalizeSessionId(pointer[field]);
6445
+ if (normalized)
6446
+ ownerIds.add(normalized);
6447
+ }
6448
+ }
6449
+ catch { }
6450
+ return ownerIds;
6451
+ }
6452
+ function assertCancellationOwnerMetadata(value, ownerIds, path) {
6453
+ if (Object.prototype.hasOwnProperty.call(value, "session_id")) {
6454
+ const sessionId = normalizeSessionId(value.session_id);
6455
+ if (!sessionId || !ownerIds.has(sessionId)) {
6456
+ throw new Error(`Refusing contradictory session_id in ${path}.`);
6457
+ }
6458
+ }
6459
+ for (const ownerField of ["owner_omx_session_id", "owner_codex_session_id"]) {
6460
+ if (!Object.prototype.hasOwnProperty.call(value, ownerField))
6461
+ continue;
6462
+ const rawOwner = value[ownerField];
6463
+ const blankOwner = typeof rawOwner === "string" && rawOwner.trim() === "";
6464
+ const owner = normalizeSessionId(rawOwner);
6465
+ if (!blankOwner && (!owner || !ownerIds.has(owner))) {
6466
+ throw new Error(`Refusing contradictory ${ownerField} in ${path}.`);
6467
+ }
6468
+ }
6469
+ }
6470
+ async function resolveExactSessionCancellationAuthority(cwd, baseStateDir, sessionId) {
6471
+ if (!sessionId)
6472
+ return undefined;
6473
+ let pointer;
6474
+ let authorityCwd = cwd;
6475
+ try {
6476
+ const rawPointer = JSON.parse(await readFile(join(baseStateDir, "session.json"), "utf-8"));
6477
+ if (typeof rawPointer.cwd === "string" && rawPointer.cwd.trim())
6478
+ authorityCwd = rawPointer.cwd.trim();
6479
+ pointer = await readSessionPointer(resolveSessionPointerContext(authorityCwd));
6480
+ }
6481
+ catch {
6482
+ return undefined;
6483
+ }
6484
+ const nativeSessionId = normalizeSessionId(pointer.state?.native_session_id);
6485
+ if (pointer.status !== "usable" || pointer.state?.session_id !== sessionId || !nativeSessionId)
6486
+ return undefined;
6487
+ try {
6488
+ const ownerEvidence = await readNativeSessionOwnerEvidence(authorityCwd, nativeSessionId);
6489
+ if (ownerEvidence.status !== "usable")
6490
+ return undefined;
6491
+ }
6492
+ catch {
6493
+ return undefined;
6494
+ }
6495
+ return { sessionId, nativeSessionId, authorityCwd };
6496
+ }
6497
+ function assertExactSkillOwner(value, authority, path, allowStaleTopCodexOwner = false) {
6498
+ if (Object.prototype.hasOwnProperty.call(value, "session_id")
6499
+ && normalizeSessionId(value.session_id) !== authority.sessionId) {
6500
+ throw new Error(`Refusing contradictory session_id in ${path}.`);
6501
+ }
6502
+ if (Object.prototype.hasOwnProperty.call(value, "owner_omx_session_id")) {
6503
+ const rawOwner = value.owner_omx_session_id;
6504
+ const blankOwner = typeof rawOwner === "string" && rawOwner.trim() === "";
6505
+ const owner = normalizeSessionId(rawOwner);
6506
+ if (!blankOwner && (!owner || owner !== authority.sessionId)) {
6507
+ throw new Error(`Refusing contradictory owner_omx_session_id in ${path}.`);
6508
+ }
6509
+ }
6510
+ if (Object.prototype.hasOwnProperty.call(value, "owner_codex_session_id")) {
6511
+ const rawOwner = value.owner_codex_session_id;
6512
+ const blankOwner = typeof rawOwner === "string" && rawOwner.trim() === "";
6513
+ const owner = normalizeSessionId(rawOwner);
6514
+ if (!blankOwner && (!owner || (!allowStaleTopCodexOwner && owner !== authority.nativeSessionId))) {
6515
+ throw new Error(`Refusing contradictory owner_codex_session_id in ${path}.`);
6516
+ }
6517
+ }
6518
+ }
6519
+ function assertCompatibleNestedSkillOwners(value, authority, path) {
6520
+ if (!Object.prototype.hasOwnProperty.call(value, "active_skills"))
6521
+ return;
6522
+ if (!Array.isArray(value.active_skills)) {
6523
+ throw new Error(`Refusing malformed active_skills in ${path}.`);
6524
+ }
6525
+ for (const [index, skill] of value.active_skills.entries()) {
6526
+ const entryPath = `${path} active_skills[${index}]`;
6527
+ if (!skill || typeof skill !== "object" || Array.isArray(skill)) {
6528
+ throw new Error(`Refusing malformed active_skills entry ${index} in ${path}.`);
6529
+ }
6530
+ const entry = skill;
6531
+ if (typeof entry.skill !== "string" || entry.skill.trim() === "") {
6532
+ throw new Error(`Refusing malformed skill in ${entryPath}.`);
6533
+ }
6534
+ for (const field of ["active"]) {
6535
+ if (Object.prototype.hasOwnProperty.call(entry, field) && typeof entry[field] !== "boolean") {
6536
+ throw new Error(`Refusing malformed ${field} in ${entryPath}.`);
6537
+ }
6538
+ }
6539
+ if (Object.prototype.hasOwnProperty.call(entry, "phase") && typeof entry.phase !== "string") {
6540
+ throw new Error(`Refusing malformed phase in ${entryPath}.`);
6541
+ }
6542
+ for (const [field, fieldValue] of Object.entries(entry)) {
6543
+ if (field.endsWith("_at") && typeof fieldValue !== "string") {
6544
+ throw new Error(`Refusing malformed ${field} in ${entryPath}.`);
6545
+ }
6546
+ }
6547
+ assertExactSkillOwner(entry, authority, entryPath);
6548
+ }
6549
+ }
6550
+ async function cancelModes(args = [], options = {}) {
6551
+ const cwd = options.cwd ?? process.cwd();
5225
6552
  const nowIso = new Date().toISOString();
5226
- const force = args.includes("--force");
6553
+ if (args.length > 1 || args.some((arg) => arg !== "--force")) {
6554
+ const unsupported = args.find((arg) => arg !== "--force") ?? args[1] ?? "";
6555
+ throw new Error(unsupported === "--all"
6556
+ ? "omx cancel --all is not supported; broad workspace cancellation requires a separate command."
6557
+ : `Unknown cancel argument: ${unsupported}`);
6558
+ }
6559
+ const force = args.length === 1;
5227
6560
  try {
5228
6561
  const writableScope = await resolveWritableStateScope(cwd);
5229
- const loadStates = async (refs) => {
6562
+ const baseStateDir = getBaseStateDir(cwd);
6563
+ const exactAuthority = writableScope.source === "session"
6564
+ ? await resolveExactSessionCancellationAuthority(cwd, baseStateDir, writableScope.sessionId)
6565
+ : undefined;
6566
+ const expectedOwnerIds = collectCancellationOwnerIds(baseStateDir, writableScope.sessionId);
6567
+ const loadStates = async (refs, authorityRoot, ownerIds = expectedOwnerIds) => {
5230
6568
  const loaded = new Map();
6569
+ if (refs.length === 0)
6570
+ return loaded;
6571
+ const canonicalAuthorityRoot = assertCancellationAuthorityPath(authorityRoot === writableScope.stateDir ? baseStateDir : authorityRoot, authorityRoot);
5231
6572
  for (const ref of refs) {
5232
- const content = await readFile(ref.path, "utf-8");
6573
+ const fileStat = lstatSync(ref.path);
6574
+ if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
6575
+ throw new Error(`Refusing cancellation through non-regular state target ${ref.path}.`);
6576
+ }
6577
+ const canonicalPath = realpathSync(ref.path);
6578
+ const canonicalParent = realpathSync(dirname(ref.path));
6579
+ if (!isCanonicalPathWithin(canonicalAuthorityRoot, canonicalParent, true)
6580
+ || !isCanonicalPathWithin(canonicalAuthorityRoot, canonicalPath)) {
6581
+ throw new Error(`Refusing cancellation outside authorized state root: ${ref.path}.`);
6582
+ }
6583
+ const content = await readFile(canonicalPath, "utf-8");
5233
6584
  let parsedState;
5234
6585
  try {
5235
- parsedState = JSON.parse(content);
6586
+ const parsed = JSON.parse(content);
6587
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
6588
+ throw new Error("state must be a JSON object");
6589
+ }
6590
+ parsedState = parsed;
5236
6591
  }
5237
6592
  catch (err) {
5238
6593
  logCliOperationFailure(err);
5239
- continue;
6594
+ throw new Error(`Refusing partial cancellation because ${ref.path} is malformed.`, { cause: err });
6595
+ }
6596
+ if (typeof parsedState.mode === "string" && parsedState.mode !== ref.mode) {
6597
+ throw new Error(`Refusing contradictory mode state in ${ref.path}.`);
6598
+ }
6599
+ if (ref.mode === SKILL_ACTIVE_STATE_MODE && exactAuthority) {
6600
+ const hasTopCodexOwner = Object.prototype.hasOwnProperty.call(parsedState, "owner_codex_session_id");
6601
+ const displacedOwnerId = hasTopCodexOwner
6602
+ ? normalizeSessionId(parsedState.owner_codex_session_id)
6603
+ : undefined;
6604
+ const blankTopCodexOwner = typeof parsedState.owner_codex_session_id === "string"
6605
+ && parsedState.owner_codex_session_id.trim() === "";
6606
+ if (hasTopCodexOwner && !blankTopCodexOwner && !displacedOwnerId) {
6607
+ throw new Error(`Refusing contradictory owner_codex_session_id in ${ref.path}.`);
6608
+ }
6609
+ assertExactSkillOwner(parsedState, exactAuthority, ref.path, hasTopCodexOwner);
6610
+ assertCompatibleNestedSkillOwners(parsedState, exactAuthority, ref.path);
6611
+ if (displacedOwnerId && displacedOwnerId !== exactAuthority.nativeSessionId) {
6612
+ let displacedEvidence;
6613
+ try {
6614
+ displacedEvidence = await readNativeSessionOwnerEvidence(exactAuthority.authorityCwd, displacedOwnerId);
6615
+ }
6616
+ catch (error) {
6617
+ throw new Error(`Refusing ambiguous displaced owner evidence for ${ref.path}.`, { cause: error });
6618
+ }
6619
+ if (displacedEvidence.status !== "absent" && displacedEvidence.status !== "stale-dead") {
6620
+ throw new Error(`Refusing non-stale displaced owner evidence for ${ref.path}.`);
6621
+ }
6622
+ parsedState.owner_codex_session_id = exactAuthority.nativeSessionId;
6623
+ }
6624
+ }
6625
+ else {
6626
+ assertCancellationOwnerMetadata(parsedState, ownerIds, ref.path);
6627
+ if (Object.prototype.hasOwnProperty.call(parsedState, "active_skills")) {
6628
+ if (!Array.isArray(parsedState.active_skills)) {
6629
+ throw new Error(`Refusing malformed active_skills in ${ref.path}.`);
6630
+ }
6631
+ for (const [index, skill] of parsedState.active_skills.entries()) {
6632
+ if (!skill || typeof skill !== "object" || Array.isArray(skill)) {
6633
+ throw new Error(`Refusing malformed active_skills entry ${index} in ${ref.path}.`);
6634
+ }
6635
+ assertCancellationOwnerMetadata(skill, ownerIds, `${ref.path} active_skills[${index}]`);
6636
+ }
6637
+ }
5240
6638
  }
5241
6639
  loaded.set(ref.mode, {
5242
- path: ref.path,
6640
+ path: canonicalPath,
5243
6641
  scope: ref.scope,
5244
6642
  state: parsedState,
6643
+ originalContent: content,
6644
+ dev: fileStat.dev,
6645
+ ino: fileStat.ino,
5245
6646
  });
5246
6647
  }
5247
6648
  return loaded;
5248
6649
  };
5249
- let states = await loadStates(await listModeStateFilesWithScopePreference(cwd));
6650
+ const preferredRefs = writableScope.source === "root"
6651
+ ? (await readdir(writableScope.stateDir).catch(() => []))
6652
+ .filter((file) => isModeStateFilename(file))
6653
+ .map((file) => ({
6654
+ mode: file.slice(0, -"-state.json".length),
6655
+ path: join(writableScope.stateDir, file),
6656
+ scope: "root",
6657
+ }))
6658
+ : await listModeStateFilesWithScopePreference(cwd, writableScope.sessionId);
6659
+ const hasPreferredSessionStateFiles = preferredRefs.some((ref) => ref.scope === "session");
6660
+ const targetRefs = writableScope.source === "session"
6661
+ ? preferredRefs.filter((ref) => ref.scope === "session")
6662
+ : preferredRefs;
6663
+ let runSelection = null;
6664
+ let states = await loadStates(targetRefs, writableScope.stateDir, expectedOwnerIds);
5250
6665
  const hasActiveWorkflowMode = (entries) => [...entries.entries()].some(([mode, entry]) => mode !== SKILL_ACTIVE_STATE_MODE && entry.state.active === true);
5251
- if (!hasActiveWorkflowMode(states)) {
5252
- const runDirStates = await loadStates(await listHookVisibleRunDirStateRefs(cwd));
5253
- if (hasActiveWorkflowMode(runDirStates))
5254
- states = runDirStates;
6666
+ if (writableScope.source === "root"
6667
+ && !hasActiveWorkflowMode(states)
6668
+ && !hasPreferredSessionStateFiles) {
6669
+ runSelection = await selectAuthorizedHookVisibleRunDirState(cwd);
6670
+ if (runSelection) {
6671
+ const runOwnerIds = new Set([runSelection.sessionId]);
6672
+ const runDirStates = await loadStates(runSelection.refs, runSelection.sessionDir, runOwnerIds);
6673
+ if (hasActiveWorkflowMode(runDirStates))
6674
+ states = runDirStates;
6675
+ else
6676
+ runSelection = null;
6677
+ }
5255
6678
  }
5256
- const currentSessionId = writableScope.sessionId ?? "";
6679
+ const currentSessionId = writableScope.sessionId ?? runSelection?.sessionId ?? "";
5257
6680
  const changed = new Set();
5258
6681
  const reported = new Set();
5259
6682
  const cancelMode = (mode, phase = "cancelled", reportIfWasActive = true) => {
@@ -5284,13 +6707,74 @@ async function cancelModes(args = []) {
5284
6707
  if (reportIfWasActive && wasActive && mode !== SKILL_ACTIVE_STATE_MODE)
5285
6708
  reported.add(mode);
5286
6709
  };
5287
- const ralphLinksUltrawork = (state) => state.linked_ultrawork === true || state.linked_mode === "ultrawork";
6710
+ const linkedRalphMode = (state) => {
6711
+ if (state.linked_ultrawork === true || state.linked_mode === "ultrawork")
6712
+ return "ultrawork";
6713
+ if (state.linked_ecomode === true || state.linked_mode === "ecomode")
6714
+ return "ecomode";
6715
+ return undefined;
6716
+ };
6717
+ const cancelRalphSkillEntries = (linkedMode) => {
6718
+ const entry = states.get(SKILL_ACTIVE_STATE_MODE);
6719
+ if (!entry)
6720
+ return;
6721
+ const matchingSkills = new Set(["ralph", ...(linkedMode ? [linkedMode] : [])]);
6722
+ const activeSkills = Array.isArray(entry.state.active_skills)
6723
+ ? entry.state.active_skills
6724
+ : [];
6725
+ let changedSkillEntries = false;
6726
+ const nextSkills = activeSkills.map((skill) => {
6727
+ if (!skill || typeof skill !== "object" || Array.isArray(skill))
6728
+ return skill;
6729
+ const skillEntry = skill;
6730
+ if (skillEntry.active === false || typeof skillEntry.skill !== "string" || !matchingSkills.has(skillEntry.skill)) {
6731
+ return skill;
6732
+ }
6733
+ changedSkillEntries = true;
6734
+ return { ...skillEntry, active: false, phase: "cancelled" };
6735
+ });
6736
+ const remainingActiveSkills = nextSkills.filter((skill) => (!!skill && typeof skill === "object" && !Array.isArray(skill)
6737
+ && skill.active !== false));
6738
+ if (!changedSkillEntries) {
6739
+ if (entry.state.active !== true)
6740
+ return;
6741
+ if (typeof entry.state.skill !== "string" || !matchingSkills.has(entry.state.skill))
6742
+ return;
6743
+ }
6744
+ entry.state.active_skills = nextSkills;
6745
+ entry.state.updated_at = nowIso;
6746
+ entry.state.last_turn_at = nowIso;
6747
+ if (remainingActiveSkills.length > 0) {
6748
+ const primary = remainingActiveSkills[0];
6749
+ entry.state.active = true;
6750
+ entry.state.skill = primary.skill;
6751
+ if (typeof primary.phase === "string") {
6752
+ entry.state.phase = primary.phase;
6753
+ entry.state.current_phase = primary.phase;
6754
+ }
6755
+ else {
6756
+ delete entry.state.phase;
6757
+ delete entry.state.current_phase;
6758
+ }
6759
+ delete entry.state.completed_at;
6760
+ }
6761
+ else {
6762
+ entry.state.active = false;
6763
+ entry.state.skill = "";
6764
+ entry.state.phase = "cancelled";
6765
+ entry.state.current_phase = "cancelled";
6766
+ entry.state.completed_at = nowIso;
6767
+ }
6768
+ changed.add(SKILL_ACTIVE_STATE_MODE);
6769
+ };
5288
6770
  const ralph = states.get("ralph");
5289
6771
  const hadActiveRalph = !!(ralph && ralph.state.active === true);
5290
6772
  if (ralph && ralph.state.active === true) {
6773
+ const linkedMode = linkedRalphMode(ralph.state);
6774
+ if (linkedMode)
6775
+ cancelMode(linkedMode, "cancelled", true);
5291
6776
  cancelMode("ralph", "cancelled", true);
5292
- if (ralphLinksUltrawork(ralph.state))
5293
- cancelMode("ultrawork", "cancelled", true);
6777
+ cancelRalphSkillEntries(linkedMode);
5294
6778
  }
5295
6779
  if (!hadActiveRalph) {
5296
6780
  for (const [mode, entry] of states.entries()) {
@@ -5298,11 +6782,11 @@ async function cancelModes(args = []) {
5298
6782
  cancelMode(mode, "cancelled", true);
5299
6783
  }
5300
6784
  }
5301
- for (const [mode, entry] of states.entries()) {
5302
- if (!changed.has(mode))
5303
- continue;
5304
- await writeFile(entry.path, JSON.stringify(entry.state, null, 2));
5305
- }
6785
+ const assertRunAuthority = () => {
6786
+ if (runSelection && !hasMatchingMadmaxDetachedRuntimeBinding(runSelection.record)) {
6787
+ throw new Error("Refusing cancellation because detached run authority changed.");
6788
+ }
6789
+ };
5306
6790
  if (force && currentSessionId) {
5307
6791
  const stopStateEntries = [...states.entries()].filter(([mode]) => mode === "native-stop");
5308
6792
  for (const [, entry] of stopStateEntries) {
@@ -5313,10 +6797,85 @@ async function cancelModes(args = []) {
5313
6797
  continue;
5314
6798
  delete sessions[currentSessionId];
5315
6799
  entry.state.sessions = sessions;
5316
- await writeFile(entry.path, JSON.stringify(entry.state, null, 2));
5317
6800
  changed.add("native-stop");
5318
6801
  }
5319
6802
  }
6803
+ const orderedChanges = [...changed]
6804
+ .sort((left, right) => {
6805
+ if (left === "ralph")
6806
+ return 1;
6807
+ if (right === "ralph")
6808
+ return -1;
6809
+ return left.localeCompare(right);
6810
+ })
6811
+ .map((mode) => {
6812
+ const entry = states.get(mode);
6813
+ if (!entry)
6814
+ throw new Error(`Missing frozen cancellation entry for ${mode}.`);
6815
+ return { mode, entry, nextContent: JSON.stringify(entry.state, null, 2) };
6816
+ });
6817
+ const opened = [];
6818
+ try {
6819
+ for (const change of orderedChanges) {
6820
+ assertRunAuthority();
6821
+ const handle = await open(change.entry.path, fsConstants.O_RDWR | fsConstants.O_NOFOLLOW);
6822
+ const currentStat = await handle.stat();
6823
+ const currentContent = await handle.readFile({ encoding: "utf-8" });
6824
+ if (!currentStat.isFile() || currentStat.dev !== change.entry.dev || currentStat.ino !== change.entry.ino) {
6825
+ await handle.close();
6826
+ throw new Error(`Refusing cancellation because state identity changed: ${change.entry.path}.`);
6827
+ }
6828
+ if (currentContent !== change.entry.originalContent) {
6829
+ await handle.close();
6830
+ throw new Error(`Refusing cancellation because state content changed: ${change.entry.path}.`);
6831
+ }
6832
+ opened.push({ ...change, handle });
6833
+ }
6834
+ const committed = [];
6835
+ const cancellationTestWriteFailureMode = options.testFaults?.writeFailureMode;
6836
+ const cancellationTestRollbackFailureMode = options.testFaults?.rollbackFailureMode;
6837
+ try {
6838
+ for (const openedEntry of opened) {
6839
+ assertRunAuthority();
6840
+ committed.push(openedEntry);
6841
+ if (cancellationTestWriteFailureMode === openedEntry.mode) {
6842
+ throw new Error(`Injected cancellation write failure for ${openedEntry.mode}.`);
6843
+ }
6844
+ await openedEntry.handle.truncate(0);
6845
+ await openedEntry.handle.write(openedEntry.nextContent, 0, "utf-8");
6846
+ await openedEntry.handle.sync();
6847
+ }
6848
+ }
6849
+ catch (writeError) {
6850
+ let rollbackFailureCount = 0;
6851
+ const rollbackFailureSample = [];
6852
+ for (const committedEntry of committed.reverse()) {
6853
+ try {
6854
+ if (cancellationTestRollbackFailureMode === committedEntry.mode) {
6855
+ throw new Error(`Injected cancellation rollback failure for ${committedEntry.mode}.`);
6856
+ }
6857
+ await committedEntry.handle.truncate(0);
6858
+ await committedEntry.handle.write(committedEntry.entry.originalContent, 0, "utf-8");
6859
+ await committedEntry.handle.sync();
6860
+ }
6861
+ catch {
6862
+ rollbackFailureCount += 1;
6863
+ if (rollbackFailureSample.length < 3) {
6864
+ rollbackFailureSample.push(committedEntry.mode.replace(/[^A-Za-z0-9_-]/g, "_"));
6865
+ }
6866
+ }
6867
+ }
6868
+ if (rollbackFailureCount > 0) {
6869
+ const primaryMessage = writeError instanceof Error ? writeError.message : String(writeError);
6870
+ const sample = rollbackFailureSample.length > 0 ? `:${rollbackFailureSample.join(",")}` : "";
6871
+ throw new Error(`${primaryMessage} (cancellation_rollback_failed:${rollbackFailureCount}${sample}).`, { cause: writeError });
6872
+ }
6873
+ throw writeError;
6874
+ }
6875
+ }
6876
+ finally {
6877
+ await Promise.all(opened.map(({ handle }) => handle.close().catch(() => undefined)));
6878
+ }
5320
6879
  for (const mode of reported) {
5321
6880
  console.log(`Cancelled: ${mode}`);
5322
6881
  }
@@ -5326,7 +6885,11 @@ async function cancelModes(args = []) {
5326
6885
  }
5327
6886
  catch (err) {
5328
6887
  logCliOperationFailure(err);
5329
- console.log("No active modes to cancel.");
6888
+ process.exitCode = 1;
5330
6889
  }
5331
6890
  }
6891
+ /** Test-only entrypoint for deterministic transaction fault injection without runtime environment controls. */
6892
+ export async function cancelModesForTest(cwd, args, testFaults) {
6893
+ await cancelModes(args, { cwd, testFaults });
6894
+ }
5332
6895
  //# sourceMappingURL=index.js.map