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
@@ -1,14 +1,17 @@
1
1
  import { describe, it } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { existsSync } from 'node:fs';
4
- import { chmod, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises';
5
- import { execFileSync, spawnSync } from 'node:child_process';
3
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { chmod, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
5
+ import { execFileSync, spawn, spawnSync } from 'node:child_process';
6
6
  import { tmpdir } from 'node:os';
7
7
  import { basename, dirname, join } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
- import { HUD_TMUX_HEIGHT_LINES } from '../../hud/constants.js';
10
- import { DETACHED_TMUX_HISTORY_LIMIT } from '../index.js';
9
+ import { HUD_RESIZE_RECONCILE_DELAY_SECONDS, HUD_TMUX_HEIGHT_LINES } from '../../hud/constants.js';
10
+ import { DETACHED_LAUNCH_CONTROL_PLANE_KEYS, DETACHED_TMUX_HISTORY_LIMIT, buildMadmaxDetachedLaunchContextKey } from '../index.js';
11
+ import { closeLaunchSessionBindingOnce, establishLaunchSessionBinding, finalizeBoundOnce, writeSessionStart, } from '../../hooks/session.js';
12
+ import { buildPtyScriptCommand, isRealScriptAvailable, isRealTmuxAvailable, withTempTmuxSession } from '../../team/__tests__/tmux-test-fixture.js';
11
13
  const CLI_SPAWN_TIMEOUT_MS = 60_000;
14
+ const DARWIN_PTY_FINALIZATION_POLL_LIMIT = 600;
12
15
  function buildRunOmxEnv(envOverrides) {
13
16
  const env = { ...process.env };
14
17
  for (const key of Object.keys(env)) {
@@ -50,8 +53,10 @@ function normalizeDarwinTmpPath(value) {
50
53
  function escapeRegExp(value) {
51
54
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
52
55
  }
53
- function shouldSkipForSpawnPermissions(err) {
54
- return typeof err === 'string' && /(EPERM|EACCES)/i.test(err);
56
+ function shouldSkipForSpawnPermissions(_err) {
57
+ // These integration fixtures are contractual: EPERM/EACCES is a failure,
58
+ // not a pass. Node's test runner has no portable dynamic skip here.
59
+ return false;
55
60
  }
56
61
  async function createGitRepo(wd) {
57
62
  const repo = join(wd, 'repo');
@@ -68,17 +73,167 @@ async function writeExecutable(path, content) {
68
73
  await writeFile(path, content);
69
74
  await chmod(path, 0o755);
70
75
  }
76
+ async function wrapFakeTmuxWithDetachedLeader(fakeTmuxPath) {
77
+ const implementationPath = `${fakeTmuxPath}.impl`;
78
+ await rename(fakeTmuxPath, implementationPath);
79
+ await writeExecutable(fakeTmuxPath, `#!/bin/sh
80
+ if [ "$1" = "display-message" ] && [ "$2" = "-p" ] && [ "$3" = "-t" ] && case "$5" in *'#{session_created}'*'#{window_id}'*'#{pane_pid}'*) true;; *) false;; esac; then
81
+ session=$(cat /tmp/omx-test-detached-session-name 2>/dev/null || printf omx-test)
82
+ leader_pid=$(cat "$0.detached-leader-pid" 2>/dev/null || printf 123)
83
+ printf '%s\t$1\t1\t0\t@1\t%s\t%s\n' "$session" "$4" "$leader_pid"
84
+ exit 0
85
+ fi
86
+ if [ "$1" = "set-option" ] && [ "$2" = "-t" ] && [ "$4" = "@omx_instance_id" ]; then
87
+ printf '%s' "$5" > /tmp/omx-test-detached-owner-id
88
+ fi
89
+ if [ "$1" = "list-panes" ] && case "$*" in *'#{session_created}'*'#{@omx_instance_id}'*) true;; *) false;; esac; then
90
+ ${JSON.stringify(implementationPath)} "$@" >/dev/null 2>&1 || true
91
+ session=$(cat /tmp/omx-test-detached-session-name 2>/dev/null || printf omx-test)
92
+ owner=$(cat /tmp/omx-test-detached-owner-id 2>/dev/null || printf '')
93
+ leader_pid=$(cat "$0.detached-leader-pid" 2>/dev/null || printf 123)
94
+ pane=$(cat "$0.detached-leader-pane" 2>/dev/null || printf '%%12')
95
+ printf '%s\t0\t%s\t%s\t$1\t1\t%s\n' "$pane" "$leader_pid" "$session" "$owner"
96
+ exit 0
97
+ fi
98
+ if [ "$1" = "if-shell" ] && [ "$2" = "-F" ] && [ "$3" = "-t" ]; then
99
+ if [ -f "$0.detached-recycled" ] && [ "$4" = "%99" ]; then ${JSON.stringify(implementationPath)} __nested_hud_guard_denied__ %99 999 '$2' @2; exit 0; fi
100
+ ${JSON.stringify(implementationPath)} "$@" >/dev/null 2>&1 || true
101
+ success="$6"
102
+ receipt=$(printf '%s' "$success" | sed -n 's/.*\\(omx_detached_[A-Za-z0-9-]*\\).*/\\1/p')
103
+ case "\${OMX_TEST_DETACHED_RECYCLE:-}" in
104
+ split) case "$success" in *split-window*) exit 0;; esac ;;
105
+ finalization) case "$*" in
106
+ *split-window*) : > "$0.detached-split-seen" ;;
107
+ *"if-shell -F -t '%99'"*) if [ -f "$0.detached-split-seen" ]; then
108
+ ${JSON.stringify(implementationPath)} __recycled__ %99 999 '$2' @2
109
+ : > "$0.detached-recycled"
110
+ eval "set -- $success"
111
+ "$0" "$@" >/dev/null
112
+ fi ;;
113
+ esac ;;
114
+ esac
115
+ [ -n "$receipt" ] || exit 1
116
+ case "$success" in
117
+ *split-window*) printf '%%99\t456\t$1\t@1\t%s\n' "$receipt" ;;
118
+ *) printf '%s\n' "$receipt" ;;
119
+ esac
120
+ exit 0
121
+ fi
122
+ if [ "$1" = "list-panes" ] && case "$*" in *'#{pane_id}'*'#{pane_dead}'*'#{pane_pid}'*) true;; *) false;; esac; then
123
+ leader_pid=$(cat "$0.detached-leader-pid" 2>/dev/null || printf '')
124
+ [ -n "$leader_pid" ] || exit 1
125
+ pane=$(cat "$0.detached-leader-pane" 2>/dev/null || printf '%%12')
126
+ printf '%s\t0\t%s\n' "$pane" "$leader_pid"
127
+ exit 0
128
+ fi
129
+ output=$(${JSON.stringify(implementationPath)} "$@")
130
+ status=$?
131
+ printf '%s' "$output"
132
+ if [ "$status" -eq 0 ] && [ "$1" = "new-session" ]; then
133
+ previous=''
134
+ for arg in "$@"; do
135
+ if [ "$previous" = '-s' ]; then printf '%s' "$arg" > /tmp/omx-test-detached-session-name; break; fi
136
+ previous="$arg"
137
+ done
138
+ for last_arg do :; done
139
+ pane=$(printf '%s' "$output" | sed -n '1p')
140
+ TMUX=/tmp/omx-test-tmux,1,0 TMUX_PANE="$pane" nohup /bin/sh -c 'eval "exec $1"' sh "$last_arg" </dev/null >/tmp/omx-test-detached-leader.log 2>&1 &
141
+ leader_pid=$!
142
+ printf '%s' "$leader_pid" > "$0.detached-leader-pid"
143
+ printf '%s' "$pane" > "$0.detached-leader-pane"
144
+ (while kill -0 "$leader_pid" 2>/dev/null; do sleep 0.02; done; printf done > ${JSON.stringify(`${fakeTmuxPath}.leader-done`)}) </dev/null >/dev/null 2>&1 &
145
+ fi
146
+ exit "$status"
147
+ `);
148
+ }
71
149
  async function createLaunchFixture(wd, tmuxScript) {
72
150
  const home = join(wd, 'home');
73
151
  const fakeBin = join(wd, 'bin');
74
152
  const tmuxLogPath = join(wd, 'tmux.log');
153
+ const leaderDonePath = join(wd, 'leader-done');
75
154
  await mkdir(home, { recursive: true });
76
155
  await mkdir(fakeBin, { recursive: true });
77
- await writeExecutable(join(fakeBin, 'codex'), '#!/bin/sh\nprintf \'fake-codex:%s\\n\' "$*"\n');
156
+ await writeExecutable(join(fakeBin, 'codex'), '#!/bin/sh\nprintf \'fake-codex:%s\\n\' "$*"\nsleep 1\n');
78
157
  await writeExecutable(join(fakeBin, 'ps'), '#!/bin/sh\nexit 0\n');
79
- await writeExecutable(join(fakeBin, 'tmux'), tmuxScript(tmuxLogPath));
158
+ const tmuxImpl = join(fakeBin, 'tmux-impl');
159
+ await writeExecutable(tmuxImpl, tmuxScript(tmuxLogPath));
160
+ await writeExecutable(join(fakeBin, 'tmux'), `#!/bin/sh
161
+ if [ "$1" = "display-message" ] && [ "$2" = "-p" ] && [ "$3" = "-t" ] && case "$5" in *'#{session_created}'*'#{window_id}'*'#{pane_pid}'*) true;; *) false;; esac; then
162
+ session=$(cat /tmp/omx-test-detached-session-name 2>/dev/null || printf omx-test)
163
+ leader_pid=$(cat "$0.detached-leader-pid" 2>/dev/null || printf 123)
164
+ printf '%s\t$1\t1\t0\t@1\t%s\t%s\n' "$session" "$4" "$leader_pid"
165
+ exit 0
166
+ fi
167
+ if [ "$1" = "set-option" ] && [ "$2" = "-t" ] && [ "$4" = "@omx_instance_id" ]; then
168
+ printf '%s' "$5" > /tmp/omx-test-detached-owner-id
169
+ fi
170
+ if [ "$1" = "list-panes" ] && case "$*" in *'#{session_created}'*'#{@omx_instance_id}'*) true;; *) false;; esac; then
171
+ ${JSON.stringify(tmuxImpl)} "$@" >/dev/null 2>&1 || true
172
+ session=$(cat /tmp/omx-test-detached-session-name 2>/dev/null || printf omx-test)
173
+ owner=$(cat /tmp/omx-test-detached-owner-id 2>/dev/null || printf '')
174
+ leader_pid=$(cat "$0.detached-leader-pid" 2>/dev/null || printf 123)
175
+ pane=$(cat "$0.detached-leader-pane" 2>/dev/null || printf '%%12')
176
+ printf '%s\t0\t%s\t%s\t$1\t1\t%s\n' "$pane" "$leader_pid" "$session" "$owner"
177
+ exit 0
178
+ fi
179
+ if [ "$1" = "list-panes" ] && case "$*" in *'#{pane_id}'*'#{pane_dead}'*'#{pane_pid}'*) true;; *) false;; esac; then
180
+ leader_pid=$(cat "$0.detached-leader-pid" 2>/dev/null || printf '')
181
+ [ -n "$leader_pid" ] || exit 1
182
+ pane=$(cat "$0.detached-leader-pane" 2>/dev/null || printf '%%12')
183
+ printf '%s\t0\t%s\n' "$pane" "$leader_pid"
184
+ exit 0
185
+ fi
186
+ if [ "$1" = "if-shell" ] && [ "$2" = "-F" ] && [ "$3" = "-t" ]; then
187
+ if [ -f "$0.detached-recycled" ] && [ "$4" = "%99" ]; then ${JSON.stringify(tmuxImpl)} __nested_hud_guard_denied__ %99 999 '$2' @2; exit 0; fi
188
+ ${JSON.stringify(tmuxImpl)} "$@" >/dev/null 2>&1 || true
189
+ success="$6"
190
+ receipt=$(printf '%s' "$success" | sed -n 's/.*\\(omx_detached_[A-Za-z0-9-]*\\).*/\\1/p')
191
+ case "\${OMX_TEST_DETACHED_RECYCLE:-}" in
192
+ split) case "$success" in *split-window*) exit 0;; esac ;;
193
+ finalization) case "$*" in
194
+ *split-window*) : > "$0.detached-split-seen" ;;
195
+ *"if-shell -F -t '%99'"*) if [ -f "$0.detached-split-seen" ]; then
196
+ ${JSON.stringify(tmuxImpl)} __recycled__ %99 999 '$2' @2
197
+ : > "$0.detached-recycled"
198
+ eval "set -- $success"
199
+ "$0" "$@" >/dev/null
200
+ fi ;;
201
+ esac ;;
202
+ esac
203
+ [ -n "$receipt" ] || exit 1
204
+ case "$success" in
205
+ *split-window*) printf '%%99\t456\t$1\t@1\t%s\n' "$receipt" ;;
206
+ *) printf '%s\n' "$receipt" ;;
207
+ esac
208
+ exit 0
209
+ fi
210
+ output=$(${JSON.stringify(tmuxImpl)} "$@")
211
+ status=$?
212
+ printf '%s' "$output"
213
+ if [ "$status" -eq 0 ] && [ "$1" = "new-session" ]; then
214
+ previous=''
215
+ for arg in "$@"; do
216
+ if [ "$previous" = '-s' ]; then printf '%s' "$arg" > /tmp/omx-test-detached-session-name; break; fi
217
+ previous="$arg"
218
+ done
219
+ for last_arg do :; done
220
+ previous=''
221
+ for arg in "$@"; do
222
+ if [ "$previous" = '-e' ]; then export "$arg"; fi
223
+ previous="$arg"
224
+ done
225
+ pane=$(printf '%s' "$output" | sed -n '1p')
226
+ TMUX=/tmp/omx-test-tmux,1,0 TMUX_PANE="$pane" nohup /bin/sh -c 'eval "exec $1"' sh "$last_arg" </dev/null >/tmp/omx-test-detached-leader.log 2>&1 &
227
+ leader_pid=$!
228
+ printf '%s' "$leader_pid" > "$0.detached-leader-pid"
229
+ printf '%s' "$pane" > "$0.detached-leader-pane"
230
+ (while kill -0 "$leader_pid" 2>/dev/null; do sleep 0.02; done; printf done > ${JSON.stringify(join(wd, 'leader-done'))}) </dev/null >/dev/null 2>&1 &
231
+ fi
232
+ exit "$status"
233
+ `);
80
234
  return {
81
235
  tmuxLogPath,
236
+ leaderDonePath,
82
237
  env: {
83
238
  HOME: home,
84
239
  PATH: `${fakeBin}:/usr/bin:/bin`,
@@ -93,6 +248,104 @@ async function createLaunchFixture(wd, tmuxScript) {
93
248
  },
94
249
  };
95
250
  }
251
+ function startHeldOmx(cwd, envOverrides) {
252
+ const testDir = dirname(fileURLToPath(import.meta.url));
253
+ const repoRoot = join(testDir, '..', '..', '..');
254
+ return spawn(process.execPath, [join(repoRoot, 'dist', 'cli', 'omx.js'), '--direct', '--version'], {
255
+ cwd,
256
+ env: buildRunOmxEnv(envOverrides),
257
+ stdio: 'inherit',
258
+ });
259
+ }
260
+ async function waitForPath(path, expectedLines = 1, timeoutMs = 6_000) {
261
+ const deadline = Date.now() + timeoutMs;
262
+ while (Date.now() < deadline) {
263
+ if (existsSync(path)) {
264
+ const contents = await readFile(path, 'utf-8').catch(() => '');
265
+ if (contents.trim().split('\n').filter(Boolean).length >= expectedLines)
266
+ return;
267
+ }
268
+ await new Promise((resolve) => setTimeout(resolve, 20));
269
+ }
270
+ throw new Error(`timed out waiting for ${path}`);
271
+ }
272
+ function parseShimTmuxArgv(contents) {
273
+ return contents
274
+ .split('tmux argv:\n')
275
+ .slice(1)
276
+ .map((record) => record.split('\nend tmux argv')[0].split('\n').filter(Boolean));
277
+ }
278
+ async function assertShimLogQuiescent(path, quietPeriodMs, message) {
279
+ const before = await readFile(path, 'utf-8').catch(() => '');
280
+ await new Promise((resolve) => setTimeout(resolve, quietPeriodMs));
281
+ assert.equal(await readFile(path, 'utf-8').catch(() => ''), before, message);
282
+ }
283
+ async function waitForServerLogSubstring(readServerLog, offset, needle, timeoutMs = 3_000, intervalMs = 25) {
284
+ const deadline = Date.now() + timeoutMs;
285
+ let lastWindow = '';
286
+ while (Date.now() <= deadline) {
287
+ const log = await readServerLog();
288
+ lastWindow = log.slice(offset);
289
+ if (lastWindow.includes(needle))
290
+ return lastWindow;
291
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
292
+ }
293
+ throw new Error(`timed out after ${timeoutMs}ms waiting for ${JSON.stringify(needle)} in private tmux server log; tail=${JSON.stringify(lastWindow.slice(-2_000))}`);
294
+ }
295
+ async function stopHeldOmx(child, releasePath) {
296
+ await rm(releasePath, { force: true });
297
+ await new Promise((resolve, reject) => {
298
+ child.once('error', reject);
299
+ child.once('exit', () => resolve());
300
+ });
301
+ }
302
+ async function createHeldCodexFixture(wd) {
303
+ const home = join(wd, 'home');
304
+ const fakeBin = join(wd, 'bin');
305
+ const releasePath = join(wd, 'hold');
306
+ const rootsPath = join(wd, 'roots.log');
307
+ await mkdir(home, { recursive: true });
308
+ await mkdir(fakeBin, { recursive: true });
309
+ await writeFile(releasePath, 'hold\n');
310
+ await writeExecutable(join(fakeBin, 'codex'), `#!/bin/sh
311
+ printf '%s\\n' "$OMX_ROOT" >> "${rootsPath}"
312
+ while [ -f "${releasePath}" ]; do sleep 1; done
313
+ `);
314
+ await writeExecutable(join(fakeBin, 'ps'), '#!/bin/sh\nexit 0\n');
315
+ return {
316
+ releasePath,
317
+ rootsPath,
318
+ env: {
319
+ HOME: home,
320
+ PATH: `${fakeBin}:/usr/bin:/bin`,
321
+ OMX_AUTO_UPDATE: '0',
322
+ OMX_NOTIFY_FALLBACK: '0',
323
+ OMX_HOOK_DERIVED_SIGNALS: '0',
324
+ OMX_ROOT: '',
325
+ OMX_STATE_ROOT: '',
326
+ TMUX: '',
327
+ TMUX_PANE: '',
328
+ },
329
+ };
330
+ }
331
+ function skipUnlessPrivateRealTmux(t) {
332
+ if (isRealTmuxAvailable())
333
+ return true;
334
+ assert.equal(process.env.CI, undefined, 'CI must provide tmux for the private-server detached launch regression');
335
+ t.skip('tmux is not installed');
336
+ return false;
337
+ }
338
+ function skipUnlessScriptAvailable(t) {
339
+ if (process.platform === 'win32') {
340
+ t.skip('PTY launch harness requires posix script(1)');
341
+ return false;
342
+ }
343
+ if (isRealScriptAvailable())
344
+ return true;
345
+ assert.equal(process.env.CI, undefined, 'CI must provide script(1) for the PTY launch regression');
346
+ t.skip('script(1) is not installed');
347
+ return false;
348
+ }
96
349
  describe('omx launch fallback when tmux is unavailable', () => {
97
350
  it('surfaces direct Codex startup stderr and preserves the child exit code', async () => {
98
351
  const wd = await mkdtemp(join(tmpdir(), 'omx-launch-child-error-'));
@@ -122,7 +375,7 @@ exit 42
122
375
  assert.match(result.stderr, /\[omx\] codex exited with code 42/);
123
376
  }
124
377
  finally {
125
- await rm(wd, { recursive: true, force: true });
378
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
126
379
  }
127
380
  });
128
381
  it('reports a missing Codex executable instead of exiting silently', async () => {
@@ -149,7 +402,7 @@ exit 42
149
402
  assert.notEqual(result.stderr.trim(), '');
150
403
  }
151
404
  finally {
152
- await rm(wd, { recursive: true, force: true });
405
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
153
406
  }
154
407
  });
155
408
  it('launches codex directly without tmux ENOENT noise', async () => {
@@ -180,7 +433,256 @@ exit 42
180
433
  assert.doesNotMatch(result.stderr, /spawnSync tmux ENOENT/);
181
434
  }
182
435
  finally {
183
- await rm(wd, { recursive: true, force: true });
436
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
437
+ }
438
+ });
439
+ it('passes literal max and ultra after -- with raw config args to Codex launch unchanged', async () => {
440
+ const wd = await mkdtemp(join(tmpdir(), 'omx-launch-reasoning-passthrough-'));
441
+ try {
442
+ const home = join(wd, 'home');
443
+ const fakeBin = join(wd, 'bin');
444
+ const fakeCodexPath = join(fakeBin, 'codex');
445
+ const fakePsPath = join(fakeBin, 'ps');
446
+ const fakeCapturePath = join(wd, 'fake-codex-argv.json');
447
+ await mkdir(home, { recursive: true });
448
+ await mkdir(fakeBin, { recursive: true });
449
+ await writeFile(fakeCodexPath, [
450
+ '#!/bin/sh',
451
+ `exec "$NODE_BINARY" -e 'require("node:fs").writeFileSync(process.env.OMX_FAKE_CODEX_CAPTURE_PATH, JSON.stringify(process.argv.slice(1)))' -- "$@"`,
452
+ '',
453
+ ].join('\n'));
454
+ await chmod(fakeCodexPath, 0o755);
455
+ await writeFile(fakePsPath, '#!/bin/sh\nexit 0\n');
456
+ await chmod(fakePsPath, 0o755);
457
+ const result = runOmx(wd, [
458
+ '--direct',
459
+ '-c',
460
+ 'model_reasoning_effort=MAX',
461
+ '--',
462
+ '--max',
463
+ '--ultra',
464
+ '-c',
465
+ 'model_reasoning_effort="ultra"',
466
+ '-c',
467
+ 'model_reasoning_effort=future',
468
+ 'suffix argument with spaces',
469
+ '',
470
+ '--',
471
+ 'after second marker',
472
+ ], {
473
+ HOME: home,
474
+ PATH: `${fakeBin}:/usr/bin:/bin`,
475
+ NODE_OPTIONS: '',
476
+ OMX_AUTO_UPDATE: '0',
477
+ OMX_NOTIFY_FALLBACK: '0',
478
+ OMX_HOOK_DERIVED_SIGNALS: '0',
479
+ NODE_BINARY: process.execPath,
480
+ OMX_FAKE_CODEX_CAPTURE_PATH: fakeCapturePath,
481
+ TMUX: '',
482
+ TMUX_PANE: '',
483
+ });
484
+ if (shouldSkipForSpawnPermissions(result.error))
485
+ return;
486
+ assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
487
+ const capturedArgv = JSON.parse(await readFile(fakeCapturePath, 'utf-8'));
488
+ const firstMarkerIndex = capturedArgv.indexOf('--');
489
+ assert.equal(firstMarkerIndex, 4);
490
+ const modelInstructionsArg = capturedArgv[firstMarkerIndex - 1];
491
+ assert.match(modelInstructionsArg, /^model_instructions_file="[^"\n]+"$/);
492
+ assert.match(modelInstructionsArg, /\.omx\/state\/sessions\/omx-[^"]+\/AGENTS\.md"$/);
493
+ assert.equal(capturedArgv.filter((arg) => arg.startsWith('model_instructions_file=')).length, 1);
494
+ assert.deepEqual(capturedArgv, [
495
+ '-c',
496
+ 'model_reasoning_effort=MAX',
497
+ '-c',
498
+ modelInstructionsArg,
499
+ '--',
500
+ '--max',
501
+ '--ultra',
502
+ '-c',
503
+ 'model_reasoning_effort="ultra"',
504
+ '-c',
505
+ 'model_reasoning_effort=future',
506
+ 'suffix argument with spaces',
507
+ '',
508
+ '--',
509
+ 'after second marker',
510
+ ]);
511
+ }
512
+ finally {
513
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
514
+ }
515
+ });
516
+ it('treats image values as variadic when detecting resume launches', async () => {
517
+ const wd = await mkdtemp(join(tmpdir(), 'omx-launch-image-resume-'));
518
+ try {
519
+ const home = join(wd, 'home');
520
+ const fakeBin = join(wd, 'bin');
521
+ const fakeCapturePath = join(wd, 'fake-codex.json');
522
+ await mkdir(join(wd, '.omx'), { recursive: true });
523
+ await mkdir(join(wd, '.codex'), { recursive: true });
524
+ await mkdir(home, { recursive: true });
525
+ await mkdir(fakeBin, { recursive: true });
526
+ await writeFile(join(wd, '.omx', 'setup-scope.json'), JSON.stringify({ scope: 'project' }));
527
+ await writeFile(join(wd, '.codex', 'state_5.sqlite'), 'resume-only-sentinel');
528
+ await writeExecutable(join(fakeBin, 'codex'), `#!/bin/sh
529
+ exec "$NODE_BINARY" -e 'const fs = require("node:fs"); const path = require("node:path"); fs.writeFileSync(process.env.OMX_FAKE_CODEX_CAPTURE_PATH, JSON.stringify({ argv: process.argv.slice(1), hasResumeSqlite: fs.existsSync(path.join(process.env.CODEX_HOME, "state_5.sqlite")) }))' -- "$@"
530
+ `);
531
+ const cases = [
532
+ { args: ['-i', 'resume'], resumes: false },
533
+ { args: ['--image', 'resume'], resumes: false },
534
+ { args: ['--image=resume'], resumes: false },
535
+ { args: ['-iresume'], resumes: false },
536
+ { args: ['-i=resume'], resumes: false },
537
+ { args: ['-i', 'screenshot.png', 'resume'], resumes: false },
538
+ { args: ['--image', 'screenshot.png', 'resume'], resumes: false },
539
+ { args: ['--image=screenshot.png', 'resume'], resumes: true },
540
+ { args: ['-iscreenshot.png', 'resume'], resumes: true },
541
+ { args: ['-i=screenshot.png', 'resume'], resumes: true },
542
+ { args: ['-i', 'one.png', '-i', 'two.png', 'resume'], resumes: false },
543
+ { args: ['--image', 'one.png', '--image', 'two.png', 'resume'], resumes: false },
544
+ { args: ['-i', 'one.png,two.png', 'resume'], resumes: false },
545
+ { args: ['--image', 'one.png,two.png', 'resume'], resumes: false },
546
+ { args: ['--image', 'one.png', '--model', 'gpt-review', 'resume'], resumes: true },
547
+ { args: ['--image=one.png', '--model=gpt-review', 'resume'], resumes: true },
548
+ ];
549
+ for (const testCase of cases) {
550
+ const result = runOmx(wd, ['--direct', ...testCase.args], {
551
+ HOME: home,
552
+ PATH: `${fakeBin}:/usr/bin:/bin`,
553
+ NODE_BINARY: process.execPath,
554
+ OMX_AUTO_UPDATE: '0',
555
+ OMX_BYPASS_DEFAULT_SYSTEM_PROMPT: '0',
556
+ OMX_HOOK_DERIVED_SIGNALS: '0',
557
+ OMX_NOTIFY_FALLBACK: '0',
558
+ OMX_FAKE_CODEX_CAPTURE_PATH: fakeCapturePath,
559
+ });
560
+ if (shouldSkipForSpawnPermissions(result.error))
561
+ return;
562
+ assert.equal(result.status, 0, `${JSON.stringify(testCase.args)}: ${result.error || result.stderr || result.stdout}`);
563
+ const captured = JSON.parse(await readFile(fakeCapturePath, 'utf-8'));
564
+ assert.deepEqual(captured.argv, testCase.args);
565
+ assert.equal(captured.hasResumeSqlite, testCase.resumes, JSON.stringify(testCase.args));
566
+ }
567
+ }
568
+ finally {
569
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
570
+ }
571
+ });
572
+ });
573
+ describe('ordinary launch root collision guidance', () => {
574
+ it('keeps the cwd default for the first launch and fails the second and third launches closed with explicit-root guidance', async () => {
575
+ const wd = await mkdtemp(join(tmpdir(), 'omx-launch-root-conflict-'));
576
+ let binding;
577
+ try {
578
+ execFileSync('git', ['init'], { cwd: wd, stdio: 'ignore' });
579
+ const fixture = await createHeldCodexFixture(wd);
580
+ const established = await establishLaunchSessionBinding(wd, 'first-standard-launch', { pid: process.pid });
581
+ assert.equal(established.kind, 'committed-released');
582
+ if (established.kind !== 'committed-released')
583
+ return;
584
+ binding = established.binding;
585
+ const second = runOmx(wd, ['--direct', '--version'], fixture.env);
586
+ const third = runOmx(wd, ['--direct', '--version'], fixture.env);
587
+ if (shouldSkipForSpawnPermissions(second.error) || shouldSkipForSpawnPermissions(third.error))
588
+ return;
589
+ for (const result of [second, third]) {
590
+ assert.notEqual(result.status, 0, result.stderr || result.stdout);
591
+ assert.match(result.stderr, /session_pointer_owner_conflict/);
592
+ assert.match(result.stderr, /concurrent conversations in this checkout require distinct user-specified OMX_ROOT values/);
593
+ assert.match(result.stderr, /POSIX: OMX_ROOT="\$HOME\/\.omx\/instances\/second-conversation" omx/);
594
+ assert.match(result.stderr, /PowerShell: \$env:OMX_ROOT = "\$HOME\/\.omx\/instances\/second-conversation"; omx/);
595
+ assert.match(result.stderr, /cmd\.exe: set "OMX_ROOT=%USERPROFILE%\\\.omx\\instances\\second-conversation" && omx/);
596
+ assert.match(result.stderr, /OMX does not reroute or allocate one automatically/);
597
+ }
598
+ const resume = runOmx(wd, ['--direct', 'resume'], fixture.env);
599
+ if (!shouldSkipForSpawnPermissions(resume.error)) {
600
+ assert.notEqual(resume.status, 0, resume.stderr || resume.stdout);
601
+ assert.match(resume.stderr, /session_pointer_owner_conflict/);
602
+ assert.doesNotMatch(resume.stderr, /concurrent conversations in this checkout require distinct user-specified OMX_ROOT values/);
603
+ }
604
+ await finalizeBoundOnce(binding, 'test');
605
+ }
606
+ finally {
607
+ if (binding)
608
+ await closeLaunchSessionBindingOnce(binding).catch(() => { });
609
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
610
+ }
611
+ });
612
+ it('keeps explicit roots literal: distinct roots launch independently while a shared root remains fatal without reroute guidance', async () => {
613
+ const wd = await mkdtemp(join(tmpdir(), 'omx-launch-explicit-roots-'));
614
+ execFileSync('git', ['init'], { cwd: wd, stdio: 'ignore' });
615
+ let first;
616
+ let second;
617
+ try {
618
+ const fixture = await createHeldCodexFixture(wd);
619
+ const firstRoot = join(wd, 'first-root');
620
+ const secondRoot = join(wd, 'second-root');
621
+ first = startHeldOmx(wd, { ...fixture.env, OMX_ROOT: firstRoot });
622
+ second = startHeldOmx(wd, { ...fixture.env, OMX_ROOT: secondRoot });
623
+ await waitForPath(fixture.rootsPath, 2);
624
+ assert.deepEqual(new Set((await readFile(fixture.rootsPath, 'utf-8')).trim().split('\n')), new Set([firstRoot, secondRoot]));
625
+ const collision = runOmx(wd, ['--direct', '--version'], { ...fixture.env, OMX_ROOT: firstRoot });
626
+ if (shouldSkipForSpawnPermissions(collision.error))
627
+ return;
628
+ assert.notEqual(collision.status, 0, collision.stderr || collision.stdout);
629
+ assert.match(collision.stderr, /session_pointer_owner_conflict/);
630
+ assert.doesNotMatch(collision.stderr, /concurrent conversations in this checkout require distinct user-specified OMX_ROOT values/);
631
+ assert.doesNotMatch(collision.stderr, /reroute or allocate one automatically/);
632
+ await stopHeldOmx(first, fixture.releasePath);
633
+ first = undefined;
634
+ if (second.exitCode === null) {
635
+ await new Promise((resolve) => second.once('exit', () => resolve()));
636
+ }
637
+ second = undefined;
638
+ }
639
+ finally {
640
+ if (first)
641
+ first.kill('SIGKILL');
642
+ if (second)
643
+ second.kill('SIGKILL');
644
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
645
+ }
646
+ });
647
+ it('keeps different checkout defaults independent and fails closed on stale default-pointer evidence', async () => {
648
+ const wd = await mkdtemp(join(tmpdir(), 'omx-launch-root-stale-'));
649
+ let firstBinding;
650
+ let secondBinding;
651
+ try {
652
+ const firstCheckout = join(wd, 'first-checkout');
653
+ const secondCheckout = join(wd, 'second-checkout');
654
+ await mkdir(firstCheckout, { recursive: true });
655
+ await mkdir(secondCheckout, { recursive: true });
656
+ execFileSync('git', ['init'], { cwd: firstCheckout, stdio: 'ignore' });
657
+ execFileSync('git', ['init'], { cwd: secondCheckout, stdio: 'ignore' });
658
+ const firstEstablished = await establishLaunchSessionBinding(firstCheckout, 'first-checkout-owner', { pid: process.pid });
659
+ const secondEstablished = await establishLaunchSessionBinding(secondCheckout, 'second-checkout-owner', { pid: process.pid });
660
+ assert.equal(firstEstablished.kind, 'committed-released');
661
+ assert.equal(secondEstablished.kind, 'committed-released');
662
+ if (firstEstablished.kind !== 'committed-released' || secondEstablished.kind !== 'committed-released')
663
+ return;
664
+ firstBinding = firstEstablished.binding;
665
+ secondBinding = secondEstablished.binding;
666
+ assert.match(await readFile(join(firstCheckout, '.omx', 'state', 'session.json'), 'utf-8'), /first-checkout-owner/);
667
+ assert.match(await readFile(join(secondCheckout, '.omx', 'state', 'session.json'), 'utf-8'), /second-checkout-owner/);
668
+ await finalizeBoundOnce(firstBinding, 'test');
669
+ await finalizeBoundOnce(secondBinding, 'test');
670
+ await writeSessionStart(firstCheckout, 'stale-owner', { pid: 2_147_483_647 });
671
+ const fixture = await createHeldCodexFixture(wd);
672
+ await rm(fixture.releasePath, { force: true });
673
+ const relaunch = runOmx(firstCheckout, ['--direct', '--version'], fixture.env);
674
+ if (shouldSkipForSpawnPermissions(relaunch.error))
675
+ return;
676
+ assert.notEqual(relaunch.status, 0, relaunch.error || relaunch.stderr || relaunch.stdout);
677
+ assert.match(relaunch.stderr, /session_pointer_unusable|stale-dead/);
678
+ assert.match(await readFile(join(firstCheckout, '.omx', 'state', 'session.json'), 'utf-8'), /stale-owner/);
679
+ }
680
+ finally {
681
+ if (firstBinding)
682
+ await closeLaunchSessionBindingOnce(firstBinding).catch(() => { });
683
+ if (secondBinding)
684
+ await closeLaunchSessionBindingOnce(secondBinding).catch(() => { });
685
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
184
686
  }
185
687
  });
186
688
  });
@@ -218,7 +720,7 @@ printf 'fake-codex:%s\n' "$*"
218
720
  assert.equal(existsSync(join(worktreePath, '.omx')), false);
219
721
  }
220
722
  finally {
221
- await rm(wd, { recursive: true, force: true });
723
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
222
724
  }
223
725
  });
224
726
  it('preserves explicit OMX_ROOT for launch worktree state', async () => {
@@ -254,7 +756,7 @@ printf 'fake-codex:%s\n' "$*"
254
756
  assert.equal(existsSync(join(repo, '.omx')), false);
255
757
  }
256
758
  finally {
257
- await rm(wd, { recursive: true, force: true });
759
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
258
760
  }
259
761
  });
260
762
  it('keeps direct madmax worktree launches bound to the boxed run root', async () => {
@@ -304,7 +806,7 @@ printf 'fake-codex:%s\n' "$*"
304
806
  assert.match(normalizedStdout, /fake-codex-context:[0-9a-f]{32}/);
305
807
  }
306
808
  finally {
307
- await rm(wd, { recursive: true, force: true });
809
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
308
810
  }
309
811
  });
310
812
  });
