oh-my-codex 0.20.2 → 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 (805) hide show
  1. package/Cargo.lock +36 -18
  2. package/Cargo.toml +1 -1
  3. package/README.md +28 -2
  4. package/crates/omx-runtime/Cargo.toml +4 -0
  5. package/crates/omx-runtime/src/main.rs +415 -2
  6. package/crates/omx-runtime/tests/execution.rs +483 -0
  7. package/crates/omx-runtime-core/src/dispatch.rs +96 -10
  8. package/crates/omx-runtime-core/src/engine.rs +839 -29
  9. package/crates/omx-runtime-core/src/lib.rs +8 -0
  10. package/dist/adapt/__tests__/herdr-target.test.d.ts +2 -0
  11. package/dist/adapt/__tests__/herdr-target.test.d.ts.map +1 -0
  12. package/dist/adapt/__tests__/herdr-target.test.js +72 -0
  13. package/dist/adapt/__tests__/herdr-target.test.js.map +1 -0
  14. package/dist/adapt/contracts.d.ts +1 -1
  15. package/dist/adapt/contracts.d.ts.map +1 -1
  16. package/dist/adapt/contracts.js +1 -1
  17. package/dist/adapt/contracts.js.map +1 -1
  18. package/dist/adapt/herdr/__tests__/authority.test.d.ts +2 -0
  19. package/dist/adapt/herdr/__tests__/authority.test.d.ts.map +1 -0
  20. package/dist/adapt/herdr/__tests__/authority.test.js +49 -0
  21. package/dist/adapt/herdr/__tests__/authority.test.js.map +1 -0
  22. package/dist/adapt/herdr/__tests__/bridge.test.d.ts +2 -0
  23. package/dist/adapt/herdr/__tests__/bridge.test.d.ts.map +1 -0
  24. package/dist/adapt/herdr/__tests__/bridge.test.js +176 -0
  25. package/dist/adapt/herdr/__tests__/bridge.test.js.map +1 -0
  26. package/dist/adapt/herdr/__tests__/sanitize.test.d.ts +2 -0
  27. package/dist/adapt/herdr/__tests__/sanitize.test.d.ts.map +1 -0
  28. package/dist/adapt/herdr/__tests__/sanitize.test.js +45 -0
  29. package/dist/adapt/herdr/__tests__/sanitize.test.js.map +1 -0
  30. package/dist/adapt/herdr/__tests__/semantic.test.d.ts +2 -0
  31. package/dist/adapt/herdr/__tests__/semantic.test.d.ts.map +1 -0
  32. package/dist/adapt/herdr/__tests__/semantic.test.js +130 -0
  33. package/dist/adapt/herdr/__tests__/semantic.test.js.map +1 -0
  34. package/dist/adapt/herdr/__tests__/seq-store.test.d.ts +2 -0
  35. package/dist/adapt/herdr/__tests__/seq-store.test.d.ts.map +1 -0
  36. package/dist/adapt/herdr/__tests__/seq-store.test.js +54 -0
  37. package/dist/adapt/herdr/__tests__/seq-store.test.js.map +1 -0
  38. package/dist/adapt/herdr/__tests__/socket-live.test.d.ts +2 -0
  39. package/dist/adapt/herdr/__tests__/socket-live.test.d.ts.map +1 -0
  40. package/dist/adapt/herdr/__tests__/socket-live.test.js +104 -0
  41. package/dist/adapt/herdr/__tests__/socket-live.test.js.map +1 -0
  42. package/dist/adapt/herdr/__tests__/team-map.test.d.ts +2 -0
  43. package/dist/adapt/herdr/__tests__/team-map.test.d.ts.map +1 -0
  44. package/dist/adapt/herdr/__tests__/team-map.test.js +46 -0
  45. package/dist/adapt/herdr/__tests__/team-map.test.js.map +1 -0
  46. package/dist/adapt/herdr/__tests__/transport.test.d.ts +2 -0
  47. package/dist/adapt/herdr/__tests__/transport.test.d.ts.map +1 -0
  48. package/dist/adapt/herdr/__tests__/transport.test.js +160 -0
  49. package/dist/adapt/herdr/__tests__/transport.test.js.map +1 -0
  50. package/dist/adapt/herdr/__tests__/wiring.test.d.ts +2 -0
  51. package/dist/adapt/herdr/__tests__/wiring.test.d.ts.map +1 -0
  52. package/dist/adapt/herdr/__tests__/wiring.test.js +118 -0
  53. package/dist/adapt/herdr/__tests__/wiring.test.js.map +1 -0
  54. package/dist/adapt/herdr/authority.d.ts +34 -0
  55. package/dist/adapt/herdr/authority.d.ts.map +1 -0
  56. package/dist/adapt/herdr/authority.js +71 -0
  57. package/dist/adapt/herdr/authority.js.map +1 -0
  58. package/dist/adapt/herdr/bridge.d.ts +84 -0
  59. package/dist/adapt/herdr/bridge.d.ts.map +1 -0
  60. package/dist/adapt/herdr/bridge.js +222 -0
  61. package/dist/adapt/herdr/bridge.js.map +1 -0
  62. package/dist/adapt/herdr/sanitize.d.ts +20 -0
  63. package/dist/adapt/herdr/sanitize.d.ts.map +1 -0
  64. package/dist/adapt/herdr/sanitize.js +70 -0
  65. package/dist/adapt/herdr/sanitize.js.map +1 -0
  66. package/dist/adapt/herdr/semantic.d.ts +67 -0
  67. package/dist/adapt/herdr/semantic.d.ts.map +1 -0
  68. package/dist/adapt/herdr/semantic.js +170 -0
  69. package/dist/adapt/herdr/semantic.js.map +1 -0
  70. package/dist/adapt/herdr/seq-store.d.ts +32 -0
  71. package/dist/adapt/herdr/seq-store.d.ts.map +1 -0
  72. package/dist/adapt/herdr/seq-store.js +126 -0
  73. package/dist/adapt/herdr/seq-store.js.map +1 -0
  74. package/dist/adapt/herdr/team-map.d.ts +30 -0
  75. package/dist/adapt/herdr/team-map.d.ts.map +1 -0
  76. package/dist/adapt/herdr/team-map.js +37 -0
  77. package/dist/adapt/herdr/team-map.js.map +1 -0
  78. package/dist/adapt/herdr/transport.d.ts +115 -0
  79. package/dist/adapt/herdr/transport.d.ts.map +1 -0
  80. package/dist/adapt/herdr/transport.js +260 -0
  81. package/dist/adapt/herdr/transport.js.map +1 -0
  82. package/dist/adapt/herdr/wiring.d.ts +24 -0
  83. package/dist/adapt/herdr/wiring.d.ts.map +1 -0
  84. package/dist/adapt/herdr/wiring.js +62 -0
  85. package/dist/adapt/herdr/wiring.js.map +1 -0
  86. package/dist/adapt/herdr.d.ts +19 -0
  87. package/dist/adapt/herdr.d.ts.map +1 -0
  88. package/dist/adapt/herdr.js +115 -0
  89. package/dist/adapt/herdr.js.map +1 -0
  90. package/dist/adapt/index.d.ts +4 -0
  91. package/dist/adapt/index.d.ts.map +1 -1
  92. package/dist/adapt/index.js +48 -0
  93. package/dist/adapt/index.js.map +1 -1
  94. package/dist/adapt/registry.d.ts.map +1 -1
  95. package/dist/adapt/registry.js +11 -0
  96. package/dist/adapt/registry.js.map +1 -1
  97. package/dist/agents/__tests__/definitions.test.js +81 -0
  98. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  99. package/dist/agents/__tests__/native-config.test.js +25 -0
  100. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  101. package/dist/agents/native-config.d.ts +2 -1
  102. package/dist/agents/native-config.d.ts.map +1 -1
  103. package/dist/agents/native-config.js.map +1 -1
  104. package/dist/auth/__tests__/hotswap.test.js +60 -1
  105. package/dist/auth/__tests__/hotswap.test.js.map +1 -1
  106. package/dist/auth/__tests__/storage.test.js +33 -1
  107. package/dist/auth/__tests__/storage.test.js.map +1 -1
  108. package/dist/auth/hotswap.d.ts +22 -2
  109. package/dist/auth/hotswap.d.ts.map +1 -1
  110. package/dist/auth/hotswap.js +107 -22
  111. package/dist/auth/hotswap.js.map +1 -1
  112. package/dist/auth/storage.js +1 -1
  113. package/dist/auth/storage.js.map +1 -1
  114. package/dist/autopilot/__tests__/ralplan-gate.test.js +86 -1333
  115. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  116. package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
  117. package/dist/autopilot/ralplan-gate.js +7 -6
  118. package/dist/autopilot/ralplan-gate.js.map +1 -1
  119. package/dist/cli/__tests__/agents-init.test.js +4 -23
  120. package/dist/cli/__tests__/agents-init.test.js.map +1 -1
  121. package/dist/cli/__tests__/api.test.js +61 -24
  122. package/dist/cli/__tests__/api.test.js.map +1 -1
  123. package/dist/cli/__tests__/auth.test.js +63 -7
  124. package/dist/cli/__tests__/auth.test.js.map +1 -1
  125. package/dist/cli/__tests__/codex-feature-probe.test.js +66 -1
  126. package/dist/cli/__tests__/codex-feature-probe.test.js.map +1 -1
  127. package/dist/cli/__tests__/codex-plugin-layout.test.js +150 -2
  128. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  129. package/dist/cli/__tests__/detached-leader-teardown.test.d.ts +2 -0
  130. package/dist/cli/__tests__/detached-leader-teardown.test.d.ts.map +1 -0
  131. package/dist/cli/__tests__/detached-leader-teardown.test.js +473 -0
  132. package/dist/cli/__tests__/detached-leader-teardown.test.js.map +1 -0
  133. package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts +2 -0
  134. package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts.map +1 -0
  135. package/dist/cli/__tests__/doctor-process-identity-readiness.test.js +103 -0
  136. package/dist/cli/__tests__/doctor-process-identity-readiness.test.js.map +1 -0
  137. package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts +2 -0
  138. package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts.map +1 -0
  139. package/dist/cli/__tests__/doctor-state-root-binding.test.js +139 -0
  140. package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -0
  141. package/dist/cli/__tests__/doctor-warning-copy.test.js +135 -1
  142. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  143. package/dist/cli/__tests__/exec.test.js +211 -0
  144. package/dist/cli/__tests__/exec.test.js.map +1 -1
  145. package/dist/cli/__tests__/index.test.js +878 -463
  146. package/dist/cli/__tests__/index.test.js.map +1 -1
  147. package/dist/cli/__tests__/launch-fallback.test.js +1290 -127
  148. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  149. package/dist/cli/__tests__/mcp-parity.test.js +8 -4
  150. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
  151. package/dist/cli/__tests__/native-assets.test.js +398 -16
  152. package/dist/cli/__tests__/native-assets.test.js.map +1 -1
  153. package/dist/cli/__tests__/native-hook-claim-journal.test.js +132 -6
  154. package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
  155. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +3 -3
  156. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  157. package/dist/cli/__tests__/ralph.test.js +3 -3
  158. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  159. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +49 -164
  160. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
  161. package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts +2 -0
  162. package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts.map +1 -0
  163. package/dist/cli/__tests__/ralplan-child-binding-3212.test.js +56 -0
  164. package/dist/cli/__tests__/ralplan-child-binding-3212.test.js.map +1 -0
  165. package/dist/cli/__tests__/ralplan.test.js +107 -109
  166. package/dist/cli/__tests__/ralplan.test.js.map +1 -1
  167. package/dist/cli/__tests__/resume.test.js +11 -10
  168. package/dist/cli/__tests__/resume.test.js.map +1 -1
  169. package/dist/cli/__tests__/session-scoped-runtime.test.js +1118 -43
  170. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  171. package/dist/cli/__tests__/session-search-help.test.js +25 -1
  172. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  173. package/dist/cli/__tests__/setup-agents-overwrite.test.js +8 -55
  174. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  175. package/dist/cli/__tests__/setup-install-mode.test.js +224 -7
  176. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  177. package/dist/cli/__tests__/sparkshell-cli.test.js +93 -41
  178. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  179. package/dist/cli/__tests__/team.test.js +150 -8
  180. package/dist/cli/__tests__/team.test.js.map +1 -1
  181. package/dist/cli/__tests__/ultragoal.test.js +117 -2
  182. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  183. package/dist/cli/__tests__/uninstall.test.js +95 -4
  184. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  185. package/dist/cli/__tests__/update.test.js +669 -177
  186. package/dist/cli/__tests__/update.test.js.map +1 -1
  187. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +33 -2
  188. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  189. package/dist/cli/api.d.ts.map +1 -1
  190. package/dist/cli/api.js +9 -10
  191. package/dist/cli/api.js.map +1 -1
  192. package/dist/cli/codex-feature-probe.d.ts +16 -0
  193. package/dist/cli/codex-feature-probe.d.ts.map +1 -1
  194. package/dist/cli/codex-feature-probe.js +44 -0
  195. package/dist/cli/codex-feature-probe.js.map +1 -1
  196. package/dist/cli/codex-home.d.ts.map +1 -1
  197. package/dist/cli/codex-home.js +7 -4
  198. package/dist/cli/codex-home.js.map +1 -1
  199. package/dist/cli/doctor.d.ts +20 -0
  200. package/dist/cli/doctor.d.ts.map +1 -1
  201. package/dist/cli/doctor.js +290 -3
  202. package/dist/cli/doctor.js.map +1 -1
  203. package/dist/cli/index.d.ts +234 -12
  204. package/dist/cli/index.d.ts.map +1 -1
  205. package/dist/cli/index.js +2409 -746
  206. package/dist/cli/index.js.map +1 -1
  207. package/dist/cli/mcp-parity.js +2 -2
  208. package/dist/cli/mcp-parity.js.map +1 -1
  209. package/dist/cli/native-assets.d.ts +25 -0
  210. package/dist/cli/native-assets.d.ts.map +1 -1
  211. package/dist/cli/native-assets.js +448 -51
  212. package/dist/cli/native-assets.js.map +1 -1
  213. package/dist/cli/native-hook-claim-journal.d.ts +20 -5
  214. package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
  215. package/dist/cli/native-hook-claim-journal.js +67 -32
  216. package/dist/cli/native-hook-claim-journal.js.map +1 -1
  217. package/dist/cli/package-manager-ownership.d.ts +63 -0
  218. package/dist/cli/package-manager-ownership.d.ts.map +1 -0
  219. package/dist/cli/package-manager-ownership.js +323 -0
  220. package/dist/cli/package-manager-ownership.js.map +1 -0
  221. package/dist/cli/ralph.js +2 -2
  222. package/dist/cli/ralph.js.map +1 -1
  223. package/dist/cli/ralplan.d.ts +5 -7
  224. package/dist/cli/ralplan.d.ts.map +1 -1
  225. package/dist/cli/ralplan.js +83 -134
  226. package/dist/cli/ralplan.js.map +1 -1
  227. package/dist/cli/session-search.d.ts +5 -0
  228. package/dist/cli/session-search.d.ts.map +1 -1
  229. package/dist/cli/session-search.js +87 -0
  230. package/dist/cli/session-search.js.map +1 -1
  231. package/dist/cli/setup-preferences.d.ts +15 -0
  232. package/dist/cli/setup-preferences.d.ts.map +1 -1
  233. package/dist/cli/setup-preferences.js +27 -1
  234. package/dist/cli/setup-preferences.js.map +1 -1
  235. package/dist/cli/setup.d.ts +8 -0
  236. package/dist/cli/setup.d.ts.map +1 -1
  237. package/dist/cli/setup.js +119 -47
  238. package/dist/cli/setup.js.map +1 -1
  239. package/dist/cli/sparkshell.d.ts.map +1 -1
  240. package/dist/cli/sparkshell.js +52 -9
  241. package/dist/cli/sparkshell.js.map +1 -1
  242. package/dist/cli/ultragoal.d.ts.map +1 -1
  243. package/dist/cli/ultragoal.js +2 -2
  244. package/dist/cli/ultragoal.js.map +1 -1
  245. package/dist/cli/uninstall.d.ts +9 -0
  246. package/dist/cli/uninstall.d.ts.map +1 -1
  247. package/dist/cli/uninstall.js +71 -30
  248. package/dist/cli/uninstall.js.map +1 -1
  249. package/dist/cli/update-worker.d.ts +2 -0
  250. package/dist/cli/update-worker.d.ts.map +1 -0
  251. package/dist/cli/update-worker.js +142 -0
  252. package/dist/cli/update-worker.js.map +1 -0
  253. package/dist/cli/update.d.ts +14 -22
  254. package/dist/cli/update.d.ts.map +1 -1
  255. package/dist/cli/update.js +192 -241
  256. package/dist/cli/update.js.map +1 -1
  257. package/dist/compat/__tests__/rust-runtime-compat.test.js +1 -1
  258. package/dist/config/__tests__/generator-idempotent.test.js +503 -30
  259. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  260. package/dist/config/__tests__/generator-notify.test.js +5 -3
  261. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  262. package/dist/config/__tests__/models.test.js +67 -3
  263. package/dist/config/__tests__/models.test.js.map +1 -1
  264. package/dist/config/generator.d.ts +27 -21
  265. package/dist/config/generator.d.ts.map +1 -1
  266. package/dist/config/generator.js +610 -152
  267. package/dist/config/generator.js.map +1 -1
  268. package/dist/config/models.d.ts +12 -2
  269. package/dist/config/models.d.ts.map +1 -1
  270. package/dist/config/models.js +15 -1
  271. package/dist/config/models.js.map +1 -1
  272. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +25 -0
  273. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
  274. package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -1
  275. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
  276. package/dist/goal-workflows/codex-goal-snapshot.js +2 -0
  277. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
  278. package/dist/hooks/__tests__/agents-overlay.test.js +5 -3
  279. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  280. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +27 -4
  281. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  282. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts +2 -0
  283. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts.map +1 -0
  284. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js +172 -0
  285. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js.map +1 -0
  286. package/dist/hooks/__tests__/keyword-detector.test.js +176 -37
  287. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  288. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +402 -545
  289. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  290. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +39 -4
  291. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  292. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +161 -26
  293. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  294. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +27 -0
  295. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  296. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +10 -118
  297. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
  298. package/dist/hooks/__tests__/notify-hook-regression-205.test.js +7 -0
  299. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
  300. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +533 -14
  301. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  302. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +159 -24
  303. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  304. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +482 -32
  305. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  306. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js +2 -1
  307. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js.map +1 -1
  308. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +68 -1
  309. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  310. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +7 -1
  311. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
  312. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +44 -4
  313. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  314. package/dist/hooks/__tests__/prompt-guidance-contract.test.js +9 -0
  315. package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
  316. package/dist/hooks/__tests__/session.test.js +3130 -365
  317. package/dist/hooks/__tests__/session.test.js.map +1 -1
  318. package/dist/hooks/agents-overlay.js +1 -1
  319. package/dist/hooks/agents-overlay.js.map +1 -1
  320. package/dist/hooks/extensibility/__tests__/dispatcher.test.js +108 -2
  321. package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
  322. package/dist/hooks/extensibility/__tests__/sdk.test.js +151 -1
  323. package/dist/hooks/extensibility/__tests__/sdk.test.js.map +1 -1
  324. package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
  325. package/dist/hooks/extensibility/dispatcher.js +20 -3
  326. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  327. package/dist/hooks/extensibility/plugin-runner.js +1 -0
  328. package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
  329. package/dist/hooks/extensibility/runtime.d.ts.map +1 -1
  330. package/dist/hooks/extensibility/runtime.js.map +1 -1
  331. package/dist/hooks/extensibility/sdk/paths.d.ts +1 -1
  332. package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -1
  333. package/dist/hooks/extensibility/sdk/paths.js +2 -2
  334. package/dist/hooks/extensibility/sdk/paths.js.map +1 -1
  335. package/dist/hooks/extensibility/sdk/runtime-state.d.ts +1 -1
  336. package/dist/hooks/extensibility/sdk/runtime-state.d.ts.map +1 -1
  337. package/dist/hooks/extensibility/sdk/runtime-state.js +55 -15
  338. package/dist/hooks/extensibility/sdk/runtime-state.js.map +1 -1
  339. package/dist/hooks/extensibility/sdk.d.ts +1 -0
  340. package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
  341. package/dist/hooks/extensibility/sdk.js +1 -1
  342. package/dist/hooks/extensibility/sdk.js.map +1 -1
  343. package/dist/hooks/extensibility/types.d.ts +7 -0
  344. package/dist/hooks/extensibility/types.d.ts.map +1 -1
  345. package/dist/hooks/keyword-detector.d.ts +10 -3
  346. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  347. package/dist/hooks/keyword-detector.js +116 -20
  348. package/dist/hooks/keyword-detector.js.map +1 -1
  349. package/dist/hooks/session.d.ts +204 -14
  350. package/dist/hooks/session.d.ts.map +1 -1
  351. package/dist/hooks/session.js +1500 -223
  352. package/dist/hooks/session.js.map +1 -1
  353. package/dist/hud/__tests__/hud-tmux-injection.test.js +6 -6
  354. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  355. package/dist/hud/__tests__/index.test.js +26 -16
  356. package/dist/hud/__tests__/index.test.js.map +1 -1
  357. package/dist/hud/__tests__/reconcile.test.js +83 -78
  358. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  359. package/dist/hud/__tests__/state.test.js +36 -31
  360. package/dist/hud/__tests__/state.test.js.map +1 -1
  361. package/dist/hud/__tests__/tmux.test.js +155 -198
  362. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  363. package/dist/hud/tmux.d.ts +42 -4
  364. package/dist/hud/tmux.d.ts.map +1 -1
  365. package/dist/hud/tmux.js +1017 -139
  366. package/dist/hud/tmux.js.map +1 -1
  367. package/dist/leader/__tests__/contract.test.js +124 -37
  368. package/dist/leader/__tests__/contract.test.js.map +1 -1
  369. package/dist/leader/contract.d.ts +17 -6
  370. package/dist/leader/contract.d.ts.map +1 -1
  371. package/dist/leader/contract.js +102 -26
  372. package/dist/leader/contract.js.map +1 -1
  373. package/dist/mcp/__tests__/code-intel-server.test.js +31 -3
  374. package/dist/mcp/__tests__/code-intel-server.test.js.map +1 -1
  375. package/dist/mcp/__tests__/hermes-bridge.test.js +17 -0
  376. package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
  377. package/dist/mcp/__tests__/server-lifecycle.test.js +4 -4
  378. package/dist/mcp/__tests__/state-paths.test.js +735 -4
  379. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  380. package/dist/mcp/__tests__/state-server.test.js +1 -1
  381. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  382. package/dist/mcp/__tests__/trace-server.test.js +2 -2
  383. package/dist/mcp/__tests__/trace-server.test.js.map +1 -1
  384. package/dist/mcp/__tests__/wiki-server.test.js +18 -0
  385. package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
  386. package/dist/mcp/code-intel-server.d.ts.map +1 -1
  387. package/dist/mcp/code-intel-server.js +13 -3
  388. package/dist/mcp/code-intel-server.js.map +1 -1
  389. package/dist/mcp/hermes-bridge.d.ts.map +1 -1
  390. package/dist/mcp/hermes-bridge.js +2 -2
  391. package/dist/mcp/hermes-bridge.js.map +1 -1
  392. package/dist/mcp/state-paths.d.ts +78 -4
  393. package/dist/mcp/state-paths.d.ts.map +1 -1
  394. package/dist/mcp/state-paths.js +543 -40
  395. package/dist/mcp/state-paths.js.map +1 -1
  396. package/dist/mcp/state-server.d.ts +4 -4
  397. package/dist/mcp/state-server.d.ts.map +1 -1
  398. package/dist/mcp/wiki-server.d.ts +11 -0
  399. package/dist/mcp/wiki-server.d.ts.map +1 -1
  400. package/dist/mcp/wiki-server.js +9 -2
  401. package/dist/mcp/wiki-server.js.map +1 -1
  402. package/dist/modes/__tests__/base-autopilot-gates.test.js +36 -7
  403. package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
  404. package/dist/modes/__tests__/base-multi-state-compat.test.js +1 -1
  405. package/dist/modes/__tests__/base-multi-state-compat.test.js.map +1 -1
  406. package/dist/modes/__tests__/base-session-scope.test.js +17 -8
  407. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  408. package/dist/modes/__tests__/base-tmux-pane.test.js +4 -4
  409. package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
  410. package/dist/modes/base.d.ts +5 -5
  411. package/dist/modes/base.d.ts.map +1 -1
  412. package/dist/modes/base.js +76 -21
  413. package/dist/modes/base.js.map +1 -1
  414. package/dist/native-assets/archive.d.ts +17 -0
  415. package/dist/native-assets/archive.d.ts.map +1 -0
  416. package/dist/native-assets/archive.js +238 -0
  417. package/dist/native-assets/archive.js.map +1 -0
  418. package/dist/native-assets/policy.d.ts +42 -0
  419. package/dist/native-assets/policy.d.ts.map +1 -0
  420. package/dist/native-assets/policy.js +155 -0
  421. package/dist/native-assets/policy.js.map +1 -0
  422. package/dist/notifications/temp-contract.d.ts.map +1 -1
  423. package/dist/notifications/temp-contract.js +4 -0
  424. package/dist/notifications/temp-contract.js.map +1 -1
  425. package/dist/pipeline/__tests__/orchestrator.test.js +164 -42
  426. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  427. package/dist/pipeline/__tests__/stages.test.js +72 -155
  428. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  429. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  430. package/dist/pipeline/orchestrator.js +124 -18
  431. package/dist/pipeline/orchestrator.js.map +1 -1
  432. package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
  433. package/dist/pipeline/stages/ralplan.js +36 -8
  434. package/dist/pipeline/stages/ralplan.js.map +1 -1
  435. package/dist/ralplan/__tests__/consensus-gate.test.js +73 -1316
  436. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
  437. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts +2 -0
  438. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts.map +1 -0
  439. package/dist/ralplan/__tests__/documented-leader-preflight.test.js +335 -0
  440. package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -0
  441. package/dist/ralplan/__tests__/runtime.test.js +85 -101
  442. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  443. package/dist/ralplan/consensus-gate.d.ts +9 -1
  444. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  445. package/dist/ralplan/consensus-gate.js +82 -338
  446. package/dist/ralplan/consensus-gate.js.map +1 -1
  447. package/dist/ralplan/documented-leader-preflight.d.ts +41 -0
  448. package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -0
  449. package/dist/ralplan/documented-leader-preflight.js +360 -0
  450. package/dist/ralplan/documented-leader-preflight.js.map +1 -0
  451. package/dist/ralplan/runtime.d.ts +2 -1
  452. package/dist/ralplan/runtime.d.ts.map +1 -1
  453. package/dist/ralplan/runtime.js +67 -168
  454. package/dist/ralplan/runtime.js.map +1 -1
  455. package/dist/runtime/__tests__/bridge.test.js +42 -1
  456. package/dist/runtime/__tests__/bridge.test.js.map +1 -1
  457. package/dist/runtime/bridge.d.ts +12 -0
  458. package/dist/runtime/bridge.d.ts.map +1 -1
  459. package/dist/runtime/bridge.js +52 -1
  460. package/dist/runtime/bridge.js.map +1 -1
  461. package/dist/runtime/run-state.d.ts +5 -1
  462. package/dist/runtime/run-state.d.ts.map +1 -1
  463. package/dist/runtime/run-state.js +15 -8
  464. package/dist/runtime/run-state.js.map +1 -1
  465. package/dist/scripts/__tests__/codex-native-hook.test.js +16323 -8978
  466. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  467. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts +2 -0
  468. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts.map +1 -0
  469. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js +225 -0
  470. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js.map +1 -0
  471. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts +2 -0
  472. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts.map +1 -0
  473. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js +188 -0
  474. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js.map +1 -0
  475. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts +2 -0
  476. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts.map +1 -0
  477. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js +259 -0
  478. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js.map +1 -0
  479. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts +2 -0
  480. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts.map +1 -0
  481. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js +491 -0
  482. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js.map +1 -0
  483. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts +2 -0
  484. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts.map +1 -0
  485. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js +708 -0
  486. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js.map +1 -0
  487. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts +2 -0
  488. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts.map +1 -0
  489. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js +208 -0
  490. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js.map +1 -0
  491. package/dist/scripts/__tests__/notify-state-io.test.js +26 -0
  492. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  493. package/dist/scripts/__tests__/notify-tmux-injection.test.js +23 -1
  494. package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -1
  495. package/dist/scripts/__tests__/postinstall.test.js +19 -1
  496. package/dist/scripts/__tests__/postinstall.test.js.map +1 -1
  497. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts +2 -0
  498. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts.map +1 -0
  499. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +108 -0
  500. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -0
  501. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +63 -173
  502. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
  503. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +74 -159
  504. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
  505. package/dist/scripts/__tests__/smoke-packed-install.test.js +404 -13
  506. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  507. package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts +2 -0
  508. package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts.map +1 -0
  509. package/dist/scripts/__tests__/verify-native-release-assets.test.js +235 -0
  510. package/dist/scripts/__tests__/verify-native-release-assets.test.js.map +1 -0
  511. package/dist/scripts/codex-native-hook.d.ts +53 -1
  512. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  513. package/dist/scripts/codex-native-hook.js +14448 -1480
  514. package/dist/scripts/codex-native-hook.js.map +1 -1
  515. package/dist/scripts/codex-native-pre-post.d.ts +9 -0
  516. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  517. package/dist/scripts/codex-native-pre-post.js +35 -12
  518. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  519. package/dist/scripts/generate-native-release-manifest.js +80 -51
  520. package/dist/scripts/generate-native-release-manifest.js.map +1 -1
  521. package/dist/scripts/notify-fallback-watcher.js +78 -115
  522. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  523. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  524. package/dist/scripts/notify-hook/auto-nudge.js +74 -3
  525. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  526. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  527. package/dist/scripts/notify-hook/state-io.js +5 -0
  528. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  529. package/dist/scripts/notify-hook/team-dispatch.d.ts +9 -0
  530. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  531. package/dist/scripts/notify-hook/team-dispatch.js +234 -79
  532. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  533. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  534. package/dist/scripts/notify-hook/team-leader-nudge.js +71 -16
  535. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  536. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +12 -4
  537. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  538. package/dist/scripts/notify-hook/team-tmux-guard.js +365 -44
  539. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  540. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  541. package/dist/scripts/notify-hook/team-worker-stop.js +42 -21
  542. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  543. package/dist/scripts/notify-hook/team-worker.d.ts +3 -0
  544. package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
  545. package/dist/scripts/notify-hook/team-worker.js +82 -33
  546. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  547. package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -0
  548. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  549. package/dist/scripts/notify-hook/tmux-injection.js +32 -3
  550. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  551. package/dist/scripts/notify-hook.js +79 -82
  552. package/dist/scripts/notify-hook.js.map +1 -1
  553. package/dist/scripts/postinstall-advisory.d.ts +14 -0
  554. package/dist/scripts/postinstall-advisory.d.ts.map +1 -0
  555. package/dist/scripts/postinstall-advisory.js +49 -0
  556. package/dist/scripts/postinstall-advisory.js.map +1 -0
  557. package/dist/scripts/postinstall.d.ts +1 -1
  558. package/dist/scripts/postinstall.d.ts.map +1 -1
  559. package/dist/scripts/postinstall.js +1 -1
  560. package/dist/scripts/postinstall.js.map +1 -1
  561. package/dist/scripts/smoke-packed-install.d.ts +80 -4
  562. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  563. package/dist/scripts/smoke-packed-install.js +3162 -26
  564. package/dist/scripts/smoke-packed-install.js.map +1 -1
  565. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  566. package/dist/scripts/sync-plugin-mirror.js +16 -1
  567. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  568. package/dist/scripts/verify-native-release-assets.js +37 -23
  569. package/dist/scripts/verify-native-release-assets.js.map +1 -1
  570. package/dist/state/__tests__/mode-state-context.test.js +61 -0
  571. package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
  572. package/dist/state/__tests__/operations.test.js +462 -850
  573. package/dist/state/__tests__/operations.test.js.map +1 -1
  574. package/dist/state/__tests__/skill-active.test.js +400 -2
  575. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  576. package/dist/state/mode-state-context.d.ts +8 -0
  577. package/dist/state/mode-state-context.d.ts.map +1 -1
  578. package/dist/state/mode-state-context.js +73 -0
  579. package/dist/state/mode-state-context.js.map +1 -1
  580. package/dist/state/operations.d.ts +3 -0
  581. package/dist/state/operations.d.ts.map +1 -1
  582. package/dist/state/operations.js +144 -40
  583. package/dist/state/operations.js.map +1 -1
  584. package/dist/state/skill-active.d.ts +18 -1
  585. package/dist/state/skill-active.d.ts.map +1 -1
  586. package/dist/state/skill-active.js +420 -23
  587. package/dist/state/skill-active.js.map +1 -1
  588. package/dist/state/workflow-transition-reconcile.d.ts +3 -0
  589. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  590. package/dist/state/workflow-transition-reconcile.js +10 -4
  591. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  592. package/dist/state/workflow-transition.d.ts.map +1 -1
  593. package/dist/state/workflow-transition.js +3 -1
  594. package/dist/state/workflow-transition.js.map +1 -1
  595. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +53 -247
  596. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
  597. package/dist/subagents/__tests__/tracker.test.js +837 -1048
  598. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  599. package/dist/subagents/tracker.d.ts +58 -135
  600. package/dist/subagents/tracker.d.ts.map +1 -1
  601. package/dist/subagents/tracker.js +811 -632
  602. package/dist/subagents/tracker.js.map +1 -1
  603. package/dist/team/__tests__/api-interop.test.js +91 -1
  604. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  605. package/dist/team/__tests__/delivery-e2e-smoke.test.js +58 -17
  606. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  607. package/dist/team/__tests__/idle-nudge.test.js +45 -15
  608. package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
  609. package/dist/team/__tests__/mcp-comm.test.js +197 -30
  610. package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
  611. package/dist/team/__tests__/model-contract.test.js +110 -3
  612. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  613. package/dist/team/__tests__/notice-ledger.test.d.ts +2 -0
  614. package/dist/team/__tests__/notice-ledger.test.d.ts.map +1 -0
  615. package/dist/team/__tests__/notice-ledger.test.js +131 -0
  616. package/dist/team/__tests__/notice-ledger.test.js.map +1 -0
  617. package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts +2 -0
  618. package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts.map +1 -0
  619. package/dist/team/__tests__/pane-teardown-residue-e2e.test.js +410 -0
  620. package/dist/team/__tests__/pane-teardown-residue-e2e.test.js.map +1 -0
  621. package/dist/team/__tests__/phase-controller.test.js +20 -0
  622. package/dist/team/__tests__/phase-controller.test.js.map +1 -1
  623. package/dist/team/__tests__/runtime-cli.test.js +9 -0
  624. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  625. package/dist/team/__tests__/runtime.test.js +3963 -428
  626. package/dist/team/__tests__/runtime.test.js.map +1 -1
  627. package/dist/team/__tests__/scaling.test.js +2405 -203
  628. package/dist/team/__tests__/scaling.test.js.map +1 -1
  629. package/dist/team/__tests__/state-root.test.js +117 -2
  630. package/dist/team/__tests__/state-root.test.js.map +1 -1
  631. package/dist/team/__tests__/state.test.js +804 -36
  632. package/dist/team/__tests__/state.test.js.map +1 -1
  633. package/dist/team/__tests__/team-ops-contract.test.js +6 -0
  634. package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
  635. package/dist/team/__tests__/tmux-session.test.js +4099 -1039
  636. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  637. package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts +2 -0
  638. package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts.map +1 -0
  639. package/dist/team/__tests__/tmux-source-authority-realtmux.test.js +149 -0
  640. package/dist/team/__tests__/tmux-source-authority-realtmux.test.js.map +1 -0
  641. package/dist/team/__tests__/tmux-test-fixture.d.ts +69 -0
  642. package/dist/team/__tests__/tmux-test-fixture.d.ts.map +1 -1
  643. package/dist/team/__tests__/tmux-test-fixture.js +242 -15
  644. package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
  645. package/dist/team/__tests__/tmux-test-fixture.test.js +177 -1
  646. package/dist/team/__tests__/tmux-test-fixture.test.js.map +1 -1
  647. package/dist/team/__tests__/worker-runtime-identity.test.js +53 -44
  648. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  649. package/dist/team/__tests__/worktree.test.js +6 -0
  650. package/dist/team/__tests__/worktree.test.js.map +1 -1
  651. package/dist/team/api-interop.d.ts.map +1 -1
  652. package/dist/team/api-interop.js +15 -9
  653. package/dist/team/api-interop.js.map +1 -1
  654. package/dist/team/exact-pane.d.ts +23 -0
  655. package/dist/team/exact-pane.d.ts.map +1 -0
  656. package/dist/team/exact-pane.js +133 -0
  657. package/dist/team/exact-pane.js.map +1 -0
  658. package/dist/team/idle-nudge.d.ts +12 -1
  659. package/dist/team/idle-nudge.d.ts.map +1 -1
  660. package/dist/team/idle-nudge.js +41 -14
  661. package/dist/team/idle-nudge.js.map +1 -1
  662. package/dist/team/model-contract.d.ts +2 -5
  663. package/dist/team/model-contract.d.ts.map +1 -1
  664. package/dist/team/model-contract.js +59 -17
  665. package/dist/team/model-contract.js.map +1 -1
  666. package/dist/team/notice-ledger.d.ts +54 -0
  667. package/dist/team/notice-ledger.d.ts.map +1 -0
  668. package/dist/team/notice-ledger.js +259 -0
  669. package/dist/team/notice-ledger.js.map +1 -0
  670. package/dist/team/phase-controller.d.ts.map +1 -1
  671. package/dist/team/phase-controller.js +2 -0
  672. package/dist/team/phase-controller.js.map +1 -1
  673. package/dist/team/runtime.d.ts +4 -1
  674. package/dist/team/runtime.d.ts.map +1 -1
  675. package/dist/team/runtime.js +1527 -291
  676. package/dist/team/runtime.js.map +1 -1
  677. package/dist/team/scaling.d.ts +13 -1
  678. package/dist/team/scaling.d.ts.map +1 -1
  679. package/dist/team/scaling.js +1503 -387
  680. package/dist/team/scaling.js.map +1 -1
  681. package/dist/team/state/dispatch.d.ts.map +1 -1
  682. package/dist/team/state/dispatch.js +26 -8
  683. package/dist/team/state/dispatch.js.map +1 -1
  684. package/dist/team/state/locks.d.ts.map +1 -1
  685. package/dist/team/state/locks.js +61 -13
  686. package/dist/team/state/locks.js.map +1 -1
  687. package/dist/team/state/monitor.d.ts +10 -0
  688. package/dist/team/state/monitor.d.ts.map +1 -1
  689. package/dist/team/state/monitor.js +13 -0
  690. package/dist/team/state/monitor.js.map +1 -1
  691. package/dist/team/state/tasks.d.ts.map +1 -1
  692. package/dist/team/state/tasks.js +4 -3
  693. package/dist/team/state/tasks.js.map +1 -1
  694. package/dist/team/state/types.d.ts +15 -0
  695. package/dist/team/state/types.d.ts.map +1 -1
  696. package/dist/team/state/types.js.map +1 -1
  697. package/dist/team/state-root.d.ts +12 -0
  698. package/dist/team/state-root.d.ts.map +1 -1
  699. package/dist/team/state-root.js +177 -0
  700. package/dist/team/state-root.js.map +1 -1
  701. package/dist/team/state.d.ts +85 -1
  702. package/dist/team/state.d.ts.map +1 -1
  703. package/dist/team/state.js +672 -129
  704. package/dist/team/state.js.map +1 -1
  705. package/dist/team/team-ops.d.ts +5 -0
  706. package/dist/team/team-ops.d.ts.map +1 -1
  707. package/dist/team/team-ops.js +4 -0
  708. package/dist/team/team-ops.js.map +1 -1
  709. package/dist/team/tmux-session.d.ts +163 -27
  710. package/dist/team/tmux-session.d.ts.map +1 -1
  711. package/dist/team/tmux-session.js +1682 -376
  712. package/dist/team/tmux-session.js.map +1 -1
  713. package/dist/team/worktree.d.ts.map +1 -1
  714. package/dist/team/worktree.js +4 -0
  715. package/dist/team/worktree.js.map +1 -1
  716. package/dist/ultragoal/__tests__/artifacts.test.js +950 -2
  717. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  718. package/dist/ultragoal/__tests__/docs-contract.test.js +1 -0
  719. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  720. package/dist/ultragoal/artifacts.d.ts +35 -1
  721. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  722. package/dist/ultragoal/artifacts.js +232 -46
  723. package/dist/ultragoal/artifacts.js.map +1 -1
  724. package/dist/utils/__tests__/file-durability.test.d.ts +2 -0
  725. package/dist/utils/__tests__/file-durability.test.d.ts.map +1 -0
  726. package/dist/utils/__tests__/file-durability.test.js +114 -0
  727. package/dist/utils/__tests__/file-durability.test.js.map +1 -0
  728. package/dist/utils/__tests__/paths.test.js +21 -1
  729. package/dist/utils/__tests__/paths.test.js.map +1 -1
  730. package/dist/utils/file-durability.d.ts +25 -0
  731. package/dist/utils/file-durability.d.ts.map +1 -0
  732. package/dist/utils/file-durability.js +67 -0
  733. package/dist/utils/file-durability.js.map +1 -0
  734. package/dist/utils/paths.d.ts.map +1 -1
  735. package/dist/utils/paths.js +14 -6
  736. package/dist/utils/paths.js.map +1 -1
  737. package/dist/verification/__tests__/ci-rust-gates.test.js +49 -4
  738. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  739. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +3 -0
  740. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
  741. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +60 -0
  742. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  743. package/dist/verification/__tests__/native-release-manifest.test.js +157 -4
  744. package/dist/verification/__tests__/native-release-manifest.test.js.map +1 -1
  745. package/dist/wiki/lint.d.ts +4 -1
  746. package/dist/wiki/lint.d.ts.map +1 -1
  747. package/dist/wiki/lint.js +3 -2
  748. package/dist/wiki/lint.js.map +1 -1
  749. package/package.json +13 -5
  750. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  751. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +105 -17
  752. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +13 -5
  753. package/plugins/oh-my-codex/skills/cancel/SKILL.md +30 -85
  754. package/plugins/oh-my-codex/skills/ralph/SKILL.md +6 -4
  755. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +16 -17
  756. package/plugins/oh-my-codex/skills/team/SKILL.md +13 -9
  757. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +3 -2
  758. package/skills/autopilot/SKILL.md +13 -5
  759. package/skills/cancel/SKILL.md +30 -85
  760. package/skills/ralph/SKILL.md +6 -4
  761. package/skills/ralplan/SKILL.md +16 -17
  762. package/skills/team/SKILL.md +13 -9
  763. package/skills/ultragoal/SKILL.md +3 -2
  764. package/src/scripts/__tests__/codex-native-hook.test.ts +8831 -951
  765. package/src/scripts/__tests__/issue-3293-callsite-parity.test.ts +187 -0
  766. package/src/scripts/__tests__/issue-3293-cancel-transaction.test.ts +189 -0
  767. package/src/scripts/__tests__/issue-3293-hook-owned-cancel.test.ts +187 -0
  768. package/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts +591 -0
  769. package/src/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.ts +701 -0
  770. package/src/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.ts +211 -0
  771. package/src/scripts/__tests__/notify-state-io.test.ts +29 -0
  772. package/src/scripts/__tests__/notify-tmux-injection.test.ts +26 -1
  773. package/src/scripts/__tests__/postinstall.test.ts +23 -1
  774. package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +129 -0
  775. package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +67 -185
  776. package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +79 -178
  777. package/src/scripts/__tests__/smoke-packed-install.test.ts +525 -16
  778. package/src/scripts/__tests__/verify-native-release-assets.test.ts +240 -0
  779. package/src/scripts/codex-native-hook.ts +14238 -1429
  780. package/src/scripts/codex-native-pre-post.ts +43 -12
  781. package/src/scripts/generate-native-release-manifest.ts +83 -79
  782. package/src/scripts/notify-fallback-watcher.ts +91 -122
  783. package/src/scripts/notify-hook/auto-nudge.ts +71 -3
  784. package/src/scripts/notify-hook/state-io.ts +11 -0
  785. package/src/scripts/notify-hook/team-dispatch.ts +242 -75
  786. package/src/scripts/notify-hook/team-leader-nudge.ts +72 -22
  787. package/src/scripts/notify-hook/team-tmux-guard.ts +366 -39
  788. package/src/scripts/notify-hook/team-worker-stop.ts +43 -20
  789. package/src/scripts/notify-hook/team-worker.ts +82 -31
  790. package/src/scripts/notify-hook/tmux-injection.ts +33 -3
  791. package/src/scripts/notify-hook.ts +84 -82
  792. package/src/scripts/postinstall-advisory.ts +67 -0
  793. package/src/scripts/postinstall.ts +1 -1
  794. package/src/scripts/smoke-packed-install.ts +3608 -39
  795. package/src/scripts/sync-plugin-mirror.ts +17 -1
  796. package/src/scripts/verify-native-release-assets.ts +39 -31
  797. package/templates/AGENTS.md +1 -0
  798. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +0 -2
  799. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +0 -1
  800. package/dist/subagents/__tests__/adapted-role-binding.test.js +0 -988
  801. package/dist/subagents/__tests__/adapted-role-binding.test.js.map +0 -1
  802. package/dist/subagents/adapted-role-binding.d.ts +0 -17
  803. package/dist/subagents/adapted-role-binding.d.ts.map +0 -1
  804. package/dist/subagents/adapted-role-binding.js +0 -75
  805. package/dist/subagents/adapted-role-binding.js.map +0 -1
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";
@@ -39,25 +39,25 @@ import { adaptCommand } from "./adapt.js";
39
39
  import { listCommand } from "./list.js";