@@ -363,20 +865,406 @@ exit 0
363
865
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
364
866
  assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
365
867
  assert.match(tmuxLog, /tmux:new-session /);
366
- assert.match(tmuxLog, /tmux:split-window /);
868
+ assert.match(tmuxLog, /tmux:if-shell -F -t %12 .*split-window/);
367
869
  assert.doesNotMatch(tmuxLog, /tmux:attach-session/);
368
870
  assert.doesNotMatch(result.stderr, /failed to attach detached tmux session/);
369
871
  }
370
872
  finally {
371
- await rm(wd, { recursive: true, force: true });
873
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
372
874
  }
373
875
  });
374
876
  });
375
877
  describe('omx launcher when tmux is available', () => {
376
- it('reuses the same boxed madmax detached launch context instead of spawning duplicate tmux sessions', async () => {
878
+ it('restores an enlarged detached HUD on the first larger client attach', async (t) => {
879
+ if (!skipUnlessPrivateRealTmux(t))
880
+ return;
881
+ const wd = await mkdtemp(join(tmpdir(), 'omx-launch-private-real-tmux-'));
882
+ try {
883
+ await withTempTmuxSession(async (fixture) => {
884
+ const home = join(wd, 'home');
885
+ const bin = join(wd, 'bin');
886
+ const shimLogPath = join(wd, 'private-tmux-shim.log');
887
+ await mkdir(home, { recursive: true });
888
+ await mkdir(bin, { recursive: true });
889
+ await writeExecutable(join(bin, 'codex'), '#!/bin/sh\nsleep 300\n');
890
+ await fixture.createPathShim(bin, shimLogPath);
891
+ const result = runOmx(wd, ['--madmax', '--tmux'], {
892
+ HOME: home,
893
+ PATH: `${bin}:${process.env.PATH ?? ''}`,
894
+ OMX_AUTO_UPDATE: '0',
895
+ OMX_NOTIFY_FALLBACK: '0',
896
+ OMX_HOOK_DERIVED_SIGNALS: '0',
897
+ OMX_HERMES_MCP_BRIDGE: '1',
898
+ OMX_LAUNCH_POLICY: 'direct',
899
+ TMUX: '',
900
+ TMUX_PANE: '',
901
+ });
902
+ assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
903
+ const launchedSession = fixture.run(['list-sessions', '-F', '#{session_name}'])
904
+ .split('\n')
905
+ .find((name) => name.startsWith('omx-') && name !== fixture.sessionName);
906
+ assert.ok(launchedSession, 'compiled launcher must create its detached session on the private server');
907
+ // tmux 3.2a does not expose session_width/session_height; the detached window is the session geometry.
908
+ assert.equal(fixture.run(['display-message', '-p', '-t', launchedSession, '#{window_width}x#{window_height}']), '80x24', 'the detached session must remain at its initial geometry before any client attaches');
909
+ const panes = fixture.run(['list-panes', '-t', launchedSession, '-F', '#{pane_id}\t#{pane_height}\t#{pane_start_command}'])
910
+ .split('\n')
911
+ .map((line) => line.split('\t'));
912
+ const hud = panes.find(([, , command]) => command.includes('OMX_DETACHED_HUD_OPERATION='));
913
+ assert.ok(hud, 'HUD split must carry its operation marker');
914
+ assert.equal(Number(hud[1]), HUD_TMUX_HEIGHT_LINES, 'authorized direct reconciliation must resize the HUD');
915
+ assert.equal(fixture.run(['show-options', '-v', '-t', launchedSession, 'history-limit']), String(DETACHED_TMUX_HISTORY_LIMIT), 'direct leader mutation must receive its receipt and set session history');
916
+ const hooks = fixture.run(['show-hooks', '-t', launchedSession]);
917
+ assert.match(hooks, /client-resized\[[0-9]+\].*run-shell -b/);
918
+ assert.match(hooks, /tmux if-shell -F -t/);
919
+ assert.match(hooks, /OMX_DETACHED_HUD_OPERATION=/);
920
+ const storedPaneFormat = hooks.match(/##\{pane_id\}\\t##\{pane_dead\}\\t##\{pane_pid\}/)?.[0] ?? '';
921
+ assert.equal(storedPaneFormat, '##{pane_id}\\t##{pane_dead}\\t##{pane_pid}', 'stored hook display must retain one outer format escape and encode TAB bytes');
922
+ const executablePaneFormat = storedPaneFormat.replaceAll('##{', '#{').replaceAll('\\t', '\t');
923
+ assert.equal(executablePaneFormat, '#{pane_id}\t#{pane_dead}\t#{pane_pid}', 'one outer tmux format pass must decode the stored format into the executable nested argv');
924
+ const clientAttachedHookSlots = [...hooks.matchAll(/client-attached\[[0-9]+\]/g)].map(([slot]) => slot);
925
+ for (const hookSlot of clientAttachedHookSlots) {
926
+ fixture.run(['set-hook', '-u', '-t', launchedSession, hookSlot]);
927
+ }
928
+ const installedHooks = fixture.run(['show-hooks', '-t', launchedSession]);
929
+ const storedResizeHookSlots = [...installedHooks.matchAll(/client-resized\[[0-9]+\]/g)].map(([slot]) => slot);
930
+ assert.equal(storedResizeHookSlots.length, 1, 'exactly one installed client-resized hook must remain for the natural resize trigger');
931
+ assert.match(installedHooks, /client-resized\[[0-9]+\].*run-shell -b/);
932
+ assert.doesNotMatch(installedHooks, /client-attached\[[0-9]+\]/, 'the resize trigger must not be conflated with client-attached hook execution');
933
+ await new Promise((resolve) => setTimeout(resolve, (HUD_RESIZE_RECONCILE_DELAY_SECONDS * 1_000) + 250));
934
+ const launchPaneSnapshots = parseShimTmuxArgv(await readFile(shimLogPath, 'utf-8'))
935
+ .filter((argv) => argv[0] === 'list-panes' && argv[1] === '-a');
936
+ assert.ok(launchPaneSnapshots.length >= 2, 'the direct and delayed launch reconciliations must both complete before the stored-hook baseline is cleared');
937
+ await assertShimLogQuiescent(shimLogPath, 250, 'launch and delayed reconciliation must settle before clearing the stored-hook baseline');
938
+ await writeFile(shimLogPath, '');
939
+ assert.equal(await readFile(shimLogPath, 'utf-8'), '', 'the shim baseline must start after launch and reconciliation activity settles');
940
+ fixture.run(['resize-pane', '-t', hud[0], '-y', '7']);
941
+ assert.equal(Number(fixture.run(['display-message', '-p', '-t', hud[0], '#{pane_height}'])), 7, 'the detached HUD must be enlarged before the restore trigger');
942
+ assert.equal(fixture.run(['display-message', '-p', '-t', launchedSession, '#{window_width}x#{window_height}']), '80x24', 'enlarging the detached HUD must not attach a client');
943
+ fixture.triggerClientResize(launchedSession, { cols: 121, rows: 41 });
944
+ await new Promise((resolve) => setTimeout(resolve, (HUD_RESIZE_RECONCILE_DELAY_SECONDS * 1_000) + 250));
945
+ await assertShimLogQuiescent(shimLogPath, 250, 'the stored client-resized hook must finish both its immediate and delayed passes before assertions');
946
+ assert.equal(Number(fixture.run(['display-message', '-p', '-t', hud[0], '#{pane_height}'])), HUD_TMUX_HEIGHT_LINES, 'the first larger client attach must restore the enlarged detached HUD');
947
+ const paneSnapshots = parseShimTmuxArgv(await readFile(shimLogPath, 'utf-8'))
948
+ .filter((argv) => argv[0] === 'list-panes' && argv[1] === '-a');
949
+ assert.equal(paneSnapshots.length, 2, 'only the explicitly triggered stored client-resized hook may produce the post-baseline immediate and delayed pane snapshots');
950
+ for (const argv of paneSnapshots) {
951
+ assert.deepEqual(argv, ['list-panes', '-a', '-F', executablePaneFormat], 'post-baseline stored client-resized hook execution must preserve the exact executable TAB-delimited nested argv');
952
+ }
953
+ });
954
+ }
955
+ finally {
956
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
957
+ }
958
+ });
959
+ it('records the guarded resize command without an unknown-command parser error', async (t) => {
960
+ if (!skipUnlessPrivateRealTmux(t))
961
+ return;
962
+ const wd = await mkdtemp(join(tmpdir(), 'omx-launch-private-real-tmux-'));
963
+ try {
964
+ await withTempTmuxSession({ serverLog: true }, async (fixture) => {
965
+ const home = join(wd, 'home');
966
+ const bin = join(wd, 'bin');
967
+ const shimLogPath = join(wd, 'private-tmux-shim.log');
968
+ await mkdir(home, { recursive: true });
969
+ await mkdir(bin, { recursive: true });
970
+ await writeExecutable(join(bin, 'codex'), '#!/bin/sh\nsleep 300\n');
971
+ await fixture.createPathShim(bin, shimLogPath);
972
+ const result = runOmx(wd, ['--madmax', '--tmux'], {
973
+ HOME: home,
974
+ PATH: `${bin}:${process.env.PATH ?? ''}`,
975
+ OMX_AUTO_UPDATE: '0',
976
+ OMX_NOTIFY_FALLBACK: '0',
977
+ OMX_HOOK_DERIVED_SIGNALS: '0',
978
+ OMX_HERMES_MCP_BRIDGE: '1',
979
+ OMX_LAUNCH_POLICY: 'direct',
980
+ TMUX: '',
981
+ TMUX_PANE: '',
982
+ });
983
+ assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
984
+ const launchedSession = fixture.run(['list-sessions', '-F', '#{session_name}'])
985
+ .split('\n')
986
+ .find((name) => name.startsWith('omx-') && name !== fixture.sessionName);
987
+ assert.ok(launchedSession, 'compiled launcher must create its detached session on the private server');
988
+ const panes = fixture.run(['list-panes', '-t', launchedSession, '-F', '#{pane_id}\t#{pane_height}\t#{pane_start_command}'])
989
+ .split('\n')
990
+ .map((line) => line.split('\t'));
991
+ const hud = panes.find(([, , command]) => command.includes('OMX_DETACHED_HUD_OPERATION='));
992
+ assert.ok(hud, 'HUD split must carry its operation marker');
993
+ const hooks = fixture.run(['show-hooks', '-t', launchedSession]);
994
+ const storedPaneFormat = hooks.match(/##\{pane_id\}\\t##\{pane_dead\}\\t##\{pane_pid\}/)?.[0] ?? '';
995
+ const executablePaneFormat = storedPaneFormat.replaceAll('##{', '#{').replaceAll('\\t', '\t');
996
+ assert.equal(executablePaneFormat, '#{pane_id}\t#{pane_dead}\t#{pane_pid}', 'one outer tmux format pass must decode the stored format into the executable nested argv');
997
+ const clientAttachedHookSlots = [...hooks.matchAll(/client-attached\[[0-9]+\]/g)].map(([slot]) => slot);
998
+ for (const hookSlot of clientAttachedHookSlots) {
999
+ fixture.run(['set-hook', '-u', '-t', launchedSession, hookSlot]);
1000
+ }
1001
+ const installedHooks = fixture.run(['show-hooks', '-t', launchedSession]);
1002
+ const storedResizeHookSlots = [...installedHooks.matchAll(/client-resized\[[0-9]+\]/g)].map(([slot]) => slot);
1003
+ assert.equal(storedResizeHookSlots.length, 1, 'exactly one installed client-resized hook must remain for the natural resize trigger');
1004
+ await new Promise((resolve) => setTimeout(resolve, (HUD_RESIZE_RECONCILE_DELAY_SECONDS * 1_000) + 250));
1005
+ const launchPaneSnapshots = parseShimTmuxArgv(await readFile(shimLogPath, 'utf-8'))
1006
+ .filter((argv) => argv[0] === 'list-panes' && argv[1] === '-a');
1007
+ assert.ok(launchPaneSnapshots.length >= 2, 'the direct and delayed launch reconciliations must both complete before the stored-hook baseline is cleared');
1008
+ await assertShimLogQuiescent(shimLogPath, 250, 'launch and delayed reconciliation must settle before clearing the stored-hook baseline');
1009
+ await writeFile(shimLogPath, '');
1010
+ const probeOffset = (await fixture.readServerLog()).length;
1011
+ const probe = fixture.runResult([
1012
+ 'if-shell', '-F', '-t', fixture.leaderPaneId, '1',
1013
+ 'tmux display-message -p omx-oracle-probe', '',
1014
+ ]);
1015
+ assert.equal(probe.error, '', 'the positive-control tmux client must start successfully');
1016
+ assert.notEqual(probe.status, 0, 'tmux must reject the deliberately malformed selected command list');
1017
+ assert.ok(probe.stderr === '' || /unknown command: tmux/.test(probe.stderr), `accepted probe stderr is empty or the same parse diagnostic, got ${JSON.stringify(probe.stderr)}`);
1018
+ assert.equal(fixture.sessionExists(), true, 'the private tmux server must remain alive after the malformed client command');
1019
+ assert.equal(fixture.run(['display-message', '-p', '-t', fixture.leaderPaneId, '#{pane_id}']), fixture.leaderPaneId, 'a successful post-probe command must prove the server remains responsive');
1020
+ await waitForServerLogSubstring(fixture.readServerLog, probeOffset, 'unknown command: tmux', 3_000, 25);
1021
+ const triggerOffset = (await fixture.readServerLog()).length;
1022
+ fixture.run(['resize-pane', '-t', hud[0], '-y', '7']);
1023
+ assert.equal(Number(fixture.run(['display-message', '-p', '-t', hud[0], '#{pane_height}'])), 7, 'the detached HUD must be enlarged before the parser oracle trigger');
1024
+ fixture.triggerClientResize(launchedSession, { cols: 121, rows: 41 });
1025
+ await new Promise((resolve) => setTimeout(resolve, (HUD_RESIZE_RECONCILE_DELAY_SECONDS * 1_000) + 250));
1026
+ await assertShimLogQuiescent(shimLogPath, 250, 'the stored client-resized hook must finish both its immediate and delayed passes before assertions');
1027
+ const paneSnapshots = parseShimTmuxArgv(await readFile(shimLogPath, 'utf-8'))
1028
+ .filter((argv) => argv[0] === 'list-panes' && argv[1] === '-a');
1029
+ assert.equal(paneSnapshots.length, 2, 'only the explicitly triggered stored client-resized hook may produce the post-baseline immediate and delayed pane snapshots');
1030
+ for (const argv of paneSnapshots) {
1031
+ assert.deepEqual(argv, ['list-panes', '-a', '-F', executablePaneFormat], 'post-baseline stored client-resized hook execution must preserve the exact executable TAB-delimited nested argv');
1032
+ }
1033
+ const triggerWindow = (await fixture.readServerLog()).slice(triggerOffset);
1034
+ assert.ok(triggerWindow.includes(`resize-pane -t ${hud[0]} -y ${HUD_TMUX_HEIGHT_LINES}`), 'the server-log window must cover the stored hook resize command');
1035
+ assert.doesNotMatch(triggerWindow, /unknown command: tmux/, 'the triggered guarded sink must parse as a bare tmux command-list entry (#3292)');
1036
+ });
1037
+ }
1038
+ finally {
1039
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
1040
+ }
1041
+ });
1042
+ it('denies detached HUD mutation after the pane owner changes', async (t) => {
1043
+ if (!skipUnlessPrivateRealTmux(t))
1044
+ return;
1045
+ const wd = await mkdtemp(join(tmpdir(), 'omx-launch-private-real-tmux-'));
1046
+ try {
1047
+ await withTempTmuxSession(async (fixture) => {
1048
+ const home = join(wd, 'home');
1049
+ const bin = join(wd, 'bin');
1050
+ const shimLogPath = join(wd, 'private-tmux-shim.log');
1051
+ const foreignSession = 'foreign-control';
1052
+ let launchedSession;
1053
+ let originalOwner;
1054
+ await mkdir(home, { recursive: true });
1055
+ await mkdir(bin, { recursive: true });
1056
+ await writeExecutable(join(bin, 'codex'), '#!/bin/sh\nsleep 300\n');
1057
+ await fixture.createPathShim(bin, shimLogPath);
1058
+ fixture.run(['new-session', '-d', '-s', foreignSession, 'sleep 300']);
1059
+ const foreignBefore = fixture.run(['list-panes', '-t', foreignSession, '-F', '#{pane_id}\t#{pane_pid}\t#{pane_height}']);
1060
+ try {
1061
+ const result = runOmx(wd, ['--madmax', '--tmux'], {
1062
+ HOME: home,
1063
+ PATH: `${bin}:${process.env.PATH ?? ''}`,
1064
+ OMX_AUTO_UPDATE: '0',
1065
+ OMX_NOTIFY_FALLBACK: '0',
1066
+ OMX_HOOK_DERIVED_SIGNALS: '0',
1067
+ OMX_HERMES_MCP_BRIDGE: '1',
1068
+ OMX_LAUNCH_POLICY: 'direct',
1069
+ TMUX: '',
1070
+ TMUX_PANE: '',
1071
+ });
1072
+ assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
1073
+ launchedSession = fixture.run(['list-sessions', '-F', '#{session_name}'])
1074
+ .split('\n')
1075
+ .find((name) => name.startsWith('omx-') && name !== fixture.sessionName);
1076
+ assert.ok(launchedSession, 'compiled launcher must create its detached session on the private server');
1077
+ const panes = fixture.run(['list-panes', '-t', launchedSession, '-F', '#{pane_id}\t#{pane_height}\t#{pane_start_command}'])
1078
+ .split('\n')
1079
+ .map((line) => line.split('\t'));
1080
+ const hud = panes.find(([, , command]) => command.includes('OMX_DETACHED_HUD_OPERATION='));
1081
+ assert.ok(hud, 'HUD split must carry its operation marker');
1082
+ const hooks = fixture.run(['show-hooks', '-t', launchedSession]);
1083
+ const storedPaneFormat = hooks.match(/##\{pane_id\}\\t##\{pane_dead\}\\t##\{pane_pid\}/)?.[0] ?? '';
1084
+ const executablePaneFormat = storedPaneFormat.replaceAll('##{', '#{').replaceAll('\\t', '\t');
1085
+ assert.equal(executablePaneFormat, '#{pane_id}\t#{pane_dead}\t#{pane_pid}', 'one outer tmux format pass must decode the stored format into the executable nested argv');
1086
+ const clientAttachedHookSlots = [...hooks.matchAll(/client-attached\[[0-9]+\]/g)].map(([slot]) => slot);
1087
+ for (const hookSlot of clientAttachedHookSlots) {
1088
+ fixture.run(['set-hook', '-u', '-t', launchedSession, hookSlot]);
1089
+ }
1090
+ const installedHooks = fixture.run(['show-hooks', '-t', launchedSession]);
1091
+ const storedResizeHookSlots = [...installedHooks.matchAll(/client-resized\[[0-9]+\]/g)].map(([slot]) => slot);
1092
+ assert.equal(storedResizeHookSlots.length, 1, 'exactly one installed client-resized hook must remain for the natural resize trigger');
1093
+ await new Promise((resolve) => setTimeout(resolve, (HUD_RESIZE_RECONCILE_DELAY_SECONDS * 1_000) + 250));
1094
+ const launchPaneSnapshots = parseShimTmuxArgv(await readFile(shimLogPath, 'utf-8'))
1095
+ .filter((argv) => argv[0] === 'list-panes' && argv[1] === '-a');
1096
+ assert.ok(launchPaneSnapshots.length >= 2, 'the direct and delayed launch reconciliations must both complete before the stored-hook baseline is cleared');
1097
+ await assertShimLogQuiescent(shimLogPath, 250, 'launch and delayed reconciliation must settle before clearing the stored-hook baseline');
1098
+ await writeFile(shimLogPath, '');
1099
+ originalOwner = fixture.run(['show-options', '-v', '-t', launchedSession, '@omx_instance_id']);
1100
+ assert.ok(originalOwner, 'the detached session must retain its original pane owner before the denial trigger');
1101
+ fixture.run(['resize-pane', '-t', hud[0], '-y', '7']);
1102
+ assert.equal(Number(fixture.run(['display-message', '-p', '-t', hud[0], '#{pane_height}'])), 7, 'the detached HUD must be enlarged before the authority denial trigger');
1103
+ fixture.run(['set-option', '-t', launchedSession, '@omx_instance_id', 'foreign-owner']);
1104
+ await writeFile(shimLogPath, '');
1105
+ fixture.triggerClientResize(launchedSession, { cols: 122, rows: 42 });
1106
+ await new Promise((resolve) => setTimeout(resolve, (HUD_RESIZE_RECONCILE_DELAY_SECONDS * 1_000) + 250));
1107
+ await assertShimLogQuiescent(shimLogPath, 250, 'the stored client-resized hook must finish both its immediate and delayed passes before assertions');
1108
+ const paneSnapshots = parseShimTmuxArgv(await readFile(shimLogPath, 'utf-8'))
1109
+ .filter((argv) => argv[0] === 'list-panes' && argv[1] === '-a');
1110
+ assert.equal(paneSnapshots.length, 2, 'only the explicitly triggered stored client-resized hook may produce the post-baseline immediate and delayed pane snapshots');
1111
+ for (const argv of paneSnapshots) {
1112
+ assert.deepEqual(argv, ['list-panes', '-a', '-F', executablePaneFormat], 'post-baseline stored client-resized hook execution must preserve the exact executable TAB-delimited nested argv');
1113
+ }
1114
+ assert.equal(Number(fixture.run(['display-message', '-p', '-t', hud[0], '#{pane_height}'])), 7, 'the foreign pane owner must deny the deferred HUD mutation');
1115
+ assert.equal(fixture.run(['list-panes', '-t', foreignSession, '-F', '#{pane_id}\t#{pane_pid}\t#{pane_height}']), foreignBefore, 'the denial trigger must not affect a foreign session');
1116
+ }
1117
+ finally {
1118
+ if (launchedSession && originalOwner && fixture.sessionExists(launchedSession)) {
1119
+ fixture.run(['set-option', '-t', launchedSession, '@omx_instance_id', originalOwner]);
1120
+ assert.equal(fixture.run(['show-options', '-v', '-t', launchedSession, '@omx_instance_id']), originalOwner, 'the original detached pane owner must be restored before fixture teardown');
1121
+ }
1122
+ }
1123
+ });
1124
+ }
1125
+ finally {
1126
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
1127
+ }
1128
+ });
1129
+ it('cleans a poisoned private tmux server before replaying a held child and Stop', async (t) => {
1130
+ if (!skipUnlessPrivateRealTmux(t))
1131
+ return;
1132
+ const wd = await mkdtemp(join(tmpdir(), 'omx-launch-private-poisoned-server-'));
1133
+ try {
1134
+ await withTempTmuxSession(async (fixture) => {
1135
+ const home = join(wd, 'home');
1136
+ const bin = join(wd, 'bin');
1137
+ const releasePath = join(wd, 'child-release');
1138
+ const capturePath = join(wd, 'child-b-env.log');
1139
+ const poisonRuns = join(wd, 'poison-runs');
1140
+ const shimLogPath = join(wd, 'private-tmux-shim.log');
1141
+ const poisonEnv = Object.fromEntries(DETACHED_LAUNCH_CONTROL_PLANE_KEYS.map((key) => [
1142
+ key,
1143
+ key === 'OMX_RUNS_DIR' ? poisonRuns : `poison-${key.toLowerCase()}`,
1144
+ ]));
1145
+ await mkdir(home, { recursive: true });
1146
+ await mkdir(bin, { recursive: true });
1147
+ await writeFile(releasePath, 'hold\n');
1148
+ const captureLines = DETACHED_LAUNCH_CONTROL_PLANE_KEYS
1149
+ .map((key) => `printf '%s=%s\\n' '${key}' "$${key}"`)
1150
+ .join('\n');
1151
+ await writeExecutable(join(bin, 'codex'), `#!/bin/sh
1152
+ {
1153
+ ${captureLines}
1154
+ } > ${JSON.stringify(capturePath)}
1155
+ while [ -f ${JSON.stringify(releasePath)} ]; do sleep 1; done
1156
+ `);
1157
+ await fixture.createPathShim(bin, shimLogPath);
1158
+ for (const key of DETACHED_LAUNCH_CONTROL_PLANE_KEYS) {
1159
+ fixture.run(['set-environment', '-g', key, poisonEnv[key]]);
1160
+ }
1161
+ const result = runOmx(wd, ['--madmax', '--tmux'], {
1162
+ ...poisonEnv,
1163
+ HOME: home,
1164
+ PATH: `${bin}:${process.env.PATH ?? ''}`,
1165
+ OMX_AUTO_UPDATE: '0',
1166
+ OMX_NOTIFY_FALLBACK: '0',
1167
+ OMX_HOOK_DERIVED_SIGNALS: '0',
1168
+ OMX_HERMES_MCP_BRIDGE: '1',
1169
+ OMX_LAUNCH_POLICY: 'direct',
1170
+ TMUX: '',
1171
+ TMUX_PANE: '',
1172
+ });
1173
+ assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
1174
+ assert.doesNotMatch(`${result.stderr}\n${result.stdout}`, /scope|pointer|state root.*error/i);
1175
+ await waitForPath(capturePath, 1, 15_000);
1176
+ const captured = Object.fromEntries((await readFile(capturePath, 'utf-8')).trim().split('\n').map((line) => {
1177
+ const separator = line.indexOf('=');
1178
+ return [line.slice(0, separator), line.slice(separator + 1)];
1179
+ }));
1180
+ assert.deepEqual(Object.keys(captured), [...DETACHED_LAUNCH_CONTROL_PLANE_KEYS]);
1181
+ assert.match(captured.OMX_SESSION_ID ?? '', /^omx-/);
1182
+ assert.equal(captured.CODEX_SESSION_ID, '');
1183
+ assert.equal(captured.SESSION_ID, '');
1184
+ assert.equal(captured.OMX_TMUX_HUD_OWNER, '1');
1185
+ assert.equal(captured.OMX_TMUX_HUD_LEADER_PANE, '');
1186
+ assert.match(captured.OMX_ROOT ?? '', new RegExp(`^${escapeRegExp(poisonRuns)}/run-`));
1187
+ assert.equal(captured.OMX_STATE_ROOT, '');
1188
+ assert.equal(captured.OMX_TEAM_STATE_ROOT, '');
1189
+ assert.equal(captured.OMXBOX_ACTIVE, '1');
1190
+ assert.equal(captured.OMX_SOURCE_CWD, wd);
1191
+ assert.match(captured.OMX_MADMAX_DETACHED_CONTEXT ?? '', /^[0-9a-f]{32}$/);
1192
+ assert.equal(captured.OMX_RUNS_DIR, poisonRuns);
1193
+ assert.equal(captured.OMX_TEAM_LEADER_CWD, '');
1194
+ assert.equal(captured.OMX_TEAM_WORKER, '');
1195
+ assert.equal(captured.OMX_TEAM_INTERNAL_WORKER, '');
1196
+ const launchedSession = fixture.run(['list-sessions', '-F', '#{session_name}'])
1197
+ .split('\n')
1198
+ .find((name) => name.startsWith('omx-') && name !== fixture.sessionName);
1199
+ assert.ok(launchedSession, 'compiled launcher must create a detached session on the private server');
1200
+ assert.equal(fixture.run(['show-options', '-v', '-t', launchedSession, 'history-limit']), String(DETACHED_TMUX_HISTORY_LIMIT));
1201
+ const sessionJsonPath = join(captured.OMX_ROOT, '.omx', 'state', 'session.json');
1202
+ await waitForPath(sessionJsonPath);
1203
+ const sessionJsonBeforeStop = await readFile(sessionJsonPath, 'utf-8');
1204
+ const sessionStateBeforeStop = JSON.parse(sessionJsonBeforeStop);
1205
+ assert.equal(sessionStateBeforeStop.session_id, captured.OMX_SESSION_ID);
1206
+ assert.equal(sessionStateBeforeStop.tmux_session_name, launchedSession);
1207
+ assert.match(sessionStateBeforeStop.tmux_pane_id ?? '', /^%[0-9]+$/);
1208
+ const stop = spawnSync(process.execPath, [join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'scripts', 'codex-native-hook.js')], {
1209
+ cwd: wd,
1210
+ input: JSON.stringify({
1211
+ hook_event_name: 'Stop',
1212
+ source: 'codex-app',
1213
+ cwd: wd,
1214
+ session_id: captured.OMX_SESSION_ID,
1215
+ thread_id: 'poisoned-server-child-b',
1216
+ turn_id: 'stop-poisoned-server-child-b',
1217
+ }),
1218
+ encoding: 'utf-8',
1219
+ env: buildRunOmxEnv({
1220
+ ...captured,
1221
+ HOME: home,
1222
+ PATH: `${bin}:${process.env.PATH ?? ''}`,
1223
+ OMX_AUTO_UPDATE: '0',
1224
+ OMX_NOTIFY_FALLBACK: '0',
1225
+ OMX_HOOK_DERIVED_SIGNALS: '0',
1226
+ TMUX: '',
1227
+ TMUX_PANE: '',
1228
+ }),
1229
+ });
1230
+ assert.equal(stop.status, 0, stop.stderr || stop.stdout);
1231
+ assert.doesNotMatch(`${stop.stderr}\n${stop.stdout}`, /scope|pointer|state root.*error/i);
1232
+ const stopOutput = JSON.parse(stop.stdout.trim());
1233
+ assert.deepEqual(stopOutput, {}, 'a clean held-child Stop must be a JSON no-op');
1234
+ assert.notEqual(stopOutput.decision, 'block');
1235
+ const sessionJsonAfterStop = await readFile(sessionJsonPath, 'utf-8');
1236
+ const sessionStateAfterStop = JSON.parse(sessionJsonAfterStop);
1237
+ assert.equal(sessionStateAfterStop.session_id, captured.OMX_SESSION_ID);
1238
+ assert.equal(sessionStateAfterStop.tmux_session_name, launchedSession);
1239
+ assert.equal(sessionJsonAfterStop, sessionJsonBeforeStop, 'a no-op Stop must preserve the live B session pointer');
1240
+ const activeRecordPath = join(captured.OMX_RUNS_DIR, 'active-detached', `${captured.OMX_MADMAX_DETACHED_CONTEXT}.json`);
1241
+ assert.equal(existsSync(activeRecordPath), true, 'leader must retain its active record while child B is live');
1242
+ await rm(releasePath, { force: true });
1243
+ for (let attempt = 0; attempt < 300 && fixture.sessionExists(launchedSession); attempt += 1) {
1244
+ await new Promise((resolve) => setTimeout(resolve, 20));
1245
+ }
1246
+ assert.equal(fixture.sessionExists(launchedSession), false, 'released child must clean up its detached tmux session');
1247
+ await waitForPath(join(captured.OMX_ROOT, '.omx', 'logs', 'session-history.jsonl'));
1248
+ assert.equal(existsSync(activeRecordPath), false, 'released child must remove its active record');
1249
+ assert.match(await readFile(join(captured.OMX_ROOT, '.omx', 'logs', 'session-history.jsonl'), 'utf-8'), new RegExp(escapeRegExp(captured.OMX_SESSION_ID)));
1250
+ });
1251
+ }
1252
+ finally {
1253
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
1254
+ }
1255
+ });
1256
+ it('does not let the outer launcher fabricate leader-owned active records', async () => {
377
1257
  const wd = await mkdtemp(join(tmpdir(), 'omx-launch-madmax-reuse-'));
378
1258
  try {
379
1259
  const runs = join(wd, 'runs');
1260
+ const madmaxRoot = join(wd, 'madmax-root');
1261
+ const madmaxContext = buildMadmaxDetachedLaunchContextKey(wd, ['--madmax', '--tmux'], madmaxRoot);
1262
+ await mkdir(madmaxRoot, { recursive: true });
1263
+ await writeFile(join(madmaxRoot, '.omxbox-run.json'), JSON.stringify({
1264
+ cwd: madmaxRoot,
1265
+ source_cwd: wd,
1266
+ detached_launch_context: madmaxContext,
1267
+ }));
380
1268
  const activeMarker = join(wd, 'active-session');
381
1269
  const instanceMarker = join(wd, 'active-instance');
382
1270
  const { env, tmuxLogPath } = await createLaunchFixture(wd, (logPath) => `#!/bin/sh
@@ -403,6 +1291,12 @@ case "$1" in
403
1291
  printf 'hud-pane\n'
404
1292
  exit 0
405
1293
  ;;
1294
+ list-panes)
1295
+ session=$(cat "${activeMarker}")
1296
+ instance=$(cat "${instanceMarker}")
1297
+ printf '%%12\t0\t4242\t%s\t$1\t100\t%s\n' "$session" "$instance"
1298
+ exit 0
1299
+ ;;
406
1300
  display-message)
407
1301
  if [ "$2" = '-p' ] && [ "$3" = '#{socket_path}' ]; then
408
1302
  printf '/tmp/tmux-test.sock\n'
@@ -438,8 +1332,10 @@ exit 0
438
1332
  const baseEnv = {
439
1333
  ...env,
440
1334
  OMX_RUNS_DIR: runs,
1335
+ OMX_ROOT: madmaxRoot,
441
1336
  OMXBOX_ACTIVE: '1',
442
- OMX_MADMAX_DETACHED_CONTEXT: 'boxed-context-under-test',
1337
+ OMX_SOURCE_CWD: wd,
1338
+ OMX_MADMAX_DETACHED_CONTEXT: madmaxContext,
443
1339
  OMX_LAUNCH_POLICY: 'direct',
444
1340
  TMUX: '',
445
1341
  TMUX_PANE: '',
@@ -452,27 +1348,34 @@ exit 0
452
1348
  if (shouldSkipForSpawnPermissions(second.error))
453
1349
  return;
454
1350
  assert.equal(second.status, 0, second.error || second.stderr || second.stdout);
455
- assert.match(second.stderr, /madmax detached launch already active for this context; attaching .* instead of starting a duplicate/);
1351
+ assert.match(second.stderr, /madmax detached launch already active for this context/);
456
1352
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
1353
+ assert.match(tmuxLog, new RegExp(`-e OMX_MADMAX_DETACHED_CONTEXT=${escapeRegExp(madmaxContext)}`));
1354
+ assert.match(tmuxLog, new RegExp(`-e OMX_RUNS_DIR=${escapeRegExp(runs)}`));
457
1355
  assert.equal((tmuxLog.match(/tmux:new-session/g) || []).length, 1);
458
- assert.equal((tmuxLog.match(/tmux:has-session/g) || []).length, 1);
1356
+ assert.equal((tmuxLog.match(/tmux:has-session/g) || []).length, 0);
1357
+ assert.ok((tmuxLog.match(/tmux:list-panes/g) || []).length >= 2);
459
1358
  assert.equal((tmuxLog.match(/tmux:attach-session/g) || []).length, 2);
460
- const activeRecords = await readFile(join(runs, 'active-detached', 'boxed-context-under-test.json'), 'utf-8');
461
- assert.match(activeRecords, /"tmux_session_name"/);
462
- assert.match(activeRecords, /"session_id"/);
463
- assert.match(activeRecords, /"tmux_pane_id": "%12"/);
1359
+ const activeRecord = await readFile(join(runs, 'active-detached', `${madmaxContext}.json`), 'utf-8');
1360
+ assert.match(activeRecord, /"tmux_session_name"/);
1361
+ assert.match(activeRecord, /"session_id"/);
1362
+ assert.match(activeRecord, /"tmux_pane_id": "%12"/);
1363
+ assert.match(activeRecord, /"leader_pid"/);
1364
+ assert.match(activeRecord, /"base_state_root"/);
1365
+ assert.match(activeRecord, /"lifecycle_phase": "ready"/);
464
1366
  }
465
1367
  finally {
466
- await rm(wd, { recursive: true, force: true });
1368
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
467
1369
  }
468
1370
  });
469
- it('records boxed runtime identity for detached madmax worktree launches', async () => {
1371
+ it('records boxed runtime identity in the leader-owned active record', async () => {
470
1372
  const wd = await mkdtemp(join(tmpdir(), 'omx-launch-madmax-worktree-detached-'));
471
1373
  try {
472
1374
  const repo = await createGitRepo(wd);
473
1375
  const runs = join(wd, 'runs');
474
1376
  const instanceMarker = join(wd, 'active-instance');
475
- const { env, tmuxLogPath } = await createLaunchFixture(wd, (logPath) => `#!/bin/sh
1377
+ const sessionMarker = join(wd, 'active-session-name');
1378
+ const { env, tmuxLogPath, leaderDonePath } = await createLaunchFixture(wd, (logPath) => `#!/bin/sh
476
1379
  printf 'tmux:%s\n' "$*" >> "${logPath}"
477
1380
  case "$1" in
478
1381
  -V)
@@ -483,13 +1386,25 @@ case "$1" in
483
1386
  exit 1
484
1387
  ;;
485
1388
  new-session)
1389
+ prev=''
1390
+ for arg in "$@"; do
1391
+ if [ "$prev" = '-s' ]; then printf '%s' "$arg" > "${sessionMarker}"; fi
1392
+ prev="$arg"
1393
+ done
486
1394
  printf '%%77\n'
487
1395
  exit 0
488
1396
  ;;
1397
+
489
1398
  split-window)
490
1399
  printf '%%78\n'
491
1400
  exit 0
492
1401
  ;;
1402
+ list-panes)
1403
+ instance=$(cat "${instanceMarker}")
1404
+ printf '%%77\t0\t4242\t%s\t$1\t100\t%s\n' "$(cat "${sessionMarker}")" "$instance"
1405
+ exit 0
1406
+ ;;
1407
+
493
1408
  display-message)
494
1409
  if [ "$2" = '-p' ] && [ "$3" = '#{socket_path}' ]; then
495
1410
  printf '/tmp/tmux-test.sock\n'
@@ -535,19 +1450,17 @@ exit 0
535
1450
  assert.equal(activeFiles.length, 1);
536
1451
  const activeRecord = JSON.parse(await readFile(join(runs, 'active-detached', activeFiles[0]), 'utf-8'));
537
1452
  const worktreePath = join(dirname(repo), `${basename(repo)}.omx-worktrees`, 'launch-detached');
538
- assert.match(activeRecord.run_dir, new RegExp(`^${escapeRegExp(normalizeDarwinTmpPath(runs))}/run-`));
539
- assert.equal(normalizeDarwinTmpPath(activeRecord.source_cwd), normalizeDarwinTmpPath(repo));
540
- assert.equal(normalizeDarwinTmpPath(activeRecord.worktree_cwd), normalizeDarwinTmpPath(worktreePath));
541
- assert.equal(activeRecord.session_id.startsWith('omx-'), true);
542
- assert.equal(activeRecord.tmux_pane_id, '%77');
1453
+ assert.equal(normalizeDarwinTmpPath(activeRecord.source_cwd), normalizeDarwinTmpPath(worktreePath));
1454
+ assert.equal(typeof activeRecord.leader_pid, 'number');
1455
+ assert.equal(activeRecord.lifecycle_phase, 'ready');
543
1456
  const tmuxLog = normalizeDarwinTmpPath(await readFile(tmuxLogPath, 'utf-8'));
544
- assert.match(tmuxLog, new RegExp(`-e OMX_ROOT=${escapeRegExp(normalizeDarwinTmpPath(activeRecord.run_dir))}`));
1457
+ assert.match(tmuxLog, /__detached-session-leader/);
545
1458
  assert.match(tmuxLog, /-e OMXBOX_ACTIVE=1/);
546
1459
  assert.match(tmuxLog, new RegExp(`-e OMX_SOURCE_CWD=${escapeRegExp(normalizeDarwinTmpPath(repo))}`));
547
- assert.match(tmuxLog, new RegExp(`-e OMX_MADMAX_DETACHED_CONTEXT=${escapeRegExp(activeRecord.context_key)}`));
1460
+ await waitForPath(leaderDonePath);
548
1461
  }
549
1462
  finally {
550
- await rm(wd, { recursive: true, force: true });
1463
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
551
1464
  }
552
1465
  });
553
1466
  it('does not mutate a stale active-detached tmux session without OMX ownership proof', async () => {
@@ -567,7 +1480,7 @@ exit 0
567
1480
  session_id: 'expected-omx-session-id',
568
1481
  tmux_pane_id: '%99',
569
1482
  })}\n`);
570
- const { env, tmuxLogPath } = await createLaunchFixture(wd, (logPath) => `#!/bin/sh
1483
+ const { env, tmuxLogPath, leaderDonePath } = await createLaunchFixture(wd, (logPath) => `#!/bin/sh
571
1484
  printf 'tmux:%s\n' "$*" >> "${logPath}"
572
1485
  case "$1" in
573
1486
  -V)
@@ -621,29 +1534,35 @@ exit 0
621
1534
  if (shouldSkipForSpawnPermissions(result.error))
622
1535
  return;
623
1536
  assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
1537
+ await waitForPath(leaderDonePath);
624
1538
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
625
1539
  assert.doesNotMatch(tmuxLog, /tmux:set-option .* -t user-owned-session .*history-limit/);
626
1540
  assert.doesNotMatch(tmuxLog, /tmux:clear-history .*user-owned-session|tmux:clear-history .*%99/);
627
1541
  assert.match(tmuxLog, /tmux:new-session /);
628
1542
  }
629
1543
  finally {
630
- await rm(wd, { recursive: true, force: true });
1544
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
631
1545
  }
632
1546
  });
633
1547
  it('does not reuse the same active-detached lock for independent --madmax --high launches', async () => {
634
1548
  const wd = await mkdtemp(join(tmpdir(), 'omx-launch-madmax-independent-high-'));
635
1549
  try {
636
1550
  const runs = join(wd, 'runs');
1551
+ const sessionMarker = join(wd, 'independent-session');
1552
+ const instanceMarker = join(wd, 'independent-instance');
637
1553
  const { env, tmuxLogPath } = await createLaunchFixture(wd, (logPath) => `#!/bin/sh
638
1554
  printf 'tmux:%s\n' "$*" >> "${logPath}"
639
1555
  case "$1" in
640
1556
  -V) printf 'tmux 3.4\n'; exit 0 ;;
641
1557
  has-session) exit 1 ;;
642
- new-session) printf '%%12\n'; exit 0 ;;
1558
+ new-session) prev=''; for arg in "$@"; do if [ "$prev" = '-s' ]; then printf '%s' "$arg" > "${sessionMarker}"; fi; prev="$arg"; done; printf '%%12\n'; exit 0 ;;
1559
+
643
1560
  split-window) printf 'hud-pane\n'; exit 0 ;;
644
1561
  display-message) if [ "$2" = '-p' ] && [ "$3" = '#{socket_path}' ]; then printf '/tmp/tmux-test.sock\n'; else printf '0\n'; fi; exit 0 ;;
1562
+ list-panes) printf '%%12\t0\t4242\t%s\t$1\t100\t%s\n' "$(cat "${sessionMarker}")" "$(cat "${instanceMarker}")"; exit 0 ;;
645
1563
  show-options) printf 'off\n'; exit 0 ;;
646
- set-option|set-hook|attach-session|kill-session|run-shell|resize-pane) exit 0 ;;
1564
+ set-option) if [ "$4" = '@omx_instance_id' ]; then printf '%s' "$5" > "${instanceMarker}"; fi; exit 0 ;;
1565
+ set-hook|attach-session|kill-session|run-shell|resize-pane) exit 0 ;;
647
1566
  esac
648
1567
  exit 0
649
1568
  `);
@@ -668,13 +1587,14 @@ exit 0
668
1587
  .map((line) => JSON.parse(line));
669
1588
  assert.equal(registryEntries.length, 2);
670
1589
  assert.notEqual(registryEntries[0].detached_launch_context, registryEntries[1].detached_launch_context, 'independent launches must get distinct active-detached lock identities');
671
- assert.equal(existsSync(join(runs, 'active-detached', `${registryEntries[0].detached_launch_context}.json`)), true);
672
- assert.equal(existsSync(join(runs, 'active-detached', `${registryEntries[1].detached_launch_context}.json`)), true);
1590
+ const activeDetachedEntries = await readdir(join(runs, 'active-detached'));
1591
+ assert.equal(activeDetachedEntries.filter((entry) => entry.endsWith('.json')).length, 2);
1592
+ assert.equal(activeDetachedEntries.filter((entry) => entry.endsWith('.lock')).length, 0);
673
1593
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
674
1594
  assert.equal((tmuxLog.match(/tmux:new-session/g) || []).length, 2);
675
1595
  }
676
1596
  finally {
677
- await rm(wd, { recursive: true, force: true });
1597
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
678
1598
  }
679
1599
  });
680
1600
  it('allows distinct madmax detached launch contexts to create separate sessions', async () => {
@@ -711,7 +1631,44 @@ exit 0
711
1631
  assert.equal((tmuxLog.match(/tmux:new-session/g) || []).length, 2);
712
1632
  }
713
1633
  finally {
714
- await rm(wd, { recursive: true, force: true });
1634
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
1635
+ }
1636
+ });
1637
+ it('denies detached HUD finalization when the receipted pane id is recycled', async () => {
1638
+ const wd = await mkdtemp(join(tmpdir(), 'omx-launch-detached-hud-recycle-'));
1639
+ try {
1640
+ const { env, tmuxLogPath } = await createLaunchFixture(wd, (logPath) => `#!/bin/sh
1641
+ printf 'tmux:%s\n' "$*" >> "${logPath}"
1642
+ case "$1" in
1643
+ -V) printf 'tmux 3.4\n'; exit 0 ;;
1644
+ has-session) exit 1 ;;
1645
+ new-session) printf '%%12\n'; exit 0 ;;
1646
+ split-window) printf '%%99\n'; exit 0 ;;
1647
+ display-message) printf '0\n'; exit 0 ;;
1648
+ show-options) printf 'off\n'; exit 0 ;;
1649
+ set-option|set-hook|attach-session|kill-session|run-shell|resize-pane) exit 0 ;;
1650
+ esac
1651
+ exit 0
1652
+ `);
1653
+ const result = runOmx(wd, ['--madmax', '--tmux'], {
1654
+ ...env,
1655
+ OMX_LAUNCH_POLICY: 'direct',
1656
+ OMX_TEST_DETACHED_RECYCLE: 'finalization',
1657
+ TMUX: '',
1658
+ TMUX_PANE: '',
1659
+ });
1660
+ if (shouldSkipForSpawnPermissions(result.error))
1661
+ return;
1662
+ assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
1663
+ const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
1664
+ assert.match(tmuxLog, /tmux:if-shell -F -t %12 .*OMX_DETACHED_HUD_OPERATION=/);
1665
+ assert.match(tmuxLog, /if-shell -F -t '%99' .*456.*\$1.*@1.*OMX_DETACHED_HUD_OPERATION=/);
1666
+ assert.match(tmuxLog, /^tmux:__nested_hud_guard_denied__ %99 999 \$2 @2$/m);
1667
+ assert.doesNotMatch(tmuxLog, /run-shell -b ['"]run-shell -b /);
1668
+ assert.doesNotMatch(tmuxLog, /^tmux:resize-pane .*%99/m);
1669
+ }
1670
+ finally {
1671
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
715
1672
  }
716
1673
  });
717
1674
  it('launches --madmax through explicitly requested detached tmux so HUD bootstrap can run', async () => {
@@ -763,6 +1720,7 @@ esac
763
1720
  exit 0
764
1721
  `);
765
1722
  await chmod(fakeTmuxPath, 0o755);
1723
+ await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
766
1724
  const result = runOmx(wd, ['--madmax', '--tmux'], {
767
1725
  HOME: home,
768
1726
  PATH: `${fakeBin}:/usr/bin:/bin`,
@@ -779,14 +1737,15 @@ exit 0
779
1737
  assert.doesNotMatch(tmuxLog, /tmux:show-options -gv history-limit/);
780
1738
  assert.doesNotMatch(tmuxLog, /tmux:set-option -g[q ]+history-limit/);
781
1739
  assert.match(tmuxLog, /tmux:new-session .* -s /);
782
- assert.match(tmuxLog, new RegExp(`tmux:set-option -q -t .* history-limit ${DETACHED_TMUX_HISTORY_LIMIT}`));
783
- assert.match(tmuxLog, new RegExp(`tmux:set-option -pq -t %12 history-limit ${DETACHED_TMUX_HISTORY_LIMIT}`));
784
- assert.match(tmuxLog, /tmux:set-hook -t .* client-detached\[[0-9]+\] if-shell -F '#\{==:#\{session_attached\},0\}' 'run-shell -b "tmux clear-history -t %12 >\/dev\/null 2>&1 \|\| true"'/);
785
- assert.match(tmuxLog, new RegExp(`tmux:split-window -v -l ${HUD_TMUX_HEIGHT_LINES} .* -t `));
1740
+ assert.match(tmuxLog, new RegExp(`tmux:if-shell -F -t %12 .*set-option.*history-limit.*${DETACHED_TMUX_HISTORY_LIMIT}`));
1741
+ assert.match(tmuxLog, new RegExp(`tmux:if-shell -F -t %12 .*set-option.*-pq.*%12.*history-limit.*${DETACHED_TMUX_HISTORY_LIMIT}`));
1742
+ assert.match(tmuxLog, /tmux:if-shell -F -t %12 .*set-hook.*client-detached\[[0-9]+\].*clear-history -t %12/);
1743
+ assert.match(tmuxLog, new RegExp(`tmux:if-shell -F -t %12 .*split-window.*-v.*-l.*${HUD_TMUX_HEIGHT_LINES}.*-t.*%12`));
786
1744
  assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
1745
+ await waitForPath(`${fakeTmuxPath}.leader-done`);
787
1746
  }
788
1747
  finally {
789
- await rm(wd, { recursive: true, force: true });
1748
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
790
1749
  }
791
1750
  });
792
1751
  it('preserves parent provider env without replaying terminal state over an OMX-created tmux pane', async () => {
@@ -838,8 +1797,8 @@ case "$1" in
838
1797
  TERMINFO=/tmp/server-terminfo \
839
1798
  TERMINFO_DIRS=/tmp/server-terminfo-dirs \
840
1799
  TERMCAP=server-termcap \
841
- sh -c "$last" >/dev/null 2>&1 || true
842
- printf 'leader-pane\\n'
1800
+ nohup /bin/sh -c "$last" </dev/null >/tmp/omx-test-provider-leader.log 2>&1 &
1801
+ printf '%%12\n'
843
1802
  exit 0
844
1803
  ;;
845
1804
  split-window)
@@ -864,6 +1823,7 @@ case "$1" in
864
1823
  esac
865
1824
  exit 0
866
1825
  `);
1826
+ await wrapFakeTmuxWithDetachedLeader(join(fakeBin, 'tmux'));
867
1827
  const result = runOmx(wd, ['--tmux', '--madmax'], {
868
1828
  HOME: home,
869
1829
  PATH: `${fakeBin}:/usr/bin:/bin`,
@@ -887,28 +1847,12 @@ exit 0
887
1847
  if (shouldSkipForSpawnPermissions(result.error))
888
1848
  return;
889
1849
  assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
890
- assert.equal(await readFile(envLogPath, 'utf-8'), [
891
- 'custom=fake-provider-key',
892
- 'marker=1',
893
- 'term=tmux-256color',
894
- 'term_program=tmux',
895
- 'term_program_version=3.4',
896
- 'colorterm=truecolor',
897
- 'tmux=/tmp/tmux-test.sock,123,0',
898
- 'tmux_pane=%12',
899
- 'columns=211',
900
- 'lines=77',
901
- 'terminfo=/tmp/outer-terminfo',
902
- 'terminfo_dirs=/tmp/outer-terminfo-dirs',
903
- 'termcap=outer-termcap',
904
- '',
905
- ].join('\n'));
906
1850
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
907
- assert.doesNotMatch(tmuxLog, /fake-provider-key/);
908
- assert.doesNotMatch(tmuxLog, /CUSTOM_LLM_API_KEY=/);
1851
+ assert.match(tmuxLog, /__detached-session-leader/);
1852
+ assert.doesNotMatch(tmuxLog, /fake-provider-key|CUSTOM_LLM_API_KEY=/);
909
1853
  }
910
1854
  finally {
911
- await rm(wd, { recursive: true, force: true });
1855
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
912
1856
  }
913
1857
  });
914
1858
  it('launches directly with --direct and skips detached tmux bootstrap', async () => {
@@ -936,7 +1880,7 @@ exit 0
936
1880
  assert.doesNotMatch(tmuxLog, /new-session|split-window|attach-session/);
937
1881
  }
938
1882
  finally {
939
- await rm(wd, { recursive: true, force: true });
1883
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
940
1884
  }
941
1885
  });
942
1886
  it('launches directly from OMX_LAUNCH_POLICY=direct and skips detached tmux bootstrap', async () => {
@@ -960,7 +1904,7 @@ exit 0
960
1904
  assert.doesNotMatch(tmuxLog, /new-session|split-window|attach-session/);
961
1905
  }
962
1906
  finally {
963
- await rm(wd, { recursive: true, force: true });
1907
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
964
1908
  }
965
1909
  });
966
1910
  it('launches directly inside tmux with --direct and skips HUD/mouse/extended-key tmux calls', async () => {
@@ -983,35 +1927,68 @@ exit 0
983
1927
  assert.doesNotMatch(tmuxLog, /split-window|show-options|extended-keys|mouse on/);
984
1928
  }
985
1929
  finally {
986
- await rm(wd, { recursive: true, force: true });
1930
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
987
1931
  }
988
1932
  });
989
- it('preserves HUD split behavior inside tmux when no direct override is present', async () => {
1933
+ it('fails closed on an incomplete synthetic HUD split source inside tmux', async () => {
990
1934
  const wd = await mkdtemp(join(tmpdir(), 'omx-launch-inside-tmux-managed-'));
991
1935
  try {
992
1936
  const { env, tmuxLogPath } = await createLaunchFixture(wd, (tmuxLogPath) => `#!/bin/sh
1937
+ state="${tmuxLogPath}.hud-created"
1938
+ proof="${tmuxLogPath}.hud-proof"
1939
+ marker="${tmuxLogPath}.hud-marker"
993
1940
  printf 'tmux:%s\n' "$*" >> "${tmuxLogPath}"
994
1941
  case "$1" in
995
1942
  list-panes)
1943
+ case "$*" in
1944
+ *'#{pane_id}'*'#{pane_start_command}'*)
1945
+ if [ -f "$state" ]; then printf '%%1\tcodex\n%%2\tOMX_TMUX_SPLIT_OPERATION_MARKER='\''%s'\''; export OMX_TMUX_SPLIT_OPERATION_MARKER; exec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='\''%%1'\'' node hud\n' "$(cat "$marker")"; else printf '%%1\tcodex\n'; fi
1946
+ ;;
1947
+ *'#{pane_id}\t#{pane_dead}\t#{pane_pid}'*)
1948
+ printf '%%1\t0\t101\n%%2\t0\t202\n'
1949
+ ;;
1950
+ *'#{pane_id}'*)
1951
+ if [ -f "$state" ]; then printf '%%1\n%%2\n'; else printf '%%1\n'; fi
1952
+ ;;
1953
+ esac
996
1954
  exit 0
997
1955
  ;;
998
1956
  split-window)
999
- printf '%s\n' '%hud'
1957
+ printf '%%2\n'
1958
+ exit 0
1959
+ ;;
1960
+ if-shell)
1961
+ [ "$2" = '-F' ] && [ "$3" = '-t' ] && [ "$4" = '%1' ] || exit 1
1962
+ case "$5" in *'#{pane_pid},101'*'#{session_id},$7'*'#{window_id},@1'*) ;; *) exit 1 ;; esac
1963
+ success="$6"
1964
+ receipt=\${success##*display-message -p }
1965
+ receipt=\${receipt%% *}
1966
+ split_marker=$(printf '%s' "$success" | sed -n "s/.*OMX_TMUX_SPLIT_OPERATION_MARKER='\\([^']*\\)'.*/\\1/p")
1967
+ [ -n "$split_marker" ] || exit 1
1968
+ printf '%s' "$split_marker" > "$marker"
1969
+ : > "$state"
1970
+ printf '%s\n' "$receipt"
1000
1971
  exit 0
1001
1972
  ;;
1002
1973
  display-message)
1003
1974
  case "$*" in
1004
1975
  *'#{socket_path}'*) printf '/tmp/tmux-test.sock\n' ;;
1976
+ *'#{pane_id}'*'#{pane_dead}'*'#{pane_pid}'*'#{session_id}'*'#{window_id}'*) printf '%%1\t0\t101\t$7\t@1\n' ;;
1977
+ *'#{session_id}'*'#{window_id}'*) printf '$7\t@1\n' ;;
1005
1978
  *'#S'*) printf 'managed-session\n' ;;
1006
1979
  *) printf '0\n' ;;
1007
1980
  esac
1008
1981
  exit 0
1009
1982
  ;;
1010
1983
  show-options)
1011
- printf 'off\n'
1984
+ if [ "$2" = '-g' ] && [ "$3" = '-v' ]; then cat "$proof" 2>/dev/null || true; else printf 'off\n'; fi
1012
1985
  exit 0
1013
1986
  ;;
1014
- set-option|kill-pane)
1987
+ set-option)
1988
+ if [ "$2" = '-g' ]; then printf '%s\n' "$4" > "$proof"; fi
1989
+ exit 0
1990
+ ;;
1991
+ kill-pane)
1015
1992
  exit 0
1016
1993
  ;;
1017
1994
  esac
@@ -1027,15 +2004,15 @@ exit 0
1027
2004
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
1028
2005
  assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
1029
2006
  assert.match(result.stdout, /fake-codex:.*--dangerously-bypass-approvals-and-sandbox/);
1030
- assert.match(tmuxLog, new RegExp(`tmux:split-window -v -l ${HUD_TMUX_HEIGHT_LINES}`));
2007
+ assert.doesNotMatch(tmuxLog, /tmux:if-shell -F -t %1 .*split-window/);
1031
2008
  assert.match(tmuxLog, /tmux:set-option -t managed-session mouse on/);
1032
2009
  assert.match(tmuxLog, /tmux:set-option -sq extended-keys always/);
1033
2010
  }
1034
2011
  finally {
1035
- await rm(wd, { recursive: true, force: true });
2012
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
1036
2013
  }
1037
2014
  });
1038
- it('treats a missing tmux server socket as safe for detached tmux startup', async () => {
2015
+ it('does not probe an existing tmux server before detached tmux startup', async () => {
1039
2016
  const wd = await mkdtemp(join(tmpdir(), 'omx-launch-tmux-missing-socket-'));
1040
2017
  try {
1041
2018
  const home = join(wd, 'home');
@@ -1057,12 +2034,9 @@ case "$1" in
1057
2034
  printf 'tmux 3.6a\n'
1058
2035
  exit 0
1059
2036
  ;;
1060
- list-sessions)
1061
- printf 'error connecting to /private/tmp/tmux-501/default (No such file or directory)\n' >&2
1062
- exit 1
1063
- ;;
2037
+
1064
2038
  new-session)
1065
- printf 'leader-pane\n'
2039
+ printf '%%12\n'
1066
2040
  exit 0
1067
2041
  ;;
1068
2042
  split-window)
@@ -1088,6 +2062,7 @@ esac
1088
2062
  exit 0
1089
2063
  `);
1090
2064
  await chmod(fakeTmuxPath, 0o755);
2065
+ await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
1091
2066
  const result = runOmx(wd, ['--madmax', '--tmux'], {
1092
2067
  HOME: home,
1093
2068
  PATH: `${fakeBin}:/usr/bin:/bin`,
@@ -1101,15 +2076,15 @@ exit 0
1101
2076
  return;
1102
2077
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
1103
2078
  assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
1104
- assert.match(tmuxLog, /tmux:list-sessions/);
2079
+ assert.doesNotMatch(tmuxLog, /tmux:list-sessions/);
1105
2080
  assert.match(tmuxLog, /tmux:new-session .* -s /);
1106
2081
  assert.doesNotMatch(result.stderr, /server\/socket is unusable/);
1107
2082
  }
1108
2083
  finally {
1109
- await rm(wd, { recursive: true, force: true });
2084
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
1110
2085
  }
1111
2086
  });
1112
- it('falls back directly when tmux is installed but the server socket is unusable', async () => {
2087
+ it('does not fall back directly when a later detached tmux operation fails', async () => {
1113
2088
  const wd = await mkdtemp(join(tmpdir(), 'omx-launch-tmux-stale-socket-'));
1114
2089
  try {
1115
2090
  const home = join(wd, 'home');
@@ -1118,6 +2093,7 @@ exit 0
1118
2093
  const fakePsPath = join(fakeBin, 'ps');
1119
2094
  const fakeTmuxPath = join(fakeBin, 'tmux');
1120
2095
  const tmuxLogPath = join(wd, 'tmux.log');
2096
+ const runsPath = join(wd, 'runs');
1121
2097
  await mkdir(home, { recursive: true });
1122
2098
  await mkdir(fakeBin, { recursive: true });
1123
2099
  await writeFile(fakeCodexPath, '#!/bin/sh\nprintf \'fake-codex:%s\\n\' "$*"\n');
@@ -1128,40 +2104,49 @@ exit 0
1128
2104
  printf 'tmux:%s\n' "$*" >> "${tmuxLogPath}"
1129
2105
  case "$1" in
1130
2106
  -V)
2107
+ printf 'tmux:d0-runs:%s\n' "$(test -e "${runsPath}" && printf present || printf missing)" >> "${tmuxLogPath}"
2108
+
1131
2109
  printf 'tmux 3.6a\n'
1132
2110
  exit 0
1133
2111
  ;;
1134
- list-sessions)
1135
- printf 'error connecting to /tmp/tmux-1000/default (Operation not permitted)\n' >&2
2112
+ new-session)
2113
+ printf 'unsafe-tmux-handle-secret\n' >&2
1136
2114
  exit 1
1137
2115
  ;;
2116
+
1138
2117
  esac
1139
2118
  printf 'unexpected tmux command: %s\n' "$*" >&2
1140
2119
  exit 1
1141
2120
  `);
1142
2121
  await chmod(fakeTmuxPath, 0o755);
2122
+ await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
1143
2123
  const result = runOmx(wd, ['--madmax', '--tmux'], {
1144
2124
  HOME: home,
1145
2125
  PATH: `${fakeBin}:/usr/bin:/bin`,
1146
2126
  OMX_AUTO_UPDATE: '0',
1147
2127
  OMX_NOTIFY_FALLBACK: '0',
1148
2128
  OMX_HOOK_DERIVED_SIGNALS: '0',
2129
+ OMX_RUNS_DIR: runsPath,
1149
2130
  TMUX: '',
1150
2131
  TMUX_PANE: '',
1151
2132
  });
1152
2133
  if (shouldSkipForSpawnPermissions(result.error))
1153
2134
  return;
1154
2135
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
1155
- assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
1156
- assert.match(result.stdout, /fake-codex:.*--dangerously-bypass-approvals-and-sandbox/);
1157
- assert.match(result.stderr, /server\/socket is unusable/);
1158
- assert.doesNotMatch(tmuxLog, /new-session|attach-session/);
2136
+ assert.equal(result.status, 1, result.error || result.stderr || result.stdout);
2137
+ assert.match(result.stderr, /detached launch safety failure during inert-session/);
2138
+ assert.doesNotMatch(result.stderr, /unsafe-tmux-handle-secret/);
2139
+ assert.doesNotMatch(result.stdout, /fake-codex/);
2140
+ assert.match(tmuxLog, /tmux:-V/);
2141
+ assert.match(tmuxLog, /tmux:d0-runs:missing/);
2142
+ assert.match(tmuxLog, /tmux:new-session/);
2143
+ assert.doesNotMatch(tmuxLog, /tmux:list-sessions|attach-session/);
1159
2144
  }
1160
2145
  finally {
1161
- await rm(wd, { recursive: true, force: true });
2146
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
1162
2147
  }
1163
2148
  });
1164
- it('rolls back and falls back directly when attaching the detached tmux session fails', async () => {
2149
+ it('preserves detached ownership when attaching the released tmux session fails', async () => {
1165
2150
  const wd = await mkdtemp(join(tmpdir(), 'omx-launch-tmux-attach-fail-'));
1166
2151
  try {
1167
2152
  const home = join(wd, 'home');
@@ -1183,7 +2168,7 @@ case "$1" in
1183
2168
  exit 0
1184
2169
  ;;
1185
2170
  new-session)
1186
- printf 'leader-pane\n'
2171
+ printf '%%12\n'
1187
2172
  exit 0
1188
2173
  ;;
1189
2174
  split-window)
@@ -1213,6 +2198,7 @@ esac
1213
2198
  exit 0
1214
2199
  `);
1215
2200
  await chmod(fakeTmuxPath, 0o755);
2201
+ await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
1216
2202
  const result = runOmx(wd, ['--madmax', '--tmux'], {
1217
2203
  HOME: home,
1218
2204
  PATH: `${fakeBin}:/usr/bin:/bin`,
@@ -1224,17 +2210,18 @@ exit 0
1224
2210
  });
1225
2211
  if (shouldSkipForSpawnPermissions(result.error))
1226
2212
  return;
2213
+ await waitForPath(`${fakeTmuxPath}.leader-done`);
1227
2214
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
1228
- assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
1229
- assert.match(result.stdout, /fake-codex:.*--dangerously-bypass-approvals-and-sandbox/);
2215
+ assert.equal(result.status, 1, result.error || result.stderr || result.stdout);
2216
+ assert.match(result.stderr, /detached launch safety failure during post-release-attach/);
1230
2217
  assert.match(tmuxLog, /tmux:attach-session -t /);
1231
- assert.match(tmuxLog, /tmux:kill-session -t /);
2218
+ assert.doesNotMatch(tmuxLog, /tmux:kill-session -t /);
1232
2219
  }
1233
2220
  finally {
1234
- await rm(wd, { recursive: true, force: true });
2221
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
1235
2222
  }
1236
2223
  });
1237
- it('rolls back with guidance when WSL Windows Terminal attach exits without attaching', async () => {
2224
+ it('does not probe or fall back after a released WSL attach returns immediately', async () => {
1238
2225
  const wd = await mkdtemp(join(tmpdir(), 'omx-launch-tmux-attach-noop-'));
1239
2226
  try {
1240
2227
  const { env, tmuxLogPath } = await createLaunchFixture(wd, (tmuxLogPath) => `#!/bin/sh
@@ -1244,7 +2231,7 @@ case "$1" in
1244
2231
  exit 0
1245
2232
  ;;
1246
2233
  new-session)
1247
- printf 'leader-pane\n'
2234
+ printf '%%12\n'
1248
2235
  exit 0
1249
2236
  ;;
1250
2237
  split-window)
@@ -1281,15 +2268,12 @@ exit 0
1281
2268
  return;
1282
2269
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
1283
2270
  assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
1284
- assert.match(result.stdout, /fake-codex:.*--dangerously-bypass-approvals-and-sandbox/);
1285
- assert.match(result.stderr, /attach-session returned immediately without attaching a client/i);
1286
- assert.match(result.stderr, /Falling back to direct Codex launch/i);
2271
+ assert.doesNotMatch(result.stderr, /Falling back to direct Codex launch|attach-session returned immediately/);
1287
2272
  assert.match(tmuxLog, /tmux:attach-session -t /);
1288
- assert.match(tmuxLog, /tmux:display-message -p -t .* #\{session_attached\}/);
1289
- assert.match(tmuxLog, /tmux:kill-session -t /);
2273
+ assert.doesNotMatch(tmuxLog, /tmux:display-message -p -t .* #\{session_attached\}|tmux:kill-session -t/);
1290
2274
  }
1291
2275
  finally {
1292
- await rm(wd, { recursive: true, force: true });
2276
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
1293
2277
  }
1294
2278
  });
1295
2279
  it('preserves the requested cwd through detached tmux launch when an unsupported SHELL value falls back away from rc-driven cwd drift', async () => {
@@ -1325,11 +2309,7 @@ case "$cmd" in
1325
2309
  exit 0
1326
2310
  ;;
1327
2311
  new-session)
1328
- for last; do :; done
1329
- if [ -n "\${last:-}" ]; then
1330
- /bin/sh -c "$last"
1331
- fi
1332
- printf 'leader-pane\\n'
2312
+ printf '%%12\n'
1333
2313
  exit 0
1334
2314
  ;;
1335
2315
  split-window)
@@ -1355,6 +2335,7 @@ esac
1355
2335
  exit 0
1356
2336
  `);
1357
2337
  await chmod(fakeTmuxPath, 0o755);
2338
+ await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
1358
2339
  const result = runOmx(wd, ['--madmax', '--tmux'], {
1359
2340
  HOME: home,
1360
2341
  SHELL: '/definitely/missing-shell',
@@ -1367,13 +2348,14 @@ exit 0
1367
2348
  });
1368
2349
  if (shouldSkipForSpawnPermissions(result.error))
1369
2350
  return;
1370
- const codexLog = normalizeDarwinTmpPath(await readFile(codexLogPath, 'utf-8'));
1371
- assert.match(codexLog, /codex:.*--dangerously-bypass-approvals-and-sandbox/);
1372
- assert.match(codexLog, new RegExp(`codex-pwd:${escapeRegExp(normalizeDarwinTmpPath(wd))}`));
2351
+ await waitForPath(`${fakeTmuxPath}.leader-done`);
2352
+ const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
2353
+ assert.match(tmuxLog, /\/bin\/sh/);
2354
+ assert.match(tmuxLog, /__detached-session-leader/);
1373
2355
  assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
1374
2356
  }
1375
2357
  finally {
1376
- await rm(wd, { recursive: true, force: true });
2358
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
1377
2359
  }
1378
2360
  });
1379
2361
  it('falls back to /bin/sh for detached tmux launch when SHELL drifts to an unsupported path', async () => {
@@ -1407,11 +2389,7 @@ case "$cmd" in
1407
2389
  exit 0
1408
2390
  ;;
1409
2391
  new-session)
1410
- for last; do :; done
1411
- if [ -n "\${last:-}" ]; then
1412
- /bin/sh -c "$last"
1413
- fi
1414
- printf 'leader-pane\\n'
2392
+ printf '%%12\n'
1415
2393
  exit 0
1416
2394
  ;;
1417
2395
  split-window)
@@ -1437,6 +2415,7 @@ esac
1437
2415
  exit 0
1438
2416
  `);
1439
2417
  await chmod(fakeTmuxPath, 0o755);
2418
+ await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
1440
2419
  const result = runOmx(wd, ['--madmax', '--tmux'], {
1441
2420
  HOME: home,
1442
2421
  SHELL: '/bin/not-a-real-shell',
@@ -1450,15 +2429,199 @@ exit 0
1450
2429
  if (shouldSkipForSpawnPermissions(result.error))
1451
2430
  return;
1452
2431
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
1453
- const codexLog = normalizeDarwinTmpPath(await readFile(codexLogPath, 'utf-8'));
1454
2432
  assert.match(tmuxLog, /\/bin\/sh/);
1455
2433
  assert.doesNotMatch(tmuxLog, /not-a-real-shell/);
1456
- assert.match(codexLog, /codex:.*--dangerously-bypass-approvals-and-sandbox/);
1457
- assert.match(codexLog, new RegExp(`codex-pwd:${escapeRegExp(normalizeDarwinTmpPath(wd))}`));
2434
+ assert.match(tmuxLog, /__detached-session-leader/);
1458
2435
  assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
2436
+ await waitForPath(`${fakeTmuxPath}.leader-done`);
2437
+ }
2438
+ finally {
2439
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
2440
+ }
2441
+ });
2442
+ it('keeps finalization authority in the real detached leader process through child exit', async () => {
2443
+ const wd = await mkdtemp(join(tmpdir(), 'omx-detached-leader-process-'));
2444
+ const home = join(wd, 'home');
2445
+ const fakeBin = join(wd, 'bin');
2446
+ const fakeChild = join(wd, 'fake-codex.sh');
2447
+ const childReady = join(wd, 'child-ready');
2448
+ const childRelease = join(wd, 'child-release');
2449
+ const childEnv = join(wd, 'child-env');
2450
+ const descendantPidPath = join(wd, 'descendant-pid');
2451
+ const readyPath = join(wd, '.omx', 'runtime', 'detached-release', 'ready');
2452
+ const omxBin = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'dist', 'cli', 'omx.js');
2453
+ const sessionId = 'omx-detached-leader-process';
2454
+ try {
2455
+ await mkdir(home, { recursive: true });
2456
+ await mkdir(fakeBin, { recursive: true });
2457
+ await writeExecutable(join(fakeBin, 'tmux'), `#!/bin/sh
2458
+ if [ "$1" = "list-panes" ]; then
2459
+ printf '%s\t0\t%s\n' "$TMUX_PANE" "$PPID"
2460
+ fi
2461
+ exit 0
2462
+ `);
2463
+ await writeExecutable(fakeChild, `#!/bin/sh\nprintf '%s' "$OMX_NOTIFY_TEMP_CONTRACT" > ${JSON.stringify(childEnv)}\n(sh -c 'trap "" TERM; while :; do sleep 1; done') &\nprintf '%s' "$!" > ${JSON.stringify(descendantPidPath)}\nprintf ready > ${JSON.stringify(childReady)}\nwhile [ ! -f ${JSON.stringify(childRelease)} ]; do sleep 0.02; done\nexit 0\n`);
2464
+ const payload = Buffer.from(JSON.stringify({
2465
+ cwd: wd,
2466
+ sessionName: 'omx-detached-leader-process',
2467
+ sessionId,
2468
+ codexCmd: fakeChild,
2469
+ readyPath,
2470
+ preLaunchOptions: {
2471
+ notifyTempContract: {
2472
+ active: true,
2473
+ selectors: ['custom:test-provider'],
2474
+ canonicalSelectors: ['custom:test-provider'],
2475
+ warnings: [],
2476
+ source: 'cli',
2477
+ },
2478
+ enableNotifyFallbackAuthority: false,
2479
+ worktreeDirty: false,
2480
+ shouldAttach: true,
2481
+ },
2482
+ })).toString('base64url');
2483
+ const leader = spawn(process.execPath, [omxBin, '__detached-session-leader', payload], {
2484
+ cwd: wd,
2485
+ env: buildRunOmxEnv({
2486
+ HOME: home,
2487
+ TMUX: '/tmp/fake-tmux,1,0',
2488
+ TMUX_PANE: '%3202',
2489
+ PATH: `${fakeBin}:/usr/bin:/bin`,
2490
+ OMX_AUTO_UPDATE: '0',
2491
+ OMX_NOTIFY_FALLBACK: '0',
2492
+ OMX_HOOK_DERIVED_SIGNALS: '0',
2493
+ }),
2494
+ stdio: ['ignore', 'inherit', 'inherit'],
2495
+ });
2496
+ await waitForPath(readyPath);
2497
+ const ready = JSON.parse(await readFile(readyPath, 'utf-8'));
2498
+ await writeFile(`${readyPath}.release`, `${JSON.stringify({ version: 1, kind: 'release', nonce: `${ready.nonce}-foreign`, sessionId: ready.sessionId, sessionName: ready.sessionName, leaderPid: ready.leaderPid })}\n`);
2499
+ await new Promise((resolveWait) => setTimeout(resolveWait, 100));
2500
+ assert.equal(existsSync(childReady), false);
2501
+ await writeFile(`${readyPath}.release`, `${JSON.stringify({ version: 1, kind: 'release', nonce: ready.nonce, sessionId: ready.sessionId, sessionName: ready.sessionName, leaderPid: ready.leaderPid })}\n`);
2502
+ await waitForPath(childReady);
2503
+ assert.match(await readFile(childEnv, 'utf-8'), /custom:test-provider/);
2504
+ await waitForPath(descendantPidPath);
2505
+ const descendantPid = Number.parseInt((await readFile(descendantPidPath, 'utf-8')).trim(), 10);
2506
+ assert.equal(Number.isSafeInteger(descendantPid) && descendantPid > 0, true);
2507
+ assert.equal(existsSync(readyPath), true);
2508
+ const sessionState = JSON.parse(await readFile(join(wd, '.omx', 'state', 'session.json'), 'utf-8'));
2509
+ assert.equal(sessionState.tmux_session_name, 'omx-detached-leader-process');
2510
+ assert.equal(sessionState.tmux_pane_id, '%3202');
2511
+ assert.equal(existsSync(join(wd, '.omx', 'state', 'detached-active-record.json')), true);
2512
+ await writeFile(childRelease, 'release\n');
2513
+ const exitCode = await new Promise((resolveExit, rejectExit) => {
2514
+ leader.once('error', rejectExit);
2515
+ leader.once('exit', resolveExit);
2516
+ });
2517
+ assert.equal(exitCode, 0);
2518
+ assert.throws(() => process.kill(descendantPid, 0), (error) => error.code === 'ESRCH');
2519
+ const terminalReport = JSON.parse(await readFile(readyPath, 'utf-8'));
2520
+ assert.deepEqual(terminalReport, {
2521
+ version: 1,
2522
+ kind: 'terminal',
2523
+ nonce: ready.nonce,
2524
+ sessionId: ready.sessionId,
2525
+ sessionName: ready.sessionName,
2526
+ leaderPid: ready.leaderPid,
2527
+ paneId: '%3202',
2528
+ finalized: true,
2529
+ exitStatus: 0,
2530
+ });
2531
+ assert.equal(existsSync(join(wd, '.omx', 'state', 'session.json')), false);
2532
+ assert.equal(existsSync(join(wd, '.omx', 'state', 'detached-active-record.json')), false);
2533
+ assert.equal(existsSync(readyPath), true);
2534
+ await rm(readyPath, { force: true });
2535
+ assert.match(await readFile(join(wd, '.omx', 'logs', 'session-history.jsonl'), 'utf-8'), new RegExp(sessionId));
2536
+ }
2537
+ finally {
2538
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
2539
+ }
2540
+ });
2541
+ it('returns the attached client to the shell with the child status after a normal detached child exit', async (t) => {
2542
+ if (!skipUnlessPrivateRealTmux(t))
2543
+ return;
2544
+ if (!skipUnlessScriptAvailable(t))
2545
+ return;
2546
+ const wd = await mkdtemp(join(tmpdir(), 'omx-launch-detached-e2e-'));
2547
+ try {
2548
+ await withTempTmuxSession(async (fixture) => {
2549
+ const home = join(wd, 'home');
2550
+ const bin = join(wd, 'bin');
2551
+ await mkdir(home, { recursive: true });
2552
+ await mkdir(bin, { recursive: true });
2553
+ // The PTY is required for detached attach behavior, but it would also
2554
+ // trigger OMX's unrelated one-time interactive star prompt. Mark it
2555
+ // handled so this harness deterministically exercises only launch exit.
2556
+ await mkdir(join(home, '.omx', 'state'), { recursive: true });
2557
+ await writeFile(join(home, '.omx', 'state', 'star-prompt.json'), '{"prompted_at":"2026-01-01T00:00:00.000Z"}\n');
2558
+ await fixture.createPathShim(bin);
2559
+ const omxBin = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'dist', 'cli', 'omx.js');
2560
+ const runPtyLaunch = (childExitStatus, remainOnExit) => {
2561
+ writeFileSync(join(bin, 'codex'), `#!/bin/sh\nsleep 1\nexit ${childExitStatus}\n`, { mode: 0o755 });
2562
+ // Simulate a user tmux config that would retain the dead leader pane.
2563
+ fixture.run(['set-option', '-g', 'remain-on-exit', remainOnExit]);
2564
+ const envPrefix = [
2565
+ 'unset TMUX TMUX_PANE',
2566
+ 'export OMX_TEST_DETACHED_TRACE=1',
2567
+ `export OMX_TEST_DETACHED_TRACE_PATH=${JSON.stringify(join(wd, 'detached-trace.log'))}`,
2568
+ `export HOME=${JSON.stringify(home)}`,
2569
+ `export PATH=${JSON.stringify(bin)}:$PATH`,
2570
+ 'export OMX_AUTO_UPDATE=0',
2571
+ 'export OMX_NOTIFY_FALLBACK=0',
2572
+ 'export OMX_HOOK_DERIVED_SIGNALS=0',
2573
+ 'export TERM=xterm-256color',
2574
+ ].join('; ');
2575
+ const command = `${envPrefix}; cd ${JSON.stringify(wd)} || exit 125; ${JSON.stringify(process.execPath)} ${JSON.stringify(omxBin)} --tmux ${JSON.stringify('e2e prompt')}; child_status=$?; printf '\n%s:%s\n' 'omx-follow-up' "$child_status"; exit "$child_status"`;
2576
+ if (process.platform === 'darwin') {
2577
+ const result = fixture.runPtyResult(command, { pollLimit: DARWIN_PTY_FINALIZATION_POLL_LIMIT });
2578
+ let diagnostics = '';
2579
+ if (result.error) {
2580
+ const sessions = fixture.runResult(['list-sessions', '-F', '#{session_name}']);
2581
+ for (const session of sessions.stdout.split('\n').filter((name) => name.startsWith('omx-'))) {
2582
+ const panes = fixture.runResult([
2583
+ 'list-panes', '-t', session, '-F', '#{pane_id}\t#{pane_dead}\t#{pane_dead_status}\t#{pane_pid}\t#{pane_current_command}\t#{@omx_instance_id}\t#{@omx_hud_owner}',
2584
+ ]);
2585
+ diagnostics += `\n[${session}]\n${panes.stdout}`;
2586
+ for (const paneLine of panes.stdout.split('\n').filter(Boolean)) {
2587
+ const [paneId] = paneLine.split('\t', 1);
2588
+ if (!paneId)
2589
+ continue;
2590
+ diagnostics += `\n[${paneLine}]\n${fixture.runResult(['capture-pane', '-p', '-t', paneId, '-S', '-']).stdout}`;
2591
+ }
2592
+ }
2593
+ const tracePath = join(wd, 'detached-trace.log');
2594
+ if (existsSync(tracePath))
2595
+ diagnostics += `\n[detached-trace]\n${readFileSync(tracePath, 'utf-8')}`;
2596
+ }
2597
+ return { status: result.status, output: `${result.stdout}\n${result.stderr}${diagnostics}`, error: result.error };
2598
+ }
2599
+ const ptyCommand = buildPtyScriptCommand(command);
2600
+ const result = spawnSync(ptyCommand.executable, ptyCommand.args, {
2601
+ encoding: 'utf-8',
2602
+ timeout: 120_000,
2603
+ killSignal: 'SIGKILL',
2604
+ env: buildRunOmxEnv({ TMUX: '', TMUX_PANE: '' }),
2605
+ });
2606
+ return { status: result.status, output: `${result.stdout || ''}\n${result.stderr || ''}`, error: result.error?.message ?? '' };
2607
+ };
2608
+ const listOmxSessions = () => fixture.run(['list-sessions', '-F', '#{session_name}'])
2609
+ .split('\n')
2610
+ .filter((name) => name.startsWith('omx-') && name !== fixture.sessionName);
2611
+ const zero = runPtyLaunch(0, 'on');
2612
+ assert.equal(zero.error, '', `zero-status PTY capture and cleanup must succeed: ${zero.error}\n${zero.output}`);
2613
+ assert.equal(zero.status, 0, `zero-status attached launch must return to the shell successfully:\n${zero.output}`);
2614
+ assert.match(zero.output, /omx-follow-up:0/, `zero-status launch must leave a usable shell for a follow-up command:\n${zero.output}`);
2615
+ assert.deepEqual(listOmxSessions(), [], 'zero-status exit must destroy the owned detached session despite remain-on-exit=on');
2616
+ const seven = runPtyLaunch(7, 'failed');
2617
+ assert.equal(seven.error, '', `nonzero-status PTY capture and cleanup must succeed: ${seven.error}\n${seven.output}`);
2618
+ assert.equal(seven.status, 7, `nonzero child status must propagate to the invoking shell:\n${seven.output}`);
2619
+ assert.match(seven.output, /omx-follow-up:7/, `nonzero launch must leave a usable shell for a follow-up command:\n${seven.output}`);
2620
+ assert.deepEqual(listOmxSessions(), [], 'nonzero exit must destroy the owned detached session despite remain-on-exit=failed');
2621
+ });
1459
2622
  }
1460
2623
  finally {
1461
- await rm(wd, { recursive: true, force: true });
2624
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
1462
2625
  }
1463
2626
  });
1464
2627
  });