40
40
  import { authCommand } from "./auth.js";
41
41
  import { missionCommand } from "./mission.js";
42
- import { runAuthHotswap } from "../auth/hotswap.js";
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, 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";
48
48
  import { discoverProjectRuntimeCodexHomes } from "./project-runtime-codex-homes.js";
49
49
  import { discoverOmxPluginCacheDirs, hasLocalOmxPluginEnablement, materializePackagedOmxPluginCache, packagedOmxPluginVersion, resolvePackagedOmxMarketplace, upsertLocalOmxMarketplaceRegistration, upsertLocalOmxPluginEnablement, } from "./plugin-marketplace.js";
50
50
  import { escapeTomlString, readTopLevelTomlString, upsertTopLevelTomlString } from "../utils/toml.js";
51
- import { CANONICAL_REASONING_EFFORTS, isAmbiguousUnsupportedReasoningEffort, } from "../config/models.js";
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";
@@ -123,6 +124,7 @@ Usage:
123
124
  omx explore DEPRECATED compatibility command; use normal repo inspection or omx sparkshell
124
125
  omx api Run native omx-api localhost gateway commands (serve|status|stop|generate)
125
126
  omx session Search and summarize local session history (--codex-home <path> escape hatch)
127
+ Includes session lock inspect/recover diagnostics
126
128
  omx url Passive URL reader (read <url> --json)
127
129
  omx capabilities
128
130
  Lock/check deterministic configured tool, skill, agent, and observation surfaces
@@ -133,7 +135,8 @@ Usage:
133
135
  Alias for agents-init (lightweight AGENTS bootstrap only)
134
136
  omx team Spawn parallel worker panes in tmux and bootstrap inbox/task state
135
137
  omx ralph Launch Codex with ralph persistence mode active
136
- 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
137
140
  omx ultragoal Create, resume, and checkpoint durable multi-goal plans over Codex goal mode
138
141
  omx performance-goal
139
142
  Create, hand off, and gate evaluator-backed performance goals
@@ -235,10 +238,15 @@ const OMX_MODEL_INSTRUCTIONS_FILE_ENV = "OMX_MODEL_INSTRUCTIONS_FILE";
235
238
  const OMX_INSTANCE_OPTION = "@omx_instance_id";
236
239
  const OMX_RALPH_APPEND_INSTRUCTIONS_FILE_ENV = "OMX_RALPH_APPEND_INSTRUCTIONS_FILE";
237
240
  const OMX_AUTORESEARCH_APPEND_INSTRUCTIONS_FILE_ENV = "OMX_AUTORESEARCH_APPEND_INSTRUCTIONS_FILE";
238
- const REASONING_MODES = CANONICAL_REASONING_EFFORTS;
241
+ const REASONING_MODES = ROOT_REASONING_EFFORTS;
239
242
  const REASONING_MODE_SET = new Set(REASONING_MODES);
240
243
  const REASONING_USAGE = "Usage: omx reasoning <low|medium|high|xhigh>";
241
- const AMBIGUOUS_REASONING_MESSAGE = 'Codex/OMX canonical highest reasoning effort is "xhigh"; "max" and "ultra" are not accepted aliases.';
244
+ function unsupportedLaunchShorthandError(flag) {
245
+ const guidance = flag === "--max"
246
+ ? 'No --max shorthand exists; use agentReasoning for per-agent "max" or pass -c model_reasoning_effort=... directly to Codex.'
247
+ : '"ultra" is not an OMX root or per-agent reasoning value and is not an alias for "max".';
248
+ return new Error(`Unsupported OMX launch shorthand "${flag}".\n${guidance}\nRun "omx help" for usage.`);
249
+ }
242
250
  const ALLOWED_SHELLS = new Set([
243
251
  "/bin/sh",
244
252
  "/bin/bash",
@@ -256,7 +264,6 @@ const ALLOWED_SHELLS = new Set([
256
264
  "/opt/local/bin/zsh",
257
265
  "/opt/homebrew/bin/zsh",
258
266
  ]);
259
- const WINDOWS_DETACHED_BOOTSTRAP_DELAY_MS = 2500;
260
267
  const CODEX_VERSION_FLAGS = new Set(["--version", "-V"]);
261
268
  const TMUX_EXTENDED_KEYS_MODE = "always";
262
269
  const TMUX_EXTENDED_KEYS_FALLBACK_MODE = "off";
@@ -264,6 +271,10 @@ const TMUX_EXTENDED_KEYS_LEASE_DIR = "tmux-extended-keys";
264
271
  const TMUX_EXTENDED_KEYS_LOCK_RETRY_MS = 20;
265
272
  const TMUX_EXTENDED_KEYS_LOCK_MAX_ATTEMPTS = 100;
266
273
  const TMUX_EXTENDED_KEYS_LOCK_STALE_MS = 30_000;
274
+ export const DETACHED_LEADER_READY_TIMEOUT_MS = 120_000;
275
+ const RESUME_HELP = `Usage: omx resume [--project] [--codex-home <path>] [codex resume options]
276
+
277
+ Read-only help does not prepare or launch a Codex session.`;
267
278
  const NESTED_HELP_COMMANDS = new Set([
268
279
  "ask",
269
280
  "question",
@@ -482,6 +493,15 @@ export function resolveSetupTeamModeArg(args) {
482
493
  }
483
494
  return value;
484
495
  }
496
+ function splitOmxArgsAtEndOfOptions(args) {
497
+ const endOfOptionsIndex = args.indexOf("--");
498
+ if (endOfOptionsIndex === -1)
499
+ return { omxArgs: args, suffix: [] };
500
+ return {
501
+ omxArgs: args.slice(0, endOfOptionsIndex),
502
+ suffix: args.slice(endOfOptionsIndex),
503
+ };
504
+ }
485
505
  export function resolveCliInvocation(args) {
486
506
  const firstArg = args[0];
487
507
  if (firstArg === "--help" || firstArg === "-h") {
@@ -502,6 +522,9 @@ export function resolveCliInvocation(args) {
502
522
  if (firstArg === "resume") {
503
523
  return { command: "resume", launchArgs: args.slice(1) };
504
524
  }
525
+ if (firstArg === "__detached-session-leader") {
526
+ return { command: firstArg, launchArgs: args.slice(1) };
527
+ }
505
528
  return { command: firstArg, launchArgs: [] };
506
529
  }
507
530
  export function resolveUpdateChannelArg(args) {
@@ -845,13 +868,14 @@ export async function prepareCodexHomeForLaunch(cwd, sessionId, env = process.en
845
868
  };
846
869
  }
847
870
  export function parseResumeCodexHomeSelection(args) {
871
+ const { omxArgs, suffix } = splitOmxArgsAtEndOfOptions(args);
848
872
  const nextArgs = [];
849
873
  let explicitCodexHome;
850
874
  let projectOnly = false;
851
- for (let index = 0; index < args.length; index += 1) {
852
- const arg = args[index];
875
+ for (let index = 0; index < omxArgs.length; index += 1) {
876
+ const arg = omxArgs[index];
853
877
  if (arg === "--codex-home") {
854
- const value = args[index + 1];
878
+ const value = omxArgs[index + 1];
855
879
  if (!value || value.startsWith("-")) {
856
880
  throw new Error("Missing value after --codex-home.");
857
881
  }
@@ -873,7 +897,7 @@ export function parseResumeCodexHomeSelection(args) {
873
897
  nextArgs.push(arg);
874
898
  }
875
899
  return {
876
- args: nextArgs,
900
+ args: [...nextArgs, ...suffix],
877
901
  explicitCodexHome,
878
902
  projectOnly,
879
903
  };
@@ -918,8 +942,32 @@ export async function preflightResumeOmxPluginState(codexHomeDir, pkgRoot = getP
918
942
  configUpdated,
919
943
  };
920
944
  }
921
- function isResumeCodexLaunch(args) {
922
- return args.includes("resume");
945
+ export { CODEX_GLOBAL_OPTIONS_WITH_SPLIT_VALUE } from "../auth/hotswap.js";
946
+ export function isResumeCodexLaunch(args) {
947
+ const { omxArgs } = splitOmxArgsAtEndOfOptions(args);
948
+ for (let index = 0; index < omxArgs.length; index += 1) {
949
+ const arg = omxArgs[index];
950
+ const optionValue = resolveCodexGlobalOptionValue(arg);
951
+ if (optionValue?.valueArity === "single") {
952
+ if (!optionValue.attached)
953
+ index += 1;
954
+ continue;
955
+ }
956
+ if (optionValue?.valueArity === "variadic") {
957
+ if (optionValue.attached)
958
+ continue;
959
+ let nextIndex = index + 1;
960
+ while (nextIndex < omxArgs.length && !omxArgs[nextIndex].startsWith("-")) {
961
+ nextIndex += 1;
962
+ }
963
+ index = nextIndex - 1;
964
+ continue;
965
+ }
966
+ if (arg.startsWith("-"))
967
+ continue;
968
+ return arg === "resume";
969
+ }
970
+ return false;
923
971
  }
924
972
  async function prepareResumeCodexHomeForLaunch(cwd, sessionId, args, env = process.env) {
925
973
  const selection = parseResumeCodexHomeSelection(args);
@@ -999,6 +1047,228 @@ function execTmuxFileSync(args, options) {
999
1047
  ...(process.platform === "win32" ? { windowsHide: true } : {}),
1000
1048
  });
1001
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
+ }
1002
1272
  function readTmuxEnvValueForTarget(targetPaneId) {
1003
1273
  if (!targetPaneId.startsWith("%"))
1004
1274
  return undefined;
@@ -1086,29 +1356,6 @@ function clearDetachedTmuxSessionHistoryIfUnattached(sessionName, leaderPaneId)
1086
1356
  logCliOperationFailure(err);
1087
1357
  }
1088
1358
  }
1089
- function readTmuxSessionInstanceId(sessionName) {
1090
- try {
1091
- return execTmuxFileSync(["show-options", "-qv", "-t", sessionName, OMX_INSTANCE_OPTION], {
1092
- stdio: ["ignore", "pipe", "ignore"],
1093
- encoding: "utf-8",
1094
- }).trim();
1095
- }
1096
- catch {
1097
- return null;
1098
- }
1099
- }
1100
- function tmuxPaneBelongsToSession(paneId, sessionName) {
1101
- try {
1102
- const paneSessionName = execTmuxFileSync(["display-message", "-p", "-t", paneId, "#{session_name}"], {
1103
- stdio: ["ignore", "pipe", "ignore"],
1104
- encoding: "utf-8",
1105
- }).trim();
1106
- return paneSessionName === sessionName;
1107
- }
1108
- catch {
1109
- return false;
1110
- }
1111
- }
1112
1359
  function buildDetachedHistoryPruneHookCommand(leaderPaneId) {
1113
1360
  // The leader pane can be gone by the time the hook fires (e.g. crashed
1114
1361
  // leader with a lingering session); suppress errors so tmux does not queue
@@ -1132,12 +1379,30 @@ function hasErrnoCode(error, code) {
1132
1379
  function isMissingTmuxLaunchNoise(error) {
1133
1380
  return error instanceof Error && /spawnSync tmux ENOENT/i.test(error.message);
1134
1381
  }
1382
+ function reportOrdinaryLaunchRootConflict(error, cwd, ordinaryLaunch) {
1383
+ if (!ordinaryLaunch || !isSessionPointerLaunchAbort(error)
1384
+ || error.code !== "session_pointer_owner_conflict"
1385
+ || getBaseStateDirWithSource(cwd).rootSource !== "cwd-default")
1386
+ return;
1387
+ console.error("[omx] concurrent conversations in this checkout require distinct user-specified OMX_ROOT values.\n" +
1388
+ "[omx] Choose a distinct directory and set OMX_ROOT before launching the additional conversation.\n" +
1389
+ "[omx] POSIX: OMX_ROOT=\"$HOME/.omx/instances/second-conversation\" omx\n" +
1390
+ "[omx] PowerShell: $env:OMX_ROOT = \"$HOME/.omx/instances/second-conversation\"; omx\n" +
1391
+ "[omx] cmd.exe: set \"OMX_ROOT=%USERPROFILE%\\.omx\\instances\\second-conversation\" && omx\n" +
1392
+ "[omx] The selected OMX_ROOT is used literally; OMX does not reroute or allocate one automatically.");
1393
+ }
1135
1394
  function logCliOperationFailure(error) {
1136
1395
  if (isMissingTmuxLaunchNoise(error))
1137
1396
  return;
1138
1397
  process.stderr.write(`[cli/index] operation failed: ${error}
1139
1398
  `);
1140
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
+ }
1141
1406
  function tmuxFailureMessage(error) {
1142
1407
  if (!error || typeof error !== "object")
1143
1408
  return String(error);
@@ -1152,77 +1417,18 @@ function isUnsupportedTmuxExtendedKeysFailure(error) {
1152
1417
  return (message.includes("extended-keys") &&
1153
1418
  /(?:invalid|unknown|unsupported) (?:option|flag|argument)|no such option|unknown option/.test(message));
1154
1419
  }
1155
- function isBenignMissingTmuxServerMessage(message) {
1156
- return (/no server running/i.test(message) ||
1157
- /error connecting to .*\(No such file or directory\)/i.test(message));
1158
- }
1159
- export function checkDetachedTmuxLaunchHealth() {
1160
- try {
1161
- execTmuxFileSync(["list-sessions"], {
1162
- stdio: ["ignore", "pipe", "pipe"],
1163
- encoding: "utf-8",
1164
- });
1165
- return { usable: true };
1166
- }
1167
- catch (err) {
1168
- const reason = tmuxFailureMessage(err);
1169
- if (isBenignMissingTmuxServerMessage(reason)) {
1170
- return { usable: true };
1171
- }
1172
- return { usable: false, reason };
1173
- }
1174
- }
1175
- function warnDetachedTmuxFallback(reason) {
1176
- const suffix = reason ? ` (${reason})` : "";
1177
- console.warn(`[omx] warning: tmux is installed but its server/socket is unusable${suffix}. Falling back to direct Codex launch.`);
1178
- }
1179
- const QUICK_ATTACH_NOOP_THRESHOLD_MS = 2_000;
1180
- function isWslWindowsTerminalEnvironment(env) {
1181
- return Boolean(env.WT_SESSION?.trim() &&
1182
- (env.WSL_INTEROP?.trim() ||
1183
- env.WSL_DISTRO_NAME?.trim() ||
1184
- env.WSLENV?.trim()));
1185
- }
1186
- function readDetachedSessionAttachedClientCount(sessionName) {
1187
- try {
1188
- const output = execTmuxFileSync(["display-message", "-p", "-t", sessionName, "#{session_attached}"], {
1189
- stdio: ["ignore", "pipe", "pipe"],
1190
- encoding: "utf-8",
1191
- }).trim();
1192
- const parsed = Number.parseInt(output, 10);
1193
- return Number.isFinite(parsed) ? parsed : null;
1194
- }
1195
- catch (err) {
1196
- logCliOperationFailure(err);
1197
- 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();
1198
1427
  }
1199
1428
  }
1200
- function assertDetachedAttachDidNotNoop(sessionName, elapsedMs, env) {
1201
- if (!isWslWindowsTerminalEnvironment(env))
1202
- return;
1203
- if (elapsedMs >= QUICK_ATTACH_NOOP_THRESHOLD_MS)
1204
- return;
1205
- const attachedClients = readDetachedSessionAttachedClientCount(sessionName);
1206
- if (attachedClients === null || attachedClients > 0)
1207
- return;
1208
- throw new Error([
1209
- "tmux attach-session returned immediately without attaching a client",
1210
- `(session=${sessionName}).`,
1211
- "This can happen on WSL2 under Windows Terminal.",
1212
- "Falling back to direct Codex launch.",
1213
- ].join(" "));
1214
- }
1215
1429
  function resolveTmuxAwareLaunchPolicy(explicitLaunchPolicy, nativeWindows) {
1216
- const launchPolicy = resolveCodexLaunchPolicy(process.env, process.platform, undefined, nativeWindows, undefined, undefined, explicitLaunchPolicy);
1217
- if (launchPolicy !== "detached-tmux") {
1218
- return { launchPolicy, effectiveExplicitLaunchPolicy: explicitLaunchPolicy };
1219
- }
1220
- const tmuxHealth = checkDetachedTmuxLaunchHealth();
1221
- if (tmuxHealth.usable) {
1222
- return { launchPolicy, effectiveExplicitLaunchPolicy: explicitLaunchPolicy };
1223
- }
1224
- warnDetachedTmuxFallback(tmuxHealth.reason);
1225
- return { launchPolicy: "direct", effectiveExplicitLaunchPolicy: "direct" };
1430
+ const launchPolicy = resolveCodexLaunchPolicy(process.env, process.platform, Boolean(resolveTmuxBinaryForPlatform()), nativeWindows, undefined, undefined, explicitLaunchPolicy);
1431
+ return { launchPolicy, effectiveExplicitLaunchPolicy: explicitLaunchPolicy };
1226
1432
  }
1227
1433
  export function resolveSignalExitCode(signal) {
1228
1434
  if (!signal)
@@ -1452,15 +1658,6 @@ export function resolveOmxRootForLaunch(cwd, env = process.env) {
1452
1658
  function resolveLaunchPath(cwd, raw) {
1453
1659
  return isCrossPlatformAbsolutePath(raw) ? raw : join(cwd, raw);
1454
1660
  }
1455
- function resolveHudRuntimeRootSource(omxRootOverride, env = process.env) {
1456
- if (env.OMX_TEAM_STATE_ROOT?.trim())
1457
- return 'team-env';
1458
- if (env.OMX_ROOT?.trim() || omxRootOverride)
1459
- return 'omx-root-env';
1460
- if (env.OMX_STATE_ROOT?.trim())
1461
- return 'omx-state-root-env';
1462
- return 'cwd-default';
1463
- }
1464
1661
  export function resolveHudRuntimeRootForLaunch(cwd, env = process.env) {
1465
1662
  const omxTeamStateRoot = env.OMX_TEAM_STATE_ROOT?.trim();
1466
1663
  if (omxTeamStateRoot) {
@@ -1519,6 +1716,8 @@ export function captureMadmaxWorktreeRuntimeContext(options) {
1519
1716
  const inheritedRoot = resolveInheritedMadmaxRoot(env);
1520
1717
  if (!inheritedRoot)
1521
1718
  return undefined;
1719
+ if (!madmaxInheritedContextMatchesLaunch(options.sourceCwd, options.originalLaunchArgs, env))
1720
+ return undefined;
1522
1721
  const sourceCwd = env.OMX_SOURCE_CWD?.trim() || options.sourceCwd;
1523
1722
  const worktreeCwd = options.worktreeCwd?.trim();
1524
1723
  const omxStateRoot = env.OMX_STATE_ROOT?.trim();
@@ -1557,10 +1756,31 @@ function launchArgRequestsDisposableWorktree(arg) {
1557
1756
  (arg.startsWith("-w") && arg.length > 2);
1558
1757
  }
1559
1758
  function launchArgsRequestMadmaxIsolation(launchArgs) {
1560
- return launchArgs.some((arg) => arg === MADMAX_FLAG || arg === MADMAX_SPARK_FLAG);
1759
+ for (const arg of launchArgs) {
1760
+ if (arg === "--")
1761
+ break;
1762
+ if (arg === MADMAX_FLAG || arg === MADMAX_SPARK_FLAG)
1763
+ return true;
1764
+ }
1765
+ return false;
1766
+ }
1767
+ function launchArgsRequestAuthHotswap(launchArgs) {
1768
+ for (const arg of launchArgs) {
1769
+ if (arg === "--")
1770
+ break;
1771
+ if (arg === "--hotswap")
1772
+ return true;
1773
+ }
1774
+ return false;
1561
1775
  }
1562
1776
  function launchArgsRequestDisposableWorktree(launchArgs) {
1563
- return launchArgs.some((arg) => launchArgRequestsDisposableWorktree(arg));
1777
+ for (const arg of launchArgs) {
1778
+ if (arg === "--")
1779
+ break;
1780
+ if (launchArgRequestsDisposableWorktree(arg))
1781
+ return true;
1782
+ }
1783
+ return false;
1564
1784
  }
1565
1785
  function clearInheritedMadmaxRootForDisposableWorktreeLaunch(launchArgs, env = process.env) {
1566
1786
  if (!launchArgsRequestDisposableWorktree(launchArgs))
@@ -1685,31 +1905,77 @@ function readMadmaxDetachedActiveRecord(recordPath) {
1685
1905
  tmux_session_name: parsed.tmux_session_name,
1686
1906
  ...(typeof parsed.session_id === "string" ? { session_id: parsed.session_id } : {}),
1687
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 } : {}),
1688
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 } : {}),
1689
1916
  };
1690
1917
  }
1691
1918
  catch {
1692
1919
  return null;
1693
1920
  }
1694
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
+ }
1695
1962
  function isReusableMadmaxDetachedActiveRecord(record) {
1696
- if (!detachedTmuxSessionExists(record.tmux_session_name))
1963
+ if (record.lifecycle_phase !== "ready" || !record.leader_pid || !record.base_state_root || !record.launch_nonce)
1697
1964
  return false;
1698
- if (!record.session_id || !record.tmux_pane_id)
1699
- return false;
1700
- if (readTmuxSessionInstanceId(record.tmux_session_name) !== record.session_id) {
1965
+ try {
1966
+ process.kill(record.leader_pid, 0);
1967
+ }
1968
+ catch {
1701
1969
  return false;
1702
1970
  }
1703
- return tmuxPaneBelongsToSession(record.tmux_pane_id, record.tmux_session_name);
1704
- }
1705
- function detachedTmuxSessionExists(sessionName) {
1706
1971
  try {
1707
- execTmuxFileSync(["has-session", "-t", sessionName], { stdio: "ignore" });
1708
- return true;
1972
+ if (realpathSync(record.base_state_root) !== record.base_state_root)
1973
+ return false;
1709
1974
  }
1710
1975
  catch {
1711
1976
  return false;
1712
1977
  }
1978
+ return hasMatchingMadmaxDetachedRuntimeBinding(record);
1713
1979
  }
1714
1980
  function readMadmaxDetachedLockOwner(lockPath) {
1715
1981
  try {
@@ -1778,7 +2044,7 @@ function inspectMadmaxDetachedContextLock(lockPath) {
1778
2044
  diagnostic: `lock has no readable owner pid yet; lock age ${Math.round(ageMs)}ms`,
1779
2045
  };
1780
2046
  }
1781
- export function withMadmaxDetachedContextLock(runsRoot, contextKey, run, options = {}) {
2047
+ export async function withMadmaxDetachedContextLock(runsRoot, contextKey, run, options = {}) {
1782
2048
  const lockPath = join(runsRoot, MADMAX_DETACHED_ACTIVE_DIR, `${contextKey}.lock`);
1783
2049
  const maxAttempts = options.maxAttempts ?? MADMAX_DETACHED_LOCK_MAX_ATTEMPTS;
1784
2050
  const retryMs = options.retryMs ?? MADMAX_DETACHED_LOCK_RETRY_MS;
@@ -1796,7 +2062,7 @@ export function withMadmaxDetachedContextLock(runsRoot, contextKey, run, options
1796
2062
  };
1797
2063
  writeFileSync(join(lockPath, "owner.json"), `${JSON.stringify(owner, null, 2)}\n`, { mode: 0o600 });
1798
2064
  writeFileSync(join(lockPath, "pid"), String(process.pid));
1799
- return run();
2065
+ return await run();
1800
2066
  }
1801
2067
  finally {
1802
2068
  rmSync(lockPath, { recursive: true, force: true });
@@ -1824,6 +2090,7 @@ function readMadmaxRunMetadata(runRoot) {
1824
2090
  const parsed = JSON.parse(readFileSync(join(runRoot, ".omxbox-run.json"), "utf-8"));
1825
2091
  return {
1826
2092
  ...(typeof parsed.cwd === "string" ? { cwd: parsed.cwd } : {}),
2093
+ ...(typeof parsed.source_cwd === "string" ? { source_cwd: parsed.source_cwd } : {}),
1827
2094
  ...(typeof parsed.detached_launch_context === "string"
1828
2095
  ? { detached_launch_context: parsed.detached_launch_context }
1829
2096
  : {}),
@@ -1851,13 +2118,27 @@ function madmaxInheritedContextMatchesLaunch(cwd, launchArgs, env) {
1851
2118
  return false;
1852
2119
  if (metadata.cwd && metadata.cwd !== inheritedRoot)
1853
2120
  return false;
2121
+ const sourceCwd = env.OMX_SOURCE_CWD?.trim();
2122
+ if (!sourceCwd || !metadata.source_cwd || metadata.source_cwd !== sourceCwd)
2123
+ return false;
1854
2124
  if (metadata.detached_launch_context !== context)
1855
2125
  return false;
1856
2126
  const expectedContext = buildMadmaxDetachedLaunchContextKey(cwd, [...launchArgs], inheritedRoot);
1857
2127
  return expectedContext === context;
1858
2128
  }
1859
- function isMadmaxDetachedGuardEnabled(env) {
1860
- 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
+ };
1861
2142
  }
1862
2143
  function cleanupCurrentMadmaxReuseRunRoot(env, runsRoot) {
1863
2144
  const runRoot = env.OMX_ROOT;
@@ -1869,15 +2150,236 @@ function cleanupCurrentMadmaxReuseRunRoot(env, runsRoot) {
1869
2150
  rmSync(runRoot, { recursive: true, force: true });
1870
2151
  }
1871
2152
  function writeMadmaxDetachedActiveRecord(recordPath, record) {
1872
- mkdirSync(dirname(recordPath), { recursive: true });
1873
- 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
+ }
1874
2197
  }
1875
- class MadmaxDetachedReuseError extends Error {
1876
- 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
+ }
1877
2348
  }
1878
2349
  class MadmaxDetachedGuardError extends Error {
1879
2350
  failClosed = true;
1880
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
+ }
1881
2383
  export function createMadmaxIsolatedRoot(sourceCwd, argv, env = process.env) {
1882
2384
  const runsRoot = resolveMadmaxRunsRoot(env);
1883
2385
  mkdirSync(runsRoot, { recursive: true });
@@ -1908,6 +2410,23 @@ function activateMadmaxIsolationIfNeeded(command, launchArgs, cwd, env = process
1908
2410
  env.OMX_SOURCE_CWD = cwd;
1909
2411
  process.stderr.write(`[omx] madmax isolated state: ${runDir} (source: ${cwd})\n`);
1910
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
+ }
1911
2430
  export async function main(args) {
1912
2431
  const knownCommands = new Set([
1913
2432
  "launch",
@@ -1916,6 +2435,7 @@ export async function main(args) {
1916
2435
  "imagegen",
1917
2436
  "capabilities",
1918
2437
  "setup",
2438
+ "__detached-session-leader",
1919
2439
  "update",
1920
2440
  "list",
1921
2441
  "agents",
@@ -1954,7 +2474,8 @@ export async function main(args) {
1954
2474
  ]);
1955
2475
  const firstArg = args[0];
1956
2476
  const { command, launchArgs } = resolveCliInvocation(args);
1957
- const flags = new Set(args.filter((a) => a.startsWith("--")));
2477
+ const { omxArgs } = splitOmxArgsAtEndOfOptions(args);
2478
+ const flags = new Set(omxArgs.filter((arg) => arg.startsWith("--")));
1958
2479
  const options = {
1959
2480
  force: flags.has("--force"),
1960
2481
  mergeAgents: undefined,
@@ -1966,11 +2487,27 @@ export async function main(args) {
1966
2487
  console.log(HELP);
1967
2488
  return;
1968
2489
  }
1969
- activateMadmaxIsolationIfNeeded(command, launchArgs, process.cwd(), process.env);
2490
+ if (command !== "launch" && command !== "resume") {
2491
+ activateMadmaxIsolationIfNeeded(command, launchArgs, process.cwd(), process.env);
2492
+ }
1970
2493
  try {
2494
+ if (command === "session") {
2495
+ await sessionCommand(args.slice(1));
2496
+ return;
2497
+ }
2498
+ if (command === "resume" && launchArgs.some((arg) => arg === "--help" || arg === "-h")) {
2499
+ console.log(RESUME_HELP);
2500
+ return;
2501
+ }
1971
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
+ }
1972
2509
  case "launch":
1973
- if (launchArgs.includes("--hotswap")) {
2510
+ if (launchArgsRequestAuthHotswap(launchArgs)) {
1974
2511
  await launchWithAuthHotswap(launchArgs);
1975
2512
  }
1976
2513
  else {
@@ -2081,9 +2618,6 @@ export async function main(args) {
2081
2618
  case "team":
2082
2619
  await teamCommand(args.slice(1), options);
2083
2620
  break;
2084
- case "session":
2085
- await sessionCommand(args.slice(1));
2086
- break;
2087
2621
  case "url":
2088
2622
  await urlCommand(args.slice(1));
2089
2623
  break;
@@ -2167,7 +2701,13 @@ export async function main(args) {
2167
2701
  }
2168
2702
  }
2169
2703
  catch (err) {
2170
- 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
+ }
2171
2711
  process.exit(1);
2172
2712
  }
2173
2713
  }
@@ -2305,9 +2845,14 @@ async function reasoningCommand(args) {
2305
2845
  return;
2306
2846
  }
2307
2847
  if (!REASONING_MODE_SET.has(mode)) {
2308
- const guidance = isAmbiguousUnsupportedReasoningEffort(mode)
2309
- ? `${AMBIGUOUS_REASONING_MESSAGE}\n`
2310
- : "";
2848
+ const unsupportedMode = isUnsupportedRootReasoningEffort(mode)
2849
+ ? normalizeUnsupportedRootReasoningEffort(mode)
2850
+ : undefined;
2851
+ const guidance = unsupportedMode === "max"
2852
+ ? `Reasoning mode "${mode}" is not supported by "omx reasoning".\nPer-agent "max" is configured with agentReasoning; direct -c model_reasoning_effort=... is passed to Codex and remains capability-dependent.\n`
2853
+ : unsupportedMode === "ultra"
2854
+ ? `Reasoning mode "${mode}" is not supported by OMX root or per-agent reasoning and is not an alias for "max".\nDirect -c model_reasoning_effort=... remains opaque Codex passthrough.\n`
2855
+ : "";
2311
2856
  throw new Error(`${guidance}Invalid reasoning mode "${mode}". Expected one of: ${REASONING_MODES.join(", ")}.\n${REASONING_USAGE}`);
2312
2857
  }
2313
2858
  const { mkdir, readFile, writeFile } = await import("fs/promises");
@@ -2321,7 +2866,9 @@ async function reasoningCommand(args) {
2321
2866
  }
2322
2867
  export async function launchWithAuthHotswap(args) {
2323
2868
  const launchCwd = process.cwd();
2324
- const parsedWorktree = parseWorktreeMode(args);
2869
+ const { omxArgs, suffix } = splitOmxArgsAtEndOfOptions(args);
2870
+ const parsedWorktree = parseWorktreeMode(omxArgs);
2871
+ const hotswapArgs = [...parsedWorktree.remainingArgs, ...suffix];
2325
2872
  let cwd = launchCwd;
2326
2873
  let worktreeDirty = false;
2327
2874
  let ensuredLaunchWorktree;
@@ -2375,7 +2922,7 @@ export async function launchWithAuthHotswap(args) {
2375
2922
  }
2376
2923
  const status = await runAuthHotswap({
2377
2924
  cwd,
2378
- argv: parsedWorktree.remainingArgs,
2925
+ argv: hotswapArgs,
2379
2926
  lifecycle: {
2380
2927
  prepareCodexHomeForLaunch,
2381
2928
  preLaunch: (launchPath, sessionId, notifyTempContract, codexHomeOverride, enableAuthority) => preLaunch(launchPath, sessionId, notifyTempContract, codexHomeOverride, enableAuthority, worktreeDirty),
@@ -2391,38 +2938,31 @@ export async function launchWithAuthHotswap(args) {
2391
2938
  process.exitCode = status;
2392
2939
  }
2393
2940
  export async function launchWithHud(args) {
2394
- if (isNativeWindows()) {
2395
- const { result } = spawnPlatformCommandSync("tmux", ["-V"], {
2396
- encoding: "utf-8",
2397
- stdio: ["pipe", "pipe", "pipe"],
2398
- });
2399
- if (result.error) {
2400
- const errno = result.error;
2401
- const kind = classifySpawnError(errno);
2402
- if (kind === "missing") {
2403
- console.warn("[omx] warning: tmux was not found on native Windows. Continuing without tmux/HUD.\n" +
2404
- "[omx] To enable tmux-backed features, install psmux:\n" +
2405
- "[omx] winget install psmux\n" +
2406
- "[omx] See: https://github.com/marlocarlo/psmux");
2407
- }
2408
- else {
2409
- console.warn(`[omx] warning: tmux probe failed on native Windows (${errno.code || errno.message}). Continuing without tmux/HUD.`);
2410
- }
2941
+ const launchCwd = process.cwd();
2942
+ const { omxArgs, suffix } = splitOmxArgsAtEndOfOptions(args);
2943
+ const parsedWorktree = parseWorktreeMode(omxArgs);
2944
+ const notifyTempResult = resolveNotifyTempContract(parsedWorktree.remainingArgs, process.env);
2945
+ const passthroughArgs = [...notifyTempResult.passthroughArgs, ...suffix];
2946
+ const explicitLaunchPolicy = resolveEffectiveLeaderLaunchPolicyOverride(passthroughArgs, process.env);
2947
+ let { launchPolicy } = resolveTmuxAwareLaunchPolicy(explicitLaunchPolicy, isNativeWindows());
2948
+ let detachedPreflight;
2949
+ if (launchPolicy === "detached-tmux") {
2950
+ try {
2951
+ detachedPreflight = preflightDetachedLaunch();
2411
2952
  }
2412
- else if (result.status !== 0 && !isTmuxAvailable()) {
2413
- const stderr = (result.stderr || "").trim();
2414
- console.warn(`[omx] warning: tmux reported an error on native Windows${stderr ? ` (${stderr})` : ""}. Continuing without tmux/HUD.`);
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";
2415
2958
  }
2416
2959
  }
2417
- const launchCwd = process.cwd();
2418
- const parsedWorktree = parseWorktreeMode(args);
2419
- const notifyTempResult = resolveNotifyTempContract(parsedWorktree.remainingArgs, process.env);
2420
- const explicitLaunchPolicy = resolveEffectiveLeaderLaunchPolicyOverride(notifyTempResult.passthroughArgs, process.env);
2421
- const persistentCodexHomeForLaunch = resolveCodexHomeForLaunch(launchCwd, process.env);
2422
- const { launchPolicy, effectiveExplicitLaunchPolicy } = resolveTmuxAwareLaunchPolicy(explicitLaunchPolicy, isNativeWindows());
2423
2960
  const enableNotifyFallbackAuthority = launchPolicy === "direct";
2424
- const workerSparkModel = resolveWorkerSparkModel(notifyTempResult.passthroughArgs, persistentCodexHomeForLaunch);
2425
- let normalizedArgs = normalizeCodexLaunchArgs(notifyTempResult.passthroughArgs);
2961
+ activateMadmaxIsolationIfNeeded("launch", args, launchCwd, process.env);
2962
+ const verifiedMadmaxLaunchContext = resolveVerifiedMadmaxDetachedContext(launchCwd, args, process.env);
2963
+ const persistentCodexHomeForLaunch = resolveCodexHomeForLaunch(launchCwd, process.env);
2964
+ const workerSparkModel = resolveWorkerSparkModel(passthroughArgs, persistentCodexHomeForLaunch);
2965
+ let normalizedArgs = normalizeCodexLaunchArgs(passthroughArgs);
2426
2966
  let cwd = launchCwd;
2427
2967
  let worktreeDirty = false;
2428
2968
  let ensuredLaunchWorktree;
@@ -2451,7 +2991,7 @@ export async function launchWithHud(args) {
2451
2991
  }
2452
2992
  }
2453
2993
  const madmaxWorktreeRuntimeContext = captureMadmaxWorktreeRuntimeContext({
2454
- originalLaunchArgs: args,
2994
+ originalLaunchArgs: omxArgs,
2455
2995
  worktreeEnabled: Boolean(parsedWorktree.mode.enabled && ensuredLaunchWorktree?.enabled),
2456
2996
  sourceCwd: launchCwd,
2457
2997
  worktreeCwd: ensuredLaunchWorktree?.enabled ? ensuredLaunchWorktree.worktreePath : undefined,
@@ -2460,6 +3000,8 @@ export async function launchWithHud(args) {
2460
3000
  clearInheritedMadmaxRootForDisposableWorktreeLaunch(parsedWorktree.remainingArgs);
2461
3001
  applyDisposableWorktreeOmxRootForLaunch(ensuredLaunchWorktree);
2462
3002
  applyWorktreeToolContextForLaunch(cwd, ensuredLaunchWorktree);
3003
+ if (launchPolicy === "detached-tmux" && detachedPreflight && handleMadmaxDetachedReuse(madmaxWorktreeRuntimeContext, detachedPreflight))
3004
+ return;
2463
3005
  const sessionId = `omx-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2464
3006
  try {
2465
3007
  await maybeCheckAndPromptUpdate(cwd);
@@ -2501,43 +3043,86 @@ export async function launchWithHud(args) {
2501
3043
  const codexHomeOverride = preparedCodexHome.codexHomeOverride;
2502
3044
  const sqliteHomeOverride = preparedCodexHome.sqliteHomeOverride;
2503
3045
  const projectLocalCodexHomeForCleanup = preparedCodexHome.projectLocalCodexHomeForCleanup;
2504
- // ── Phase 1: preLaunch ──────────────────────────────────────────────────
2505
- try {
2506
- 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
+ });
2507
3065
  }
2508
- catch (err) {
2509
- if (isSessionPointerLaunchAbort(err)) {
2510
- console.error(`[omx] session pointer launch aborted: ${err.code}`);
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?.();
2511
3081
  await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch((cleanupErr) => {
2512
3082
  console.error(`[omx] preLaunch abort cleanup warning: ${cleanupErr instanceof Error ? cleanupErr.message : cleanupErr}`);
2513
3083
  });
2514
3084
  throw err;
2515
3085
  }
2516
- // preLaunch errors after pointer commit must not prevent Codex from starting.
2517
- console.error(`[omx] preLaunch warning: ${err instanceof Error ? err.message : err}`);
2518
3086
  }
2519
- // ── Phase 2: run ────────────────────────────────────────────────────────
2520
3087
  let postLaunchHandledExternally = false;
2521
3088
  try {
2522
3089
  const notifyTempContractRaw = notifyTempResult.contract.active
2523
3090
  ? serializeNotifyTempContract(notifyTempResult.contract)
2524
3091
  : null;
2525
- 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);
2526
3097
  postLaunchHandledExternally = launchResult.postLaunchHandledExternally;
2527
3098
  }
3099
+ catch (error) {
3100
+ if (error instanceof DetachedLaunchSafetyError)
3101
+ postLaunchHandledExternally = true;
3102
+ throw error;
3103
+ }
2528
3104
  finally {
2529
- // ── Phase 3: postLaunch ─────────────────────────────────────────────
2530
- if (!postLaunchHandledExternally) {
2531
- 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
+ }
2532
3112
  await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch(logCliOperationFailure);
2533
3113
  }
3114
+ else {
3115
+ restoreInsideTmuxControlPlane?.();
3116
+ }
2534
3117
  }
2535
3118
  }
2536
3119
  export async function execWithOverlay(args) {
2537
3120
  const launchCwd = process.cwd();
2538
- const parsedWorktree = parseWorktreeMode(args);
3121
+ const { omxArgs, suffix } = splitOmxArgsAtEndOfOptions(args);
3122
+ const parsedWorktree = parseWorktreeMode(omxArgs);
2539
3123
  const notifyTempResult = resolveNotifyTempContract(parsedWorktree.remainingArgs, process.env);
2540
- const normalizedArgs = normalizeCodexLaunchArgs(notifyTempResult.passthroughArgs);
3124
+ const passthroughArgs = [...notifyTempResult.passthroughArgs, ...suffix];
3125
+ const normalizedArgs = normalizeCodexLaunchArgs(passthroughArgs);
2541
3126
  let cwd = launchCwd;
2542
3127
  let worktreeDirty = false;
2543
3128
  let ensuredLaunchWorktree;
@@ -2595,18 +3180,19 @@ export async function execWithOverlay(args) {
2595
3180
  const codexHomeOverride = preparedCodexHome.codexHomeOverride;
2596
3181
  const sqliteHomeOverride = preparedCodexHome.sqliteHomeOverride;
2597
3182
  const projectLocalCodexHomeForCleanup = preparedCodexHome.projectLocalCodexHomeForCleanup;
3183
+ let launch;
2598
3184
  try {
2599
- await preLaunch(cwd, sessionId, notifyTempResult.contract, codexHomeOverride, true, worktreeDirty);
3185
+ launch = await preLaunch(cwd, sessionId, notifyTempResult.contract, codexHomeOverride, true, worktreeDirty);
2600
3186
  }
2601
3187
  catch (err) {
3188
+ logEstablishmentCleanupEvidence(err);
2602
3189
  if (isSessionPointerLaunchAbort(err)) {
2603
3190
  console.error(`[omx] session pointer launch aborted: ${err.code}`);
2604
- await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch((cleanupErr) => {
2605
- console.error(`[omx] preLaunch abort cleanup warning: ${cleanupErr instanceof Error ? cleanupErr.message : cleanupErr}`);
2606
- });
2607
- throw err;
2608
3191
  }
2609
- 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;
2610
3196
  }
2611
3197
  try {
2612
3198
  const notifyTempContractRaw = notifyTempResult.contract.active
@@ -2614,27 +3200,34 @@ export async function execWithOverlay(args) {
2614
3200
  : null;
2615
3201
  const codexArgs = injectModelInstructionsBypassArgs(cwd, ["exec", ...normalizedArgs], process.env, sessionModelInstructionsPath(cwd, sessionId));
2616
3202
  const omxRootOverride = resolveOmxRootForLaunch(cwd, process.env);
2617
- const codexEnvBase = {
2618
- ...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),
2619
3209
  ...(codexHomeOverride ? { CODEX_HOME: codexHomeOverride } : {}),
2620
3210
  ...(sqliteHomeOverride ? { [CODEX_SQLITE_HOME_ENV]: sqliteHomeOverride } : {}),
2621
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 } : {}),
2622
3220
  };
2623
- const codexEnv = notifyTempContractRaw
2624
- ? {
2625
- ...codexEnvBase,
2626
- [OMX_NOTIFY_TEMP_CONTRACT_ENV]: notifyTempContractRaw,
2627
- }
2628
- : codexEnvBase;
2629
3221
  runCodexBlocking(cwd, codexArgs, codexEnv);
2630
3222
  }
2631
3223
  finally {
2632
- await postLaunch(cwd, sessionId, codexHomeOverride, true, projectLocalCodexHomeForCleanup);
3224
+ await postLaunch(cwd, sessionId, launch.binding, codexHomeOverride, true, projectLocalCodexHomeForCleanup);
2633
3225
  await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch(logCliOperationFailure);
2634
3226
  }
2635
3227
  }
2636
3228
  export function normalizeCodexLaunchArgs(args) {
2637
- const parsed = parseWorktreeMode(args);
3229
+ const { omxArgs, suffix } = splitOmxArgsAtEndOfOptions(args);
3230
+ const parsed = parseWorktreeMode(omxArgs);
2638
3231
  const launchPolicyParsed = splitLeaderLaunchPolicyArgs(parsed.remainingArgs);
2639
3232
  const normalized = [];
2640
3233
  let wantsBypass = false;
@@ -2662,7 +3255,7 @@ export function normalizeCodexLaunchArgs(args) {
2662
3255
  continue;
2663
3256
  }
2664
3257
  if (arg === "--max" || arg === "--ultra") {
2665
- throw new Error(AMBIGUOUS_REASONING_MESSAGE);
3258
+ throw unsupportedLaunchShorthandError(arg);
2666
3259
  }
2667
3260
  if (arg === SPARK_FLAG) {
2668
3261
  // Spark model is injected into worker env only (not the leader). Consume flag.
@@ -2681,7 +3274,7 @@ export function normalizeCodexLaunchArgs(args) {
2681
3274
  if (reasoningMode) {
2682
3275
  normalized.push(CONFIG_FLAG, `${REASONING_KEY}="${reasoningMode}"`);
2683
3276
  }
2684
- return normalized;
3277
+ return [...normalized, ...suffix];
2685
3278
  }
2686
3279
  /**
2687
3280
  * Returns the spark model string if --spark or --madmax-spark appears in the
@@ -2690,6 +3283,8 @@ export function normalizeCodexLaunchArgs(args) {
2690
3283
  */
2691
3284
  export function resolveWorkerSparkModel(args, codexHomeOverride) {
2692
3285
  for (const arg of args) {
3286
+ if (arg === "--")
3287
+ break;
2693
3288
  if (arg === SPARK_FLAG || arg === MADMAX_SPARK_FLAG) {
2694
3289
  return resolveTeamLowComplexityDefaultModel(codexHomeOverride);
2695
3290
  }
@@ -2830,12 +3425,14 @@ function buildNativeHookBaseContext(cwd, sessionId, normalizedEvent, extra = {})
2830
3425
  export function injectModelInstructionsBypassArgs(cwd, args, env = process.env, defaultFilePath) {
2831
3426
  if (!shouldBypassDefaultSystemPrompt(env))
2832
3427
  return [...args];
2833
- if (hasModelInstructionsOverride(args))
3428
+ const { omxArgs, suffix } = splitOmxArgsAtEndOfOptions(args);
3429
+ if (hasModelInstructionsOverride(omxArgs))
2834
3430
  return [...args];
2835
3431
  return [
2836
- ...args,
3432
+ ...omxArgs,
2837
3433
  CONFIG_FLAG,
2838
3434
  buildModelInstructionsOverride(cwd, env, defaultFilePath),
3435
+ ...suffix,
2839
3436
  ];
2840
3437
  }
2841
3438
  export function collectInheritableTeamWorkerArgs(codexArgs) {
@@ -2906,9 +3503,6 @@ export function detectDetachedSessionWindowIndex(sessionName) {
2906
3503
  return null;
2907
3504
  }
2908
3505
  }
2909
- function escapeShellDoubleQuotedValue(value) {
2910
- return value.replace(/["\\$`]/g, "\\$&");
2911
- }
2912
3506
  function sanitizeTmuxLeaseKey(value) {
2913
3507
  const cleaned = value
2914
3508
  .toLowerCase()
@@ -3094,96 +3688,163 @@ function withTmuxExtendedKeysLeaseLock(cwd, socketPath, run) {
3094
3688
  }
3095
3689
  throw new Error(`timed out waiting for tmux extended-keys lease lock: ${lockPath}`);
3096
3690
  }
3097
- function buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath) {
3098
- const detachedPostLaunchHelper = sessionId
3099
- ? `${buildDetachedSessionPostLaunchHelperCommand(cwd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup)} >/dev/null 2>&1 || true;`
3100
- : "";
3101
- const parentEnvSource = parentEnvFilePath && parentEnvFilePath.trim()
3102
- ? `if [ -r ${quoteShellArg(parentEnvFilePath)} ]; then . ${quoteShellArg(parentEnvFilePath)}; rm -f ${quoteShellArg(parentEnvFilePath)}; fi;`
3103
- : "";
3104
- const parentEnvCleanup = parentEnvFilePath && parentEnvFilePath.trim()
3105
- ? `rm -f ${quoteShellArg(parentEnvFilePath)} 2>/dev/null || true;`
3106
- : "";
3107
- const wrapped = [
3108
- buildTmuxExtendedKeysAcquireShellSnippet(cwd),
3109
- 'exec 3<&0;',
3110
- 'omx_codex_pid="";',
3111
- "omx_detached_session_cleanup() {",
3112
- "status=$?;",
3113
- "trap - 0 INT TERM HUP;",
3114
- 'if [ -n "$omx_codex_pid" ] && kill -0 "$omx_codex_pid" 2>/dev/null; then',
3115
- 'kill -TERM "$omx_codex_pid" 2>/dev/null || true;',
3116
- 'wait "$omx_codex_pid" 2>/dev/null || true;',
3117
- "fi;",
3118
- 'exec 3<&- 2>/dev/null || true;',
3119
- buildTmuxExtendedKeysReleaseShellSnippet(cwd),
3120
- parentEnvCleanup,
3121
- detachedPostLaunchHelper,
3122
- 'if [ "$status" -eq 0 ]; then',
3123
- `tmux kill-session -t "${escapeShellDoubleQuotedValue(sessionName)}" >/dev/null 2>&1 || true;`,
3124
- "fi;",
3125
- "exit $status;",
3126
- "};",
3127
- "trap omx_detached_session_cleanup 0 INT TERM HUP;",
3128
- parentEnvSource,
3129
- "unset OMX_HERMES_MCP_BRIDGE;",
3130
- "omx_codex_started_at=$(date +%s 2>/dev/null || printf 0);",
3131
- `${codexCmd} <&3 &`,
3132
- "omx_codex_pid=$!;",
3133
- 'wait "$omx_codex_pid";',
3134
- "omx_codex_status=$?;",
3135
- "omx_codex_finished_at=$(date +%s 2>/dev/null || printf 0);",
3136
- 'omx_codex_elapsed=$((omx_codex_finished_at - omx_codex_started_at));',
3137
- 'if [ "$omx_codex_status" -eq 0 ] && [ "$omx_codex_elapsed" -le 2 ]; then',
3138
- '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;',
3139
- 'IFS= read -r _omx_close || true;',
3140
- 'elif [ "$omx_codex_status" -gt 0 ] && [ "$omx_codex_status" -lt 128 ] && [ "$omx_codex_elapsed" -le 2 ]; then',
3141
- '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;',
3142
- 'IFS= read -r _omx_close || true;',
3143
- 'elif [ "$omx_codex_status" -gt 0 ] && [ "$omx_codex_status" -lt 128 ]; then',
3144
- '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;',
3145
- 'IFS= read -r _omx_close || true;',
3146
- "fi;",
3147
- 'exit "$omx_codex_status";',
3148
- ].join(" ");
3149
- return `/bin/sh -c ${quoteShellArg(wrapped)}`;
3150
- }
3151
- function buildDetachedSessionPostLaunchHelperCommand(cwd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup) {
3152
- const cwdLiteral = JSON.stringify(cwd);
3153
- const sessionIdLiteral = JSON.stringify(sessionId);
3154
- const codexHomeLiteral = typeof codexHomeOverride === "string" && codexHomeOverride.length > 0
3155
- ? JSON.stringify(codexHomeOverride)
3156
- : "undefined";
3157
- const projectLocalCleanupLiteral = typeof projectLocalCodexHomeForCleanup === "string" &&
3158
- projectLocalCodexHomeForCleanup.length > 0
3159
- ? JSON.stringify(projectLocalCodexHomeForCleanup)
3160
- : "undefined";
3161
- const runtimeCodexHomeCleanupLiteral = typeof runtimeCodexHomeForCleanup === "string" &&
3162
- runtimeCodexHomeForCleanup.length > 0
3163
- ? JSON.stringify(runtimeCodexHomeForCleanup)
3164
- : "undefined";
3165
- const moduleUrlLiteral = JSON.stringify(import.meta.url);
3166
- const script = [
3167
- `const mod = await import(${moduleUrlLiteral});`,
3168
- `await mod.runDetachedSessionPostLaunch(${cwdLiteral}, ${sessionIdLiteral}, ${codexHomeLiteral}, ${projectLocalCleanupLiteral}, ${runtimeCodexHomeCleanupLiteral});`,
3169
- ].join(" ");
3170
- return `${quoteShellArg(process.execPath)} --input-type=module -e ${quoteShellArg(script)}`;
3171
- }
3172
- function execTmuxSync(args, execFileSyncImpl = (file, tmuxArgs) => execFileSync(file, tmuxArgs, {
3173
- encoding: "utf-8",
3174
- ...(process.platform === "win32" ? { windowsHide: true } : {}),
3175
- })) {
3176
- 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;
3177
3718
  }
3178
- export function acquireTmuxExtendedKeysLease(cwd, execFileSyncImpl = (file, tmuxArgs) => execFileSync(file, tmuxArgs, {
3179
- encoding: "utf-8",
3180
- ...(process.platform === "win32" ? { windowsHide: true } : {}),
3181
- }), 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;
3182
3723
  try {
3183
- const socketPath = resolveTmuxSocketPath(execFileSyncImpl);
3184
- const leasePath = tmuxExtendedKeysLeasePath(cwd, socketPath);
3185
- const holderPid = Number.isSafeInteger(ownerPid) && ownerPid > 0 ? ownerPid : process.pid;
3186
- const leaseId = `${holderPid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
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);
3740
+ }
3741
+ catch (error) {
3742
+ if (!(process.platform === "win32" && error.code === "EPERM"))
3743
+ throw error;
3744
+ }
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)}`;
3187
3848
  withTmuxExtendedKeysLeaseLock(cwd, socketPath, () => {
3188
3849
  const stateRaw = readTmuxExtendedKeysLeaseState(leasePath);
3189
3850
  const state = stateRaw ? reapDeadTmuxExtendedKeysLeaseHolders(stateRaw) : null;
@@ -3253,19 +3914,123 @@ export function releaseTmuxExtendedKeysLease(cwd, leaseHandle, execFileSyncImpl
3253
3914
  }
3254
3915
  }
3255
3916
  }
3256
- function buildTmuxExtendedKeysHelperCommand(cwd, operation) {
3257
- const cwdLiteral = JSON.stringify(cwd);
3258
- const moduleUrlLiteral = JSON.stringify(import.meta.url);
3259
- const script = operation === "acquire"
3260
- ? `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);`
3261
- : `const mod = await import(${moduleUrlLiteral}); mod.releaseTmuxExtendedKeysLease(${cwdLiteral}, process.argv[1] ?? "");`;
3262
- return `${quoteShellArg(process.execPath)} --input-type=module -e ${quoteShellArg(script)}`;
3263
- }
3264
- function buildTmuxExtendedKeysAcquireShellSnippet(cwd) {
3265
- 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
+ });
3266
4010
  }
3267
- function buildTmuxExtendedKeysReleaseShellSnippet(cwd) {
3268
- 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
+ };
3269
4034
  }
3270
4035
  const SHELL_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
3271
4036
  const DETACHED_SESSION_PANE_ENV_KEYS = new Set([
@@ -3277,21 +4042,23 @@ const DETACHED_SESSION_PANE_ENV_KEYS = new Set([
3277
4042
  "COLUMNS",
3278
4043
  "LINES",
3279
4044
  ]);
3280
- export function serializeDetachedSessionParentEnv(env) {
3281
- const lines = [];
4045
+ function detachedSessionParentEnvironment(env, caseInsensitive = false) {
4046
+ const values = {};
3282
4047
  for (const key of Object.keys(env).sort()) {
3283
- if (!SHELL_ENV_NAME_PATTERN.test(key))
3284
- continue;
3285
- 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))
3286
4051
  continue;
3287
4052
  const value = env[key];
3288
- if (typeof value !== "string")
3289
- continue;
3290
- if (value.includes("\0"))
3291
- continue;
3292
- lines.push(`export ${key}=${quoteShellArg(value)}`);
4053
+ if (typeof value === "string" && !value.includes("\0"))
4054
+ values[key] = value;
3293
4055
  }
3294
- 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`;
3295
4062
  }
3296
4063
  export function detachedSessionParentEnvFilePath(cwd, sessionId) {
3297
4064
  const safeSessionId = sessionId.replace(/[^A-Za-z0-9_.-]/g, "_");
@@ -3319,27 +4086,21 @@ export function withTmuxExtendedKeys(cwd, run, execFileSyncImpl = (file, tmuxArg
3319
4086
  releaseTmuxExtendedKeysLease(cwd, leaseHandle, execFileSyncImpl);
3320
4087
  }
3321
4088
  }
3322
- 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) {
3323
4094
  const detachedLeaderCmd = nativeWindows
3324
- ? "powershell.exe"
3325
- : buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath);
3326
- const resolvedEnvStateRoot = env.OMX_STATE_ROOT?.trim()
3327
- ? resolveLaunchPath(cwd, env.OMX_STATE_ROOT.trim())
3328
- : undefined;
3329
- const hasExplicitRootOverride = Boolean(env.OMX_ROOT?.trim()
3330
- || (omxRootOverride && omxRootOverride !== resolvedEnvStateRoot));
3331
- const hudRuntimeRoot = env.OMX_TEAM_STATE_ROOT?.trim()
3332
- ? resolveHudRuntimeRootForLaunch(cwd, env)
3333
- : hasExplicitRootOverride
3334
- ? {
3335
- omxRoot: omxRootOverride,
3336
- rootSource: resolveHudRuntimeRootSource(omxRootOverride, env),
3337
- }
3338
- : resolveHudRuntimeRootForLaunch(cwd, env);
3339
- const hudRuntimeEnv = buildHudRuntimeEnv({
3340
- sessionId,
3341
- ...hudRuntimeRoot,
3342
- }).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);
3343
4104
  const newSessionArgs = [
3344
4105
  "new-session",
3345
4106
  "-d",
@@ -3350,15 +4111,10 @@ export function buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, h
3350
4111
  sessionName,
3351
4112
  "-c",
3352
4113
  cwd,
4114
+ ...DETACHED_LAUNCH_CONTROL_PLANE_KEYS.flatMap((key) => ["-e", `${key}=${controlPlaneEnv[key]}`]),
3353
4115
  ...(workerLaunchArgs ? ["-e", `${TEAM_WORKER_LAUNCH_ARGS_ENV}=${workerLaunchArgs}`] : []),
3354
- ...Object.entries(hudRuntimeEnv).map(([key, value]) => ["-e", `${key}=${value}`]).flat(),
3355
4116
  ...(codexHomeOverride ? ["-e", `CODEX_HOME=${codexHomeOverride}`] : []),
3356
4117
  ...(sqliteHomeOverride ? ["-e", `${CODEX_SQLITE_HOME_ENV}=${sqliteHomeOverride}`] : []),
3357
- ...(env.OMXBOX_ACTIVE ? ["-e", `OMXBOX_ACTIVE=${env.OMXBOX_ACTIVE}`] : []),
3358
- ...(env.OMX_SOURCE_CWD ? ["-e", `OMX_SOURCE_CWD=${env.OMX_SOURCE_CWD}`] : []),
3359
- ...(env[OMX_MADMAX_DETACHED_CONTEXT_ENV]
3360
- ? ["-e", `${OMX_MADMAX_DETACHED_CONTEXT_ENV}=${env[OMX_MADMAX_DETACHED_CONTEXT_ENV]}`]
3361
- : []),
3362
4118
  ...(notifyTempContractRaw
3363
4119
  ? ["-e", `${OMX_NOTIFY_TEMP_CONTRACT_ENV}=${notifyTempContractRaw}`]
3364
4120
  : []),
@@ -3618,41 +4374,40 @@ function isAutopilotReviewPendingPostLaunchState(state) {
3618
4374
  function postLaunchUniqueStrings(values) {
3619
4375
  return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
3620
4376
  }
3621
- async function scrubPostLaunchRootSkillActiveForSession(stateDir, sessionId, nowIso, writeFileFn, rootStateBeforeCleanup) {
4377
+ async function scrubPostLaunchRootSkillActiveForSession(stateDir, sessionId, nowIso, writeRootStateFn) {
3622
4378
  const normalizedSessionId = cleanPostLaunchString(sessionId);
3623
4379
  if (!normalizedSessionId)
3624
4380
  return;
3625
- const { rootPath } = getSkillActiveStatePathsForStateDir(stateDir);
3626
- const rootState = rootStateBeforeCleanup ?? await readSkillActiveState(rootPath);
3627
- if (!rootState)
3628
- return;
3629
- const rootSessionIds = postLaunchUniqueStrings([
3630
- cleanPostLaunchString(rootState.session_id),
3631
- cleanPostLaunchString(extractSessionIdFromInitializedStatePath(rootState.initialized_state_path)),
3632
- ]);
3633
- const rootBelongsToSession = rootSessionIds.includes(normalizedSessionId);
3634
- const entries = listActiveSkills(rootState);
3635
- const keptEntries = entries.filter((entry) => {
3636
- const entrySessionId = cleanPostLaunchString(entry.session_id);
3637
- if (entrySessionId)
3638
- return entrySessionId !== normalizedSessionId;
3639
- 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
+ };
3640
4410
  });
3641
- if (keptEntries.length === entries.length && rootState.active !== true)
3642
- return;
3643
- if (keptEntries.length === entries.length && !rootBelongsToSession)
3644
- return;
3645
- const nextRoot = {
3646
- ...rootState,
3647
- active: keptEntries.length > 0,
3648
- skill: keptEntries[0]?.skill ?? (keptEntries.length > 0 ? cleanPostLaunchString(rootState.skill) : ""),
3649
- phase: keptEntries[0]?.phase ?? (keptEntries.length > 0 ? cleanPostLaunchString(rootState.phase) : "complete"),
3650
- updated_at: nowIso,
3651
- active_skills: keptEntries,
3652
- post_launch_reconciled_at: nowIso,
3653
- post_launch_reconciliation_reason: "terminal_session_cleanup",
3654
- };
3655
- await writeFileFn(rootPath, JSON.stringify(nextRoot, null, 2));
3656
4411
  }
3657
4412
  function buildRecoveredPostLaunchModeState(mode, completedAt) {
3658
4413
  return {
@@ -3699,9 +4454,14 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
3699
4454
  ? [getStateDir(cwd, sessionId)]
3700
4455
  : [getBaseStateDir(cwd)];
3701
4456
  const rootStateDir = getBaseStateDir(cwd);
3702
- const rootSkillActiveStateBeforeCleanup = sessionId
3703
- ? await readSkillActiveState(getSkillActiveStatePathsForStateDir(rootStateDir).rootPath)
3704
- : 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
+ };
3705
4465
  let preserveSkillActiveForReviewPendingAutopilot = false;
3706
4466
  for (const stateDir of scopedDirs) {
3707
4467
  const files = await readdir(stateDir).catch(() => []);
@@ -3713,7 +4473,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
3713
4473
  && isAutopilotReviewPendingPostLaunchState(autopilotPrecheck.state);
3714
4474
  preserveSkillActiveForReviewPendingAutopilot ||= preserveReviewPendingAutopilot;
3715
4475
  for (const file of files) {
3716
- if (!file.endsWith("-state.json") || file === "session.json")
4476
+ if (!isModeStateFilename(file))
3717
4477
  continue;
3718
4478
  const path = join(stateDir, file);
3719
4479
  const mode = file.slice(0, -"-state.json".length);
@@ -3722,9 +4482,10 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
3722
4482
  if (result.kind === "recoverable") {
3723
4483
  try {
3724
4484
  const completedAt = now().toISOString();
3725
- await writeFile(path, JSON.stringify(mode === SKILL_ACTIVE_STATE_MODE
4485
+ const recoveredState = mode === SKILL_ACTIVE_STATE_MODE
3726
4486
  ? buildRecoveredPostLaunchSkillActiveState(completedAt)
3727
- : buildRecoveredPostLaunchModeState(mode, completedAt), null, 2));
4487
+ : buildRecoveredPostLaunchModeState(mode, completedAt);
4488
+ await writeModeState(stateDir, mode, path, recoveredState);
3728
4489
  if (isTrackedWorkflowMode(mode)) {
3729
4490
  await syncCanonicalSkillStateForMode({
3730
4491
  cwd,
@@ -3757,12 +4518,12 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
3757
4518
  : normalizeTerminalWorkflowState(result.state, { mode, nowIso: completedAt });
3758
4519
  if (normalized.changed) {
3759
4520
  result.state = normalized.state;
3760
- await writeFile(path, JSON.stringify(result.state, null, 2));
4521
+ await writeModeState(stateDir, mode, path, result.state);
3761
4522
  }
3762
4523
  if (mode === "ralph") {
3763
4524
  const completedAt = now().toISOString();
3764
4525
  if (markRalphCompletionAuditBlockedForPostLaunch(result.state, cwd, completedAt)) {
3765
- await writeFile(path, JSON.stringify(result.state, null, 2));
4526
+ await writeModeState(stateDir, mode, path, result.state);
3766
4527
  await syncCanonicalSkillStateForMode({
3767
4528
  cwd,
3768
4529
  baseStateDir: rootStateDir,
@@ -3788,7 +4549,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
3788
4549
  result.state.phase = "complete";
3789
4550
  result.state.updated_at = completedAt;
3790
4551
  result.state.active_skills = [];
3791
- await writeFile(path, JSON.stringify(result.state, null, 2));
4552
+ await writeModeState(stateDir, mode, path, result.state);
3792
4553
  continue;
3793
4554
  }
3794
4555
  result.state.active = false;
@@ -3799,7 +4560,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
3799
4560
  result.state.interrupted_at = completedAt;
3800
4561
  result.state.stop_reason = cleanPostLaunchString(result.state.stop_reason) || "session_exit";
3801
4562
  }
3802
- await writeFile(path, JSON.stringify(result.state, null, 2));
4563
+ await writeModeState(stateDir, mode, path, result.state);
3803
4564
  if (isTrackedWorkflowMode(mode)) {
3804
4565
  await syncCanonicalSkillStateForMode({
3805
4566
  cwd,
@@ -3821,7 +4582,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
3821
4582
  if (sessionId) {
3822
4583
  try {
3823
4584
  if (!preserveSkillActiveForReviewPendingAutopilot) {
3824
- await scrubPostLaunchRootSkillActiveForSession(rootStateDir, sessionId, now().toISOString(), writeFile, rootSkillActiveStateBeforeCleanup);
4585
+ await scrubPostLaunchRootSkillActiveForSession(rootStateDir, sessionId, now().toISOString(), writeRootState);
3825
4586
  }
3826
4587
  }
3827
4588
  catch (err) {
@@ -3847,115 +4608,140 @@ export async function reapPostLaunchOrphanedMcpProcesses(dependencies = {}) {
3847
4608
  writeError(`[cli/index] postLaunch MCP cleanup failed: ${err}\n`);
3848
4609
  }
3849
4610
  }
3850
- /**
3851
- * preLaunch: Prepare environment before Codex starts.
3852
- * 1. Best-effort launch-safe orphan cleanup for detached OMX MCP processes
3853
- * 2. Establish the canonical session pointer
3854
- * 3. Generate session-scoped launch artifacts and start best-effort helpers
3855
- *
3856
- * Automatic broad stale-session cleanup remains disabled here. Only detached
3857
- * OMX MCP processes without a live Codex ancestor are reaped so new launches
3858
- * do not accumulate stale processes from prior crashed/closed sessions.
3859
- */
3860
- export async function preLaunch(cwd, sessionId, notifyTempContract, codexHomeOverride, enableNotifyFallbackAuthority = false, worktreeDirty = false) {
3861
- // 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 = [];
3862
4643
  try {
3863
4644
  const cleanup = await cleanupLaunchOrphanedMcpProcesses();
3864
- if (cleanup.terminatedCount > 0) {
4645
+ if (cleanup.terminatedCount > 0)
3865
4646
  console.log(`[omx] Reaped ${cleanup.terminatedCount} orphaned OMX MCP process(es) before launch.`);
3866
- }
3867
- if (cleanup.failedPids.length > 0) {
4647
+ if (cleanup.failedPids.length > 0)
3868
4648
  console.warn(`[omx] Failed to reap ${cleanup.failedPids.length} orphaned OMX MCP process(es); continuing launch.`);
3869
- }
3870
4649
  }
3871
- catch (err) {
3872
- logCliOperationFailure(err);
3873
- // 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 };
3874
4679
  }
3875
- // 2. Establish the canonical pointer before any session-scoped launch artifact.
3876
- await writeSessionStart(cwd, sessionId, await resolvePreLaunchSessionPointerOptions());
3877
- // 3. Generate runtime overlay + write session-scoped model instructions file
3878
- const orchestrationMode = await resolveSessionOrchestrationMode(cwd, sessionId);
3879
- const overlay = await generateOverlay(cwd, sessionId, { orchestrationMode });
3880
- const launchAppendix = await readLaunchAppendInstructions();
3881
- const dirtyWorktreeGuidance = worktreeDirty
3882
- ? `\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.`
3883
- : "";
3884
- const sessionInstructions = launchAppendix.trim().length > 0
3885
- ? `${overlay}
3886
-
3887
- ${launchAppendix}${dirtyWorktreeGuidance}`
3888
- : `${overlay}${dirtyWorktreeGuidance}`;
3889
- await writeSessionModelInstructionsFile(cwd, sessionId, sessionInstructions);
3890
- // 4. Reset session metrics and tag the established session.
3891
- await resetSessionMetrics(cwd, sessionId);
3892
- tagCurrentTmuxSessionWithInstance(sessionId);
3893
- // 5. Start notify fallback watcher (best effort)
3894
4680
  try {
3895
4681
  await startNotifyFallbackWatcher(cwd, { codexHomeOverride, enableAuthority: enableNotifyFallbackAuthority, sessionId });
3896
4682
  }
3897
- catch (err) {
3898
- logCliOperationFailure(err);
3899
- // Non-fatal
4683
+ catch (error) {
4684
+ degraded.push("notify-watcher");
4685
+ logCliOperationFailure(error);
3900
4686
  }
3901
- // 6. Start derived watcher (best effort, opt-in)
3902
4687
  try {
3903
4688
  await startHookDerivedWatcher(cwd);
3904
4689
  }
3905
- catch (err) {
3906
- logCliOperationFailure(err);
3907
- // Non-fatal
4690
+ catch (error) {
4691
+ degraded.push("derived-watcher");
4692
+ logCliOperationFailure(error);
3908
4693
  }
3909
- // 7. Emit temp notification startup summary + warnings, then send session-start lifecycle notification (best effort)
3910
4694
  try {
3911
4695
  if (notifyTempContract?.active) {
3912
- process.env[OMX_NOTIFY_TEMP_CONTRACT_ENV] =
3913
- serializeNotifyTempContract(notifyTempContract);
4696
+ process.env[OMX_NOTIFY_TEMP_CONTRACT_ENV] = serializeNotifyTempContract(notifyTempContract);
3914
4697
  const { getNotificationConfig } = await import("../notifications/config.js");
3915
- const resolved = getNotificationConfig();
3916
- const startup = buildNotifyTempStartupMessages(notifyTempContract, Boolean(resolved?.enabled));
3917
- for (const info of startup.infoLines) {
4698
+ const startup = buildNotifyTempStartupMessages(notifyTempContract, Boolean(getNotificationConfig()?.enabled));
4699
+ for (const info of startup.infoLines)
3918
4700
  console.log(`[omx] ${info}`);
3919
- }
3920
- for (const warning of startup.warningLines) {
4701
+ for (const warning of startup.warningLines)
3921
4702
  console.warn(`[omx] ${warning}`);
3922
- }
3923
4703
  }
3924
- else {
4704
+ else
3925
4705
  delete process.env[OMX_NOTIFY_TEMP_CONTRACT_ENV];
3926
- }
3927
4706
  const { notifyLifecycle } = await import("../notifications/index.js");
3928
- await notifyLifecycle("session-start", {
3929
- sessionId,
3930
- projectPath: cwd,
3931
- projectName: basename(cwd),
3932
- });
4707
+ await notifyLifecycle("session-start", { sessionId, projectPath: cwd, projectName: basename(cwd) });
3933
4708
  }
3934
- catch (err) {
3935
- logCliOperationFailure(err);
3936
- // Non-fatal: notification failures must never block launch
4709
+ catch (error) {
4710
+ degraded.push("notification");
4711
+ logCliOperationFailure(error);
3937
4712
  }
3938
- // 8. Dispatch native hook event (best effort)
3939
4713
  try {
3940
- await emitNativeHookEvent(cwd, "session-start", {
3941
- session_id: sessionId,
3942
- context: buildNativeHookBaseContext(cwd, sessionId, "started", {
3943
- project_path: cwd,
3944
- project_name: basename(cwd),
3945
- status: "started",
3946
- }),
3947
- });
4714
+ await emitNativeHookEvent(cwd, "session-start", { session_id: sessionId, context: buildNativeHookBaseContext(cwd, sessionId, "started", { project_path: cwd, project_name: basename(cwd), status: "started" }) });
3948
4715
  }
3949
- catch (err) {
3950
- logCliOperationFailure(err);
3951
- // 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;
3952
4736
  }
4737
+ return { binding: established.binding, completion };
3953
4738
  }
3954
4739
  /**
3955
4740
  * runCodex: Launch Codex CLI (blocks until exit).
3956
4741
  * All 3 paths (new tmux, existing tmux, no tmux) block via execSync/execFileSync.
3957
4742
  */
3958
- 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;
3959
4745
  const launchArgs = injectModelInstructionsBypassArgs(cwd, args, process.env, sessionModelInstructionsPath(cwd, sessionId));
3960
4746
  const nativeWindows = isNativeWindows();
3961
4747
  const omxBin = resolveOmxCliEntryPath({ argv1: process.argv[1], cwd, env: process.env });
@@ -3963,19 +4749,59 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
3963
4749
  throw new Error("Unable to resolve OMX launcher path for tmux HUD bootstrap");
3964
4750
  }
3965
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;
3966
4787
  const omxRootOverride = runtimeContext?.omxRoot ?? resolveOmxRootForLaunch(cwd, process.env);
4788
+ const selectedOmxRootOverride = launchOwnedControlPlane
4789
+ ? launchOwnedControlPlane.values.OMX_ROOT || undefined
4790
+ : omxRootOverride;
3967
4791
  const currentPaneId = process.env.TMUX_PANE;
3968
4792
  const hudRuntimeRoot = runtimeContext
3969
4793
  ? { omxRoot: runtimeContext.omxRoot, rootSource: 'omx-root-env' }
3970
4794
  : resolveHudRuntimeRootForLaunch(cwd, process.env);
3971
- const hudRuntimeEnv = {
3972
- ...buildHudRuntimeEnv({
3973
- sessionId,
3974
- leaderPaneId: currentPaneId,
3975
- ...hudRuntimeRoot,
3976
- }).env,
3977
- ...runtimeEnvOverlay,
3978
- };
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
+ };
3979
4805
  const hudEnvArgs = Object.entries(hudRuntimeEnv).map(([key, value]) => `${key}=${value}`);
3980
4806
  const hudCmd = nativeWindows
3981
4807
  ? buildWindowsPromptCommand("node", [omxBin, "hud", "--watch"])
@@ -3990,13 +4816,17 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
3990
4816
  ...stripHermesMcpBridgeEnv(process.env),
3991
4817
  ...(codexHomeOverride ? { CODEX_HOME: codexHomeOverride } : {}),
3992
4818
  ...(sqliteHomeOverride ? { [CODEX_SQLITE_HOME_ENV]: sqliteHomeOverride } : {}),
3993
- ...(omxRootOverride ? { OMX_ROOT: omxRootOverride } : {}),
4819
+ ...(!launchOwnedControlPlane && omxRootOverride ? { OMX_ROOT: omxRootOverride } : {}),
3994
4820
  ...runtimeEnvOverlay,
4821
+ ...launchOwnedControlPlaneEnv,
3995
4822
  }, omxBin);
4823
+ // Correlates plugin hook routing only; it is not an authority token.
4824
+ const pluginHookRoutingLaunchId = randomUUID();
3996
4825
  const codexEnvWithSession = {
3997
4826
  ...codexBaseEnv,
3998
- OMX_CODEX_LAUNCH_ID: randomUUID(),
4827
+ OMX_CODEX_LAUNCH_ID: pluginHookRoutingLaunchId,
3999
4828
  ...buildHudRuntimeEnv({ sessionId }).env,
4829
+ ...launchOwnedControlPlaneEnv,
4000
4830
  };
4001
4831
  const codexEnv = workerLaunchArgs
4002
4832
  ? {
@@ -4008,8 +4838,9 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
4008
4838
  const codexEnvWithNotify = notifyTempContractRaw
4009
4839
  ? { ...codexEnv, [OMX_NOTIFY_TEMP_CONTRACT_ENV]: notifyTempContractRaw }
4010
4840
  : codexEnv;
4011
- const runtimeHookEnv = { ...process.env, ...runtimeEnvOverlay };
4012
- const { launchPolicy } = resolveTmuxAwareLaunchPolicy(explicitLaunchPolicy, nativeWindows);
4841
+ const runtimeHookEnv = launchOwnedControlPlane
4842
+ ? { ...process.env, ...runtimeEnvOverlay, ...launchOwnedControlPlaneEnv }
4843
+ : { ...process.env, ...runtimeEnvOverlay };
4013
4844
  if (isCodexVersionRequest(launchArgs)) {
4014
4845
  runCodexBlocking(cwd, launchArgs, codexEnvWithNotify);
4015
4846
  return { postLaunchHandledExternally: false };
@@ -4045,7 +4876,7 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
4045
4876
  currentPaneId,
4046
4877
  cwd,
4047
4878
  sessionId,
4048
- omxRootOverride,
4879
+ omxRootOverride: selectedOmxRootOverride,
4049
4880
  baseEnv: runtimeHookEnv,
4050
4881
  });
4051
4882
  }
@@ -4072,7 +4903,7 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
4072
4903
  currentPaneId,
4073
4904
  cwd,
4074
4905
  sessionId,
4075
- omxRootOverride,
4906
+ omxRootOverride: selectedOmxRootOverride,
4076
4907
  baseEnv: runtimeHookEnv,
4077
4908
  });
4078
4909
  }
@@ -4133,214 +4964,247 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
4133
4964
  return { postLaunchHandledExternally: false };
4134
4965
  }
4135
4966
  else {
4136
- // 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
+ }
4137
4970
  const codexCmd = buildTmuxPaneCommand("codex", launchArgs);
4138
4971
  const detachedWindowsCodexCmd = nativeWindows
4139
- ? buildWindowsPromptCommand("codex", launchArgs)
4972
+ ? buildWindowsDetachedChildCommand("codex", launchArgs)
4140
4973
  : null;
4141
4974
  const sessionName = buildDetachedTmuxSessionName(cwd, sessionId);
4142
- const launchDetachedSession = () => {
4143
- const contextKey = runtimeContext?.madmaxDetachedContext ?? process.env[OMX_MADMAX_DETACHED_CONTEXT_ENV]?.trim();
4144
- const runsRoot = resolveMadmaxRunsRoot(process.env);
4145
- const activeRecordPath = contextKey
4146
- ? madmaxDetachedActiveRecordPath(runsRoot, contextKey)
4147
- : null;
4148
- const activeRecord = activeRecordPath
4149
- ? readMadmaxDetachedActiveRecord(activeRecordPath)
4150
- : null;
4151
- if (activeRecord &&
4152
- activeRecord.context_key === contextKey &&
4153
- isReusableMadmaxDetachedActiveRecord(activeRecord)) {
4154
- cleanupCurrentMadmaxReuseRunRoot(process.env, runsRoot);
4155
- setDetachedTmuxSessionHistoryLimit(activeRecord.tmux_session_name, activeRecord.tmux_pane_id);
4156
- if (!shouldAttachDetachedTmuxSession(process.env)) {
4157
- clearDetachedTmuxSessionHistoryIfUnattached(activeRecord.tmux_session_name, activeRecord.tmux_pane_id);
4158
- 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`);
4159
- return { postLaunchHandledExternally: true };
4160
- }
4161
- process.stderr.write(`[omx] madmax detached launch already active for this context; attaching ${activeRecord.tmux_session_name} instead of starting a duplicate.\n`);
4162
- try {
4163
- execTmuxFileSync(["attach-session", "-t", activeRecord.tmux_session_name], {
4164
- stdio: "inherit",
4165
- });
4166
- }
4167
- catch (err) {
4168
- logCliOperationFailure(err);
4169
- throw new MadmaxDetachedReuseError(`refusing duplicate madmax detached launch: existing session ${activeRecord.tmux_session_name} is active but attach failed`);
4170
- }
4171
- return { postLaunchHandledExternally: true };
4172
- }
4173
- if (activeRecordPath && activeRecord) {
4174
- rmSync(activeRecordPath, { force: true });
4175
- }
4176
- let detachedSessionBindingWrite = Promise.resolve();
4177
- const writeDetachedSessionBinding = (tmuxPaneId) => {
4178
- detachedSessionBindingWrite = detachedSessionBindingWrite
4179
- .catch((err) => {
4180
- logCliOperationFailure(err);
4181
- })
4182
- .then(() => writeSessionStart(cwd, sessionId, {
4183
- tmuxSessionName: sessionName,
4184
- ...(tmuxPaneId ? { tmuxPaneId } : {}),
4185
- }));
4186
- void detachedSessionBindingWrite.catch((err) => {
4187
- logCliOperationFailure(err);
4188
- // Non-fatal: managed tmux recovery can still use compatibility fallback.
4189
- });
4190
- };
4191
- writeDetachedSessionBinding();
4192
- let createdDetachedSession = false;
4193
- let registeredHookTarget = null;
4194
- let registeredHookName = null;
4195
- let registeredClientAttachedHookName = null;
4196
- let detachedParentEnvFilePath;
4197
- let detachedLeaderPaneId = null;
4198
- try {
4199
- // This path is the user-shell interactive launch: OMX creates a tmux
4200
- // session and immediately attaches the user's terminal to it. If a tmux
4201
- // server already exists, `new-session -e` only forwards explicit values,
4202
- // so provider-specific parent-shell keys would disappear. Source a
4203
- // private env file inside the leader shell instead of putting every
4204
- // parent env value on the tmux command line or in logs.
4205
- 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)
4206
4990
  detachedParentEnvFilePath = writeDetachedSessionParentEnvFile(cwd, sessionId, codexEnvWithNotify);
4207
- }
4208
- 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);
4209
4997
  for (const step of bootstrapSteps) {
4210
- const output = execTmuxFileSync(step.args, {
4211
- stdio: "pipe",
4212
- encoding: "utf-8",
4213
- });
4214
- if (step.name === "new-session") {
4215
- createdDetachedSession = true;
4216
- const leaderPaneId = parsePaneIdFromTmuxOutput(output || "");
4217
- 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");
4218
5005
  detachedLeaderPaneId = leaderPaneId;
4219
- setDetachedTmuxSessionHistoryLimit(sessionName, leaderPaneId);
4220
- if (activeRecordPath && contextKey) {
4221
- writeMadmaxDetachedActiveRecord(activeRecordPath, {
4222
- version: 1,
4223
- context_key: contextKey,
4224
- created_at: new Date().toISOString(),
4225
- source_cwd: runtimeContext?.sourceCwd ?? process.env.OMX_SOURCE_CWD ?? cwd,
4226
- ...(runtimeContext?.worktreeCwd ? { worktree_cwd: runtimeContext.worktreeCwd } : {}),
4227
- argv: args,
4228
- run_dir: runtimeContext?.omxRoot ?? process.env.OMX_ROOT ?? cwd,
4229
- tmux_session_name: sessionName,
4230
- session_id: sessionId,
4231
- tmux_pane_id: leaderPaneId,
4232
- });
4233
- }
4234
- writeDetachedSessionBinding(leaderPaneId);
5006
+ detachedLeaderAuthority = captureDetachedLeaderAuthority(leaderPaneId, sessionName, sessionId);
5007
+ continue;
4235
5008
  }
4236
- }
4237
- if (step.name === "split-and-capture-hud-pane") {
4238
- const hudPaneId = parsePaneIdFromTmuxOutput(output || "");
4239
- const hookWindowIndex = hudPaneId
4240
- ? detectDetachedSessionWindowIndex(sessionName)
4241
- : null;
4242
- const hookTarget = hudPaneId && hookWindowIndex
4243
- ? buildResizeHookTarget(sessionName, hookWindowIndex)
4244
- : null;
4245
- const hookName = hudPaneId && hookWindowIndex
4246
- ? buildResizeHookName("launch", sessionName, hookWindowIndex, hudPaneId)
4247
- : null;
4248
- const clientAttachedHookName = hudPaneId && hookWindowIndex
4249
- ? buildClientAttachedReconcileHookName("launch", sessionName, hookWindowIndex, hudPaneId)
4250
- : null;
4251
- const finalizeSteps = buildDetachedSessionFinalizeSteps(sessionName, hudPaneId, hookWindowIndex, process.env.OMX_MOUSE !== "0", nativeWindows, shouldAttachDetachedTmuxSession(process.env), detachedLeaderPaneId);
4252
- if (nativeWindows && detachedWindowsCodexCmd) {
4253
- 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;
4254
5021
  }
4255
- for (const finalizeStep of finalizeSteps) {
4256
- if (finalizeStep.name === "sanitize-copy-mode-style") {
4257
- try {
4258
- mitigateCopyModeUnderlineArtifacts(sessionName);
4259
- }
4260
- catch (err) {
4261
- logCliOperationFailure(err);
4262
- }
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;
4263
5030
  continue;
4264
5031
  }
4265
- const stdio = finalizeStep.name === "attach-session" ? "inherit" : "ignore";
4266
- try {
4267
- const startedAtMs = Date.now();
4268
- execTmuxFileSync(finalizeStep.args, { stdio });
4269
- if (finalizeStep.name === "attach-session") {
4270
- assertDetachedAttachDidNotNoop(sessionName, Date.now() - startedAtMs, process.env);
4271
- }
4272
- }
4273
- catch (err) {
4274
- logCliOperationFailure(err);
4275
- if (finalizeStep.name === "attach-session")
4276
- throw new Error("failed to attach detached tmux session");
5032
+ if (finalizeStep.name === "sanitize-copy-mode-style")
4277
5033
  continue;
4278
- }
4279
- if (finalizeStep.name === "register-resize-hook" &&
4280
- hookTarget &&
4281
- hookName) {
4282
- registeredHookTarget = hookTarget;
4283
- registeredHookName = hookName;
4284
- }
4285
- if (finalizeStep.name === "register-client-attached-reconcile" &&
4286
- clientAttachedHookName) {
4287
- registeredClientAttachedHookName = clientAttachedHookName;
4288
- }
4289
- if (finalizeStep.name === "reconcile-hud-resize") {
4290
- registerDetachedHudLayoutReconcileHook({
4291
- hudPaneId,
4292
- detachedLeaderPaneId,
4293
- cwd,
4294
- sessionId,
4295
- omxBin,
4296
- omxRootOverride,
4297
- baseEnv: runtimeHookEnv,
4298
- });
4299
- }
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);
4300
5044
  }
4301
5045
  }
5046
+ catch (error) {
5047
+ throw new DetachedLaunchSafetyError(step.name === "new-session" ? "inert-session" : "pane-id", error, detachedPreflight.report);
5048
+ }
4302
5049
  }
4303
- return { postLaunchHandledExternally: !nativeWindows };
4304
- }
4305
- catch (err) {
4306
- if (detachedParentEnvFilePath) {
4307
- rmSync(detachedParentEnvFilePath, { force: true });
4308
- }
4309
- if (activeRecordPath) {
4310
- rmSync(activeRecordPath, { force: true });
4311
- }
4312
- if (createdDetachedSession) {
4313
- const rollbackSteps = buildDetachedSessionRollbackSteps(sessionName, registeredHookTarget, registeredHookName, registeredClientAttachedHookName);
4314
- 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);
4315
5164
  try {
4316
- execTmuxFileSync(rollbackStep.args, { stdio: "ignore" });
5165
+ await operation();
4317
5166
  }
4318
- catch (rollbackErr) {
4319
- logCliOperationFailure(rollbackErr);
4320
- // 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
+ });
4321
5186
  }
4322
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;
4323
5200
  }
4324
- throw err;
4325
5201
  }
5202
+ return { postLaunchHandledExternally: true };
4326
5203
  };
4327
- const contextKey = process.env[OMX_MADMAX_DETACHED_CONTEXT_ENV]?.trim();
4328
- const runsRoot = resolveMadmaxRunsRoot(process.env);
4329
- try {
4330
- if (isMadmaxDetachedGuardEnabled(process.env) && contextKey) {
4331
- return withMadmaxDetachedContextLock(runsRoot, contextKey, launchDetachedSession);
4332
- }
4333
- return launchDetachedSession();
4334
- }
4335
- catch (err) {
4336
- if (err instanceof MadmaxDetachedReuseError || err instanceof MadmaxDetachedGuardError) {
4337
- throw err;
4338
- }
4339
- logCliOperationFailure(err);
4340
- // tmux not available or failed, just run codex directly
4341
- runCodexBlocking(cwd, launchArgs, codexEnvWithNotify);
4342
- return { postLaunchHandledExternally: false };
5204
+ if (contextKey) {
5205
+ return await withMadmaxDetachedContextLock(runsRoot, contextKey, launchDetachedSession);
4343
5206
  }
5207
+ return await launchDetachedSession();
4344
5208
  }
4345
5209
  }
4346
5210
  function listHudWatchPaneIdsInCurrentWindow(currentPaneId, owner = {}) {
@@ -4383,8 +5247,9 @@ export function buildTmuxShellCommand(command, args) {
4383
5247
  function encodePowerShellCommand(commandText) {
4384
5248
  return Buffer.from(commandText, "utf16le").toString("base64");
4385
5249
  }
4386
- function isCodexVersionRequest(args) {
4387
- return args.some((arg) => CODEX_VERSION_FLAGS.has(arg));
5250
+ export function isCodexVersionRequest(args) {
5251
+ const { omxArgs } = splitOmxArgsAtEndOfOptions(args);
5252
+ return omxArgs.some((arg) => CODEX_VERSION_FLAGS.has(arg));
4388
5253
  }
4389
5254
  export function buildWindowsPromptCommand(command, args) {
4390
5255
  const invocation = [
@@ -4399,9 +5264,27 @@ export function buildWindowsPromptCommand(command, args) {
4399
5264
  return `powershell.exe -NoLogo -NoExit -EncodedCommand ${encodePowerShellCommand(wrappedCommand)}`;
4400
5265
  }
4401
5266
  /**
4402
- * Wrap a command for tmux pane execution while preserving the tmux pane cwd.
4403
- * tmux already starts the pane at `-c <cwd>`; using a login shell here can
4404
- * reset that cwd back to the shell's startup directory on some setups.
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) {
5272
+ const invocation = [
5273
+ "&",
5274
+ quotePowerShellArg(command),
5275
+ ...args.map(quotePowerShellArg),
5276
+ ].join(" ");
5277
+ const wrappedCommand = [
5278
+ "$ErrorActionPreference = 'Stop'",
5279
+ invocation,
5280
+ "exit $LASTEXITCODE",
5281
+ ].join("; ");
5282
+ return `powershell.exe -NoLogo -NoProfile -NonInteractive -EncodedCommand ${encodePowerShellCommand(wrappedCommand)}`;
5283
+ }
5284
+ /**
5285
+ * Wrap a command for tmux pane execution while preserving the tmux pane cwd.
5286
+ * tmux already starts the pane at `-c <cwd>`; using a login shell here can
5287
+ * reset that cwd back to the shell's startup directory on some setups.
4405
5288
  *
4406
5289
  * Do not source user shell rc files by default. In issue #2282 the surviving
4407
5290
  * OOM signature was thousands of bash processes, not MCP node children;
@@ -4434,45 +5317,323 @@ function quoteShellArg(value) {
4434
5317
  function quotePowerShellArg(value) {
4435
5318
  return `'${value.replace(/'/g, "''")}'`;
4436
5319
  }
4437
- export function buildDetachedWindowsBootstrapScript(sessionName, commandText, delayMs = WINDOWS_DETACHED_BOOTSTRAP_DELAY_MS, tmuxCommand = resolveTmuxExecutableForLaunch()) {
4438
- const delay = Number.isFinite(delayMs) && delayMs > 0
4439
- ? Math.floor(delayMs)
4440
- : WINDOWS_DETACHED_BOOTSTRAP_DELAY_MS;
4441
- const targetLiteral = JSON.stringify(`${sessionName}:0.0`);
4442
- const commandLiteral = JSON.stringify(commandText);
4443
- const tmuxCommandLiteral = JSON.stringify(tmuxCommand);
4444
- return [
4445
- "const { execFileSync } = require('child_process');",
4446
- `const tmuxCommand = ${tmuxCommandLiteral};`,
4447
- `setTimeout(() => {`,
4448
- `try { execFileSync(tmuxCommand, ['send-keys', '-t', ${targetLiteral}, '-l', '--', ${commandLiteral}], { stdio: 'ignore' }); } catch {}`,
4449
- `try { execFileSync(tmuxCommand, ['send-keys', '-t', ${targetLiteral}, 'C-m'], { stdio: 'ignore' }); } catch {}`,
4450
- `}, ${delay});`,
4451
- ].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
+ };
4452
5362
  }
4453
- function scheduleDetachedWindowsCodexLaunch(sessionName, commandText) {
4454
- const child = spawn(process.execPath, ["-e", buildDetachedWindowsBootstrapScript(sessionName, commandText)], {
4455
- detached: true,
4456
- stdio: "ignore",
4457
- 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,
4458
5431
  });
4459
- 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
+ }
4460
5610
  }
4461
5611
  /**
4462
5612
  * postLaunch: Clean up after Codex exits.
4463
5613
  * Each step is independently fault-tolerant (try/catch per step).
4464
5614
  */
4465
- export async function postLaunch(cwd, sessionId, codexHomeOverride, enableNotifyFallbackAuthority = false, projectLocalCodexHomeForCleanup) {
4466
- // Capture session start time before cleanup (writeSessionEnd deletes session.json)
4467
- 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;
4468
5620
  try {
4469
- const sessionState = await readSessionState(cwd);
4470
- 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"}`);
4471
5627
  }
4472
- catch (err) {
4473
- logCliOperationFailure(err);
4474
- // Non-fatal
5628
+ catch (error) {
5629
+ terminalError = error;
4475
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?.();
4476
5637
  // 0. Reap MCP orphans left behind by the session that just exited.
4477
5638
  await reapPostLaunchOrphanedMcpProcesses();
4478
5639
  // 0. Flush fallback watcher once to reduce race with fast codex exit.
@@ -4523,13 +5684,6 @@ export async function postLaunch(cwd, sessionId, codexHomeOverride, enableNotify
4523
5684
  catch (err) {
4524
5685
  console.error(`[omx] postLaunch: model instructions cleanup failed: ${err instanceof Error ? err.message : err}`);
4525
5686
  }
4526
- // 2. Archive session (write history, delete session.json)
4527
- try {
4528
- await writeSessionEnd(cwd, sessionId);
4529
- }
4530
- catch (err) {
4531
- console.error(`[omx] postLaunch: session archive failed: ${err instanceof Error ? err.message : err}`);
4532
- }
4533
5687
  // 2.5. Best-effort wiki session capture
4534
5688
  try {
4535
5689
  const { onSessionEnd } = await import("../wiki/lifecycle.js");
@@ -4601,10 +5755,59 @@ export async function postLaunch(cwd, sessionId, codexHomeOverride, enableNotify
4601
5755
  logCliOperationFailure(err);
4602
5756
  // Non-fatal
4603
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
+ }
4604
5762
  }
4605
- export async function runDetachedSessionPostLaunch(cwd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup) {
4606
- await postLaunch(cwd, sessionId, codexHomeOverride, false, projectLocalCodexHomeForCleanup);
4607
- 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");
4608
5811
  }
4609
5812
  async function emitNativeHookEvent(cwd, event, opts = {}) {
4610
5813
  const payload = buildHookEvent(event, {
@@ -5043,14 +6246,13 @@ async function listHookVisibleRunDirStateRefs(cwd) {
5043
6246
  if (!sourceCwd || !runDir)
5044
6247
  return;
5045
6248
  try {
5046
- if (canonicalizePathForRunDirMatch(sourceCwd) !== canonicalCwd &&
5047
- (!worktreeCwd || canonicalizePathForRunDirMatch(worktreeCwd) !== canonicalCwd))
6249
+ if (canonicalizePathForRunDirMatch(sourceCwd) !== canonicalCwd
6250
+ && (!worktreeCwd || canonicalizePathForRunDirMatch(worktreeCwd) !== canonicalCwd))
5048
6251
  return;
5049
6252
  const resolvedRunDir = resolve(runDir);
5050
6253
  if (resolvedRunDir !== canonicalRunsRoot
5051
- && !resolvedRunDir.startsWith(`${canonicalRunsRoot}/`)) {
6254
+ && !resolvedRunDir.startsWith(`${canonicalRunsRoot}/`))
5052
6255
  return;
5053
- }
5054
6256
  runDirs.add(resolvedRunDir);
5055
6257
  }
5056
6258
  catch {
@@ -5103,7 +6305,7 @@ async function listHookVisibleRunDirStateRefs(cwd) {
5103
6305
  for (const dir of candidateDirs) {
5104
6306
  const files = await readdir(dir).catch(() => []);
5105
6307
  for (const file of files) {
5106
- if (!file.endsWith("-state.json") || file === "session.json")
6308
+ if (!isModeStateFilename(file))
5107
6309
  continue;
5108
6310
  const path = join(dir, file);
5109
6311
  if (seenPaths.has(path))
@@ -5119,41 +6321,362 @@ async function listHookVisibleRunDirStateRefs(cwd) {
5119
6321
  }
5120
6322
  return refs.sort((a, b) => a.mode.localeCompare(b.mode));
5121
6323
  }
5122
- async function cancelModes(args = []) {
5123
- const { writeFile, readFile } = await import("fs/promises");
5124
- 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();
5125
6552
  const nowIso = new Date().toISOString();
5126
- 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;
5127
6560
  try {
5128
6561
  const writableScope = await resolveWritableStateScope(cwd);
5129
- 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) => {
5130
6568
  const loaded = new Map();
6569
+ if (refs.length === 0)
6570
+ return loaded;
6571
+ const canonicalAuthorityRoot = assertCancellationAuthorityPath(authorityRoot === writableScope.stateDir ? baseStateDir : authorityRoot, authorityRoot);
5131
6572
  for (const ref of refs) {
5132
- 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");
5133
6584
  let parsedState;
5134
6585
  try {
5135
- 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;
5136
6591
  }
5137
6592
  catch (err) {
5138
6593
  logCliOperationFailure(err);
5139
- 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
+ }
5140
6638
  }
5141
6639
  loaded.set(ref.mode, {
5142
- path: ref.path,
6640
+ path: canonicalPath,
5143
6641
  scope: ref.scope,
5144
6642
  state: parsedState,
6643
+ originalContent: content,
6644
+ dev: fileStat.dev,
6645
+ ino: fileStat.ino,
5145
6646
  });
5146
6647
  }
5147
6648
  return loaded;
5148
6649
  };
5149
- 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);
5150
6665
  const hasActiveWorkflowMode = (entries) => [...entries.entries()].some(([mode, entry]) => mode !== SKILL_ACTIVE_STATE_MODE && entry.state.active === true);
5151
- if (!hasActiveWorkflowMode(states)) {
5152
- const runDirStates = await loadStates(await listHookVisibleRunDirStateRefs(cwd));
5153
- if (hasActiveWorkflowMode(runDirStates))
5154
- 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
+ }
5155
6678
  }
5156
- const currentSessionId = writableScope.sessionId ?? "";
6679
+ const currentSessionId = writableScope.sessionId ?? runSelection?.sessionId ?? "";
5157
6680
  const changed = new Set();
5158
6681
  const reported = new Set();
5159
6682
  const cancelMode = (mode, phase = "cancelled", reportIfWasActive = true) => {
@@ -5184,13 +6707,74 @@ async function cancelModes(args = []) {
5184
6707
  if (reportIfWasActive && wasActive && mode !== SKILL_ACTIVE_STATE_MODE)
5185
6708
  reported.add(mode);
5186
6709
  };
5187
- 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
+ };
5188
6770
  const ralph = states.get("ralph");
5189
6771
  const hadActiveRalph = !!(ralph && ralph.state.active === true);
5190
6772
  if (ralph && ralph.state.active === true) {
6773
+ const linkedMode = linkedRalphMode(ralph.state);
6774
+ if (linkedMode)
6775
+ cancelMode(linkedMode, "cancelled", true);
5191
6776
  cancelMode("ralph", "cancelled", true);
5192
- if (ralphLinksUltrawork(ralph.state))
5193
- cancelMode("ultrawork", "cancelled", true);
6777
+ cancelRalphSkillEntries(linkedMode);
5194
6778
  }
5195
6779
  if (!hadActiveRalph) {
5196
6780
  for (const [mode, entry] of states.entries()) {
@@ -5198,11 +6782,11 @@ async function cancelModes(args = []) {
5198
6782
  cancelMode(mode, "cancelled", true);
5199
6783
  }
5200
6784
  }
5201
- for (const [mode, entry] of states.entries()) {
5202
- if (!changed.has(mode))
5203
- continue;
5204
- await writeFile(entry.path, JSON.stringify(entry.state, null, 2));
5205
- }
6785
+ const assertRunAuthority = () => {
6786
+ if (runSelection && !hasMatchingMadmaxDetachedRuntimeBinding(runSelection.record)) {
6787
+ throw new Error("Refusing cancellation because detached run authority changed.");
6788
+ }
6789
+ };
5206
6790
  if (force && currentSessionId) {
5207
6791
  const stopStateEntries = [...states.entries()].filter(([mode]) => mode === "native-stop");
5208
6792
  for (const [, entry] of stopStateEntries) {
@@ -5213,10 +6797,85 @@ async function cancelModes(args = []) {
5213
6797
  continue;
5214
6798
  delete sessions[currentSessionId];
5215
6799
  entry.state.sessions = sessions;
5216
- await writeFile(entry.path, JSON.stringify(entry.state, null, 2));
5217
6800
  changed.add("native-stop");
5218
6801
  }
5219
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
+ }
5220
6879
  for (const mode of reported) {
5221
6880
  console.log(`Cancelled: ${mode}`);
5222
6881
  }
@@ -5226,7 +6885,11 @@ async function cancelModes(args = []) {
5226
6885
  }
5227
6886
  catch (err) {
5228
6887
  logCliOperationFailure(err);
5229
- console.log("No active modes to cancel.");
6888
+ process.exitCode = 1;
5230
6889
  }
5231
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
+ }
5232
6895
  //# sourceMappingURL=index.js.map