oh-my-codex 0.20.3 → 0.20.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (661) hide show
  1. package/Cargo.lock +35 -18
  2. package/Cargo.toml +1 -1
  3. package/README.md +2 -2
  4. package/crates/omx-runtime/Cargo.toml +3 -0
  5. package/crates/omx-runtime/src/main.rs +390 -0
  6. package/crates/omx-runtime/tests/execution.rs +264 -0
  7. package/dist/adapt/__tests__/herdr-target.test.d.ts +2 -0
  8. package/dist/adapt/__tests__/herdr-target.test.d.ts.map +1 -0
  9. package/dist/adapt/__tests__/herdr-target.test.js +72 -0
  10. package/dist/adapt/__tests__/herdr-target.test.js.map +1 -0
  11. package/dist/adapt/contracts.d.ts +1 -1
  12. package/dist/adapt/contracts.d.ts.map +1 -1
  13. package/dist/adapt/contracts.js +1 -1
  14. package/dist/adapt/contracts.js.map +1 -1
  15. package/dist/adapt/herdr/__tests__/authority.test.d.ts +2 -0
  16. package/dist/adapt/herdr/__tests__/authority.test.d.ts.map +1 -0
  17. package/dist/adapt/herdr/__tests__/authority.test.js +49 -0
  18. package/dist/adapt/herdr/__tests__/authority.test.js.map +1 -0
  19. package/dist/adapt/herdr/__tests__/bridge.test.d.ts +2 -0
  20. package/dist/adapt/herdr/__tests__/bridge.test.d.ts.map +1 -0
  21. package/dist/adapt/herdr/__tests__/bridge.test.js +176 -0
  22. package/dist/adapt/herdr/__tests__/bridge.test.js.map +1 -0
  23. package/dist/adapt/herdr/__tests__/sanitize.test.d.ts +2 -0
  24. package/dist/adapt/herdr/__tests__/sanitize.test.d.ts.map +1 -0
  25. package/dist/adapt/herdr/__tests__/sanitize.test.js +45 -0
  26. package/dist/adapt/herdr/__tests__/sanitize.test.js.map +1 -0
  27. package/dist/adapt/herdr/__tests__/semantic.test.d.ts +2 -0
  28. package/dist/adapt/herdr/__tests__/semantic.test.d.ts.map +1 -0
  29. package/dist/adapt/herdr/__tests__/semantic.test.js +130 -0
  30. package/dist/adapt/herdr/__tests__/semantic.test.js.map +1 -0
  31. package/dist/adapt/herdr/__tests__/seq-store.test.d.ts +2 -0
  32. package/dist/adapt/herdr/__tests__/seq-store.test.d.ts.map +1 -0
  33. package/dist/adapt/herdr/__tests__/seq-store.test.js +54 -0
  34. package/dist/adapt/herdr/__tests__/seq-store.test.js.map +1 -0
  35. package/dist/adapt/herdr/__tests__/socket-live.test.d.ts +2 -0
  36. package/dist/adapt/herdr/__tests__/socket-live.test.d.ts.map +1 -0
  37. package/dist/adapt/herdr/__tests__/socket-live.test.js +104 -0
  38. package/dist/adapt/herdr/__tests__/socket-live.test.js.map +1 -0
  39. package/dist/adapt/herdr/__tests__/team-map.test.d.ts +2 -0
  40. package/dist/adapt/herdr/__tests__/team-map.test.d.ts.map +1 -0
  41. package/dist/adapt/herdr/__tests__/team-map.test.js +46 -0
  42. package/dist/adapt/herdr/__tests__/team-map.test.js.map +1 -0
  43. package/dist/adapt/herdr/__tests__/transport.test.d.ts +2 -0
  44. package/dist/adapt/herdr/__tests__/transport.test.d.ts.map +1 -0
  45. package/dist/adapt/herdr/__tests__/transport.test.js +160 -0
  46. package/dist/adapt/herdr/__tests__/transport.test.js.map +1 -0
  47. package/dist/adapt/herdr/__tests__/wiring.test.d.ts +2 -0
  48. package/dist/adapt/herdr/__tests__/wiring.test.d.ts.map +1 -0
  49. package/dist/adapt/herdr/__tests__/wiring.test.js +118 -0
  50. package/dist/adapt/herdr/__tests__/wiring.test.js.map +1 -0
  51. package/dist/adapt/herdr/authority.d.ts +34 -0
  52. package/dist/adapt/herdr/authority.d.ts.map +1 -0
  53. package/dist/adapt/herdr/authority.js +71 -0
  54. package/dist/adapt/herdr/authority.js.map +1 -0
  55. package/dist/adapt/herdr/bridge.d.ts +84 -0
  56. package/dist/adapt/herdr/bridge.d.ts.map +1 -0
  57. package/dist/adapt/herdr/bridge.js +222 -0
  58. package/dist/adapt/herdr/bridge.js.map +1 -0
  59. package/dist/adapt/herdr/sanitize.d.ts +20 -0
  60. package/dist/adapt/herdr/sanitize.d.ts.map +1 -0
  61. package/dist/adapt/herdr/sanitize.js +70 -0
  62. package/dist/adapt/herdr/sanitize.js.map +1 -0
  63. package/dist/adapt/herdr/semantic.d.ts +67 -0
  64. package/dist/adapt/herdr/semantic.d.ts.map +1 -0
  65. package/dist/adapt/herdr/semantic.js +170 -0
  66. package/dist/adapt/herdr/semantic.js.map +1 -0
  67. package/dist/adapt/herdr/seq-store.d.ts +32 -0
  68. package/dist/adapt/herdr/seq-store.d.ts.map +1 -0
  69. package/dist/adapt/herdr/seq-store.js +126 -0
  70. package/dist/adapt/herdr/seq-store.js.map +1 -0
  71. package/dist/adapt/herdr/team-map.d.ts +30 -0
  72. package/dist/adapt/herdr/team-map.d.ts.map +1 -0
  73. package/dist/adapt/herdr/team-map.js +37 -0
  74. package/dist/adapt/herdr/team-map.js.map +1 -0
  75. package/dist/adapt/herdr/transport.d.ts +115 -0
  76. package/dist/adapt/herdr/transport.d.ts.map +1 -0
  77. package/dist/adapt/herdr/transport.js +260 -0
  78. package/dist/adapt/herdr/transport.js.map +1 -0
  79. package/dist/adapt/herdr/wiring.d.ts +24 -0
  80. package/dist/adapt/herdr/wiring.d.ts.map +1 -0
  81. package/dist/adapt/herdr/wiring.js +62 -0
  82. package/dist/adapt/herdr/wiring.js.map +1 -0
  83. package/dist/adapt/herdr.d.ts +19 -0
  84. package/dist/adapt/herdr.d.ts.map +1 -0
  85. package/dist/adapt/herdr.js +115 -0
  86. package/dist/adapt/herdr.js.map +1 -0
  87. package/dist/adapt/index.d.ts +4 -0
  88. package/dist/adapt/index.d.ts.map +1 -1
  89. package/dist/adapt/index.js +48 -0
  90. package/dist/adapt/index.js.map +1 -1
  91. package/dist/adapt/registry.d.ts.map +1 -1
  92. package/dist/adapt/registry.js +11 -0
  93. package/dist/adapt/registry.js.map +1 -1
  94. package/dist/auth/__tests__/hotswap.test.js +60 -1
  95. package/dist/auth/__tests__/hotswap.test.js.map +1 -1
  96. package/dist/auth/__tests__/storage.test.js +33 -1
  97. package/dist/auth/__tests__/storage.test.js.map +1 -1
  98. package/dist/auth/hotswap.d.ts +16 -2
  99. package/dist/auth/hotswap.d.ts.map +1 -1
  100. package/dist/auth/hotswap.js +22 -7
  101. package/dist/auth/hotswap.js.map +1 -1
  102. package/dist/auth/storage.js +1 -1
  103. package/dist/auth/storage.js.map +1 -1
  104. package/dist/autopilot/__tests__/ralplan-gate.test.js +86 -1333
  105. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  106. package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
  107. package/dist/autopilot/ralplan-gate.js +7 -6
  108. package/dist/autopilot/ralplan-gate.js.map +1 -1
  109. package/dist/cli/__tests__/agents-init.test.js +4 -23
  110. package/dist/cli/__tests__/agents-init.test.js.map +1 -1
  111. package/dist/cli/__tests__/api.test.js +61 -24
  112. package/dist/cli/__tests__/api.test.js.map +1 -1
  113. package/dist/cli/__tests__/auth.test.js +3 -1
  114. package/dist/cli/__tests__/auth.test.js.map +1 -1
  115. package/dist/cli/__tests__/codex-feature-probe.test.js +66 -1
  116. package/dist/cli/__tests__/codex-feature-probe.test.js.map +1 -1
  117. package/dist/cli/__tests__/codex-plugin-layout.test.js +70 -2
  118. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  119. package/dist/cli/__tests__/detached-leader-teardown.test.d.ts +2 -0
  120. package/dist/cli/__tests__/detached-leader-teardown.test.d.ts.map +1 -0
  121. package/dist/cli/__tests__/detached-leader-teardown.test.js +473 -0
  122. package/dist/cli/__tests__/detached-leader-teardown.test.js.map +1 -0
  123. package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts +2 -0
  124. package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts.map +1 -0
  125. package/dist/cli/__tests__/doctor-process-identity-readiness.test.js +103 -0
  126. package/dist/cli/__tests__/doctor-process-identity-readiness.test.js.map +1 -0
  127. package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts +2 -0
  128. package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts.map +1 -0
  129. package/dist/cli/__tests__/doctor-state-root-binding.test.js +139 -0
  130. package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -0
  131. package/dist/cli/__tests__/doctor-warning-copy.test.js +35 -3
  132. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  133. package/dist/cli/__tests__/exec.test.js +75 -0
  134. package/dist/cli/__tests__/exec.test.js.map +1 -1
  135. package/dist/cli/__tests__/index.test.js +621 -460
  136. package/dist/cli/__tests__/index.test.js.map +1 -1
  137. package/dist/cli/__tests__/launch-fallback.test.js +1020 -143
  138. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  139. package/dist/cli/__tests__/mcp-parity.test.js +4 -3
  140. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
  141. package/dist/cli/__tests__/native-assets.test.js +398 -16
  142. package/dist/cli/__tests__/native-assets.test.js.map +1 -1
  143. package/dist/cli/__tests__/native-hook-claim-journal.test.js +53 -5
  144. package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
  145. package/dist/cli/__tests__/ralph.test.js +2 -2
  146. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  147. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +57 -57
  148. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
  149. package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts +2 -0
  150. package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts.map +1 -0
  151. package/dist/cli/__tests__/ralplan-child-binding-3212.test.js +56 -0
  152. package/dist/cli/__tests__/ralplan-child-binding-3212.test.js.map +1 -0
  153. package/dist/cli/__tests__/ralplan.test.js +84 -12
  154. package/dist/cli/__tests__/ralplan.test.js.map +1 -1
  155. package/dist/cli/__tests__/session-scoped-runtime.test.js +1118 -43
  156. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  157. package/dist/cli/__tests__/setup-agents-overwrite.test.js +8 -55
  158. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  159. package/dist/cli/__tests__/setup-install-mode.test.js +183 -4
  160. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  161. package/dist/cli/__tests__/sparkshell-cli.test.js +93 -41
  162. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  163. package/dist/cli/__tests__/team.test.js +45 -1
  164. package/dist/cli/__tests__/team.test.js.map +1 -1
  165. package/dist/cli/__tests__/ultragoal.test.js +117 -2
  166. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  167. package/dist/cli/__tests__/uninstall.test.js +50 -5
  168. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  169. package/dist/cli/__tests__/update.test.js +669 -177
  170. package/dist/cli/__tests__/update.test.js.map +1 -1
  171. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +32 -2
  172. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  173. package/dist/cli/api.d.ts.map +1 -1
  174. package/dist/cli/api.js +9 -10
  175. package/dist/cli/api.js.map +1 -1
  176. package/dist/cli/codex-feature-probe.d.ts +16 -0
  177. package/dist/cli/codex-feature-probe.d.ts.map +1 -1
  178. package/dist/cli/codex-feature-probe.js +44 -0
  179. package/dist/cli/codex-feature-probe.js.map +1 -1
  180. package/dist/cli/codex-home.d.ts.map +1 -1
  181. package/dist/cli/codex-home.js +7 -4
  182. package/dist/cli/codex-home.js.map +1 -1
  183. package/dist/cli/doctor.d.ts +17 -0
  184. package/dist/cli/doctor.d.ts.map +1 -1
  185. package/dist/cli/doctor.js +277 -3
  186. package/dist/cli/doctor.js.map +1 -1
  187. package/dist/cli/index.d.ts +231 -12
  188. package/dist/cli/index.d.ts.map +1 -1
  189. package/dist/cli/index.js +2298 -735
  190. package/dist/cli/index.js.map +1 -1
  191. package/dist/cli/native-assets.d.ts +25 -0
  192. package/dist/cli/native-assets.d.ts.map +1 -1
  193. package/dist/cli/native-assets.js +448 -51
  194. package/dist/cli/native-assets.js.map +1 -1
  195. package/dist/cli/native-hook-claim-journal.d.ts +8 -4
  196. package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
  197. package/dist/cli/native-hook-claim-journal.js +30 -8
  198. package/dist/cli/native-hook-claim-journal.js.map +1 -1
  199. package/dist/cli/package-manager-ownership.d.ts +63 -0
  200. package/dist/cli/package-manager-ownership.d.ts.map +1 -0
  201. package/dist/cli/package-manager-ownership.js +323 -0
  202. package/dist/cli/package-manager-ownership.js.map +1 -0
  203. package/dist/cli/ralph.js +2 -2
  204. package/dist/cli/ralph.js.map +1 -1
  205. package/dist/cli/ralplan.d.ts +4 -2
  206. package/dist/cli/ralplan.d.ts.map +1 -1
  207. package/dist/cli/ralplan.js +76 -34
  208. package/dist/cli/ralplan.js.map +1 -1
  209. package/dist/cli/setup-preferences.d.ts +15 -0
  210. package/dist/cli/setup-preferences.d.ts.map +1 -1
  211. package/dist/cli/setup-preferences.js +27 -1
  212. package/dist/cli/setup-preferences.js.map +1 -1
  213. package/dist/cli/setup.d.ts +3 -0
  214. package/dist/cli/setup.d.ts.map +1 -1
  215. package/dist/cli/setup.js +55 -32
  216. package/dist/cli/setup.js.map +1 -1
  217. package/dist/cli/sparkshell.d.ts.map +1 -1
  218. package/dist/cli/sparkshell.js +52 -9
  219. package/dist/cli/sparkshell.js.map +1 -1
  220. package/dist/cli/ultragoal.d.ts.map +1 -1
  221. package/dist/cli/ultragoal.js +2 -2
  222. package/dist/cli/ultragoal.js.map +1 -1
  223. package/dist/cli/uninstall.d.ts.map +1 -1
  224. package/dist/cli/uninstall.js +27 -28
  225. package/dist/cli/uninstall.js.map +1 -1
  226. package/dist/cli/update-worker.d.ts +2 -0
  227. package/dist/cli/update-worker.d.ts.map +1 -0
  228. package/dist/cli/update-worker.js +142 -0
  229. package/dist/cli/update-worker.js.map +1 -0
  230. package/dist/cli/update.d.ts +14 -22
  231. package/dist/cli/update.d.ts.map +1 -1
  232. package/dist/cli/update.js +192 -241
  233. package/dist/cli/update.js.map +1 -1
  234. package/dist/config/__tests__/generator-idempotent.test.js +173 -29
  235. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  236. package/dist/config/__tests__/generator-notify.test.js +3 -1
  237. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  238. package/dist/config/generator.d.ts +25 -3
  239. package/dist/config/generator.d.ts.map +1 -1
  240. package/dist/config/generator.js +113 -35
  241. package/dist/config/generator.js.map +1 -1
  242. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +25 -0
  243. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
  244. package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -1
  245. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
  246. package/dist/goal-workflows/codex-goal-snapshot.js +2 -0
  247. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
  248. package/dist/hooks/__tests__/agents-overlay.test.js +3 -1
  249. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  250. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +27 -4
  251. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  252. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts +2 -0
  253. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts.map +1 -0
  254. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js +172 -0
  255. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js.map +1 -0
  256. package/dist/hooks/__tests__/keyword-detector.test.js +172 -33
  257. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  258. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +173 -11
  259. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  260. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +1 -0
  261. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  262. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +65 -17
  263. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  264. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +27 -0
  265. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  266. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +10 -118
  267. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
  268. package/dist/hooks/__tests__/notify-hook-regression-205.test.js +7 -0
  269. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
  270. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +2 -2
  271. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  272. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +4 -1
  273. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  274. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js +2 -1
  275. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js.map +1 -1
  276. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +1 -0
  277. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  278. package/dist/hooks/__tests__/prompt-guidance-contract.test.js +9 -0
  279. package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
  280. package/dist/hooks/__tests__/session.test.js +2976 -557
  281. package/dist/hooks/__tests__/session.test.js.map +1 -1
  282. package/dist/hooks/agents-overlay.js +1 -1
  283. package/dist/hooks/agents-overlay.js.map +1 -1
  284. package/dist/hooks/extensibility/__tests__/dispatcher.test.js +108 -2
  285. package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
  286. package/dist/hooks/extensibility/__tests__/sdk.test.js +151 -1
  287. package/dist/hooks/extensibility/__tests__/sdk.test.js.map +1 -1
  288. package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
  289. package/dist/hooks/extensibility/dispatcher.js +20 -3
  290. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  291. package/dist/hooks/extensibility/plugin-runner.js +1 -0
  292. package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
  293. package/dist/hooks/extensibility/runtime.d.ts.map +1 -1
  294. package/dist/hooks/extensibility/runtime.js.map +1 -1
  295. package/dist/hooks/extensibility/sdk/paths.d.ts +1 -1
  296. package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -1
  297. package/dist/hooks/extensibility/sdk/paths.js +2 -2
  298. package/dist/hooks/extensibility/sdk/paths.js.map +1 -1
  299. package/dist/hooks/extensibility/sdk/runtime-state.d.ts +1 -1
  300. package/dist/hooks/extensibility/sdk/runtime-state.d.ts.map +1 -1
  301. package/dist/hooks/extensibility/sdk/runtime-state.js +55 -15
  302. package/dist/hooks/extensibility/sdk/runtime-state.js.map +1 -1
  303. package/dist/hooks/extensibility/sdk.d.ts +1 -0
  304. package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
  305. package/dist/hooks/extensibility/sdk.js +1 -1
  306. package/dist/hooks/extensibility/sdk.js.map +1 -1
  307. package/dist/hooks/extensibility/types.d.ts +7 -0
  308. package/dist/hooks/extensibility/types.d.ts.map +1 -1
  309. package/dist/hooks/keyword-detector.d.ts +10 -3
  310. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  311. package/dist/hooks/keyword-detector.js +116 -20
  312. package/dist/hooks/keyword-detector.js.map +1 -1
  313. package/dist/hooks/session.d.ts +180 -14
  314. package/dist/hooks/session.d.ts.map +1 -1
  315. package/dist/hooks/session.js +1402 -281
  316. package/dist/hooks/session.js.map +1 -1
  317. package/dist/hud/__tests__/hud-tmux-injection.test.js +6 -6
  318. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  319. package/dist/hud/__tests__/index.test.js +26 -16
  320. package/dist/hud/__tests__/index.test.js.map +1 -1
  321. package/dist/hud/__tests__/reconcile.test.js +83 -78
  322. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  323. package/dist/hud/__tests__/state.test.js +36 -31
  324. package/dist/hud/__tests__/state.test.js.map +1 -1
  325. package/dist/hud/__tests__/tmux.test.js +155 -198
  326. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  327. package/dist/hud/tmux.d.ts +42 -4
  328. package/dist/hud/tmux.d.ts.map +1 -1
  329. package/dist/hud/tmux.js +1017 -139
  330. package/dist/hud/tmux.js.map +1 -1
  331. package/dist/leader/__tests__/contract.test.js +71 -34
  332. package/dist/leader/__tests__/contract.test.js.map +1 -1
  333. package/dist/leader/contract.d.ts +1 -6
  334. package/dist/leader/contract.d.ts.map +1 -1
  335. package/dist/leader/contract.js +25 -24
  336. package/dist/leader/contract.js.map +1 -1
  337. package/dist/mcp/__tests__/hermes-bridge.test.js +17 -0
  338. package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
  339. package/dist/mcp/__tests__/server-lifecycle.test.js +4 -4
  340. package/dist/mcp/__tests__/state-paths.test.js +735 -4
  341. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  342. package/dist/mcp/__tests__/state-server.test.js +1 -1
  343. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  344. package/dist/mcp/__tests__/trace-server.test.js +2 -2
  345. package/dist/mcp/__tests__/trace-server.test.js.map +1 -1
  346. package/dist/mcp/hermes-bridge.d.ts.map +1 -1
  347. package/dist/mcp/hermes-bridge.js +2 -2
  348. package/dist/mcp/hermes-bridge.js.map +1 -1
  349. package/dist/mcp/state-paths.d.ts +78 -4
  350. package/dist/mcp/state-paths.d.ts.map +1 -1
  351. package/dist/mcp/state-paths.js +543 -40
  352. package/dist/mcp/state-paths.js.map +1 -1
  353. package/dist/mcp/state-server.d.ts +4 -4
  354. package/dist/mcp/state-server.d.ts.map +1 -1
  355. package/dist/modes/__tests__/base-autopilot-gates.test.js +36 -7
  356. package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
  357. package/dist/modes/__tests__/base-multi-state-compat.test.js +1 -1
  358. package/dist/modes/__tests__/base-multi-state-compat.test.js.map +1 -1
  359. package/dist/modes/__tests__/base-session-scope.test.js +17 -8
  360. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  361. package/dist/modes/base.d.ts +5 -5
  362. package/dist/modes/base.d.ts.map +1 -1
  363. package/dist/modes/base.js +76 -21
  364. package/dist/modes/base.js.map +1 -1
  365. package/dist/native-assets/archive.d.ts +17 -0
  366. package/dist/native-assets/archive.d.ts.map +1 -0
  367. package/dist/native-assets/archive.js +238 -0
  368. package/dist/native-assets/archive.js.map +1 -0
  369. package/dist/native-assets/policy.d.ts +42 -0
  370. package/dist/native-assets/policy.d.ts.map +1 -0
  371. package/dist/native-assets/policy.js +155 -0
  372. package/dist/native-assets/policy.js.map +1 -0
  373. package/dist/pipeline/__tests__/orchestrator.test.js +162 -40
  374. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  375. package/dist/pipeline/__tests__/stages.test.js +66 -149
  376. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  377. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  378. package/dist/pipeline/orchestrator.js +124 -18
  379. package/dist/pipeline/orchestrator.js.map +1 -1
  380. package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
  381. package/dist/pipeline/stages/ralplan.js +36 -8
  382. package/dist/pipeline/stages/ralplan.js.map +1 -1
  383. package/dist/ralplan/__tests__/consensus-gate.test.js +70 -1437
  384. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
  385. package/dist/ralplan/__tests__/documented-leader-preflight.test.js +328 -39
  386. package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -1
  387. package/dist/ralplan/__tests__/runtime.test.js +82 -102
  388. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  389. package/dist/ralplan/consensus-gate.d.ts +9 -1
  390. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  391. package/dist/ralplan/consensus-gate.js +40 -304
  392. package/dist/ralplan/consensus-gate.js.map +1 -1
  393. package/dist/ralplan/documented-leader-preflight.d.ts +15 -8
  394. package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -1
  395. package/dist/ralplan/documented-leader-preflight.js +356 -54
  396. package/dist/ralplan/documented-leader-preflight.js.map +1 -1
  397. package/dist/ralplan/runtime.d.ts +1 -1
  398. package/dist/ralplan/runtime.d.ts.map +1 -1
  399. package/dist/ralplan/runtime.js +68 -173
  400. package/dist/ralplan/runtime.js.map +1 -1
  401. package/dist/runtime/run-state.d.ts +5 -1
  402. package/dist/runtime/run-state.d.ts.map +1 -1
  403. package/dist/runtime/run-state.js +15 -8
  404. package/dist/runtime/run-state.js.map +1 -1
  405. package/dist/scripts/__tests__/codex-native-hook.test.js +14071 -8859
  406. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  407. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts +2 -0
  408. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts.map +1 -0
  409. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js +225 -0
  410. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js.map +1 -0
  411. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts +2 -0
  412. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts.map +1 -0
  413. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js +188 -0
  414. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js.map +1 -0
  415. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts +2 -0
  416. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts.map +1 -0
  417. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js +259 -0
  418. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js.map +1 -0
  419. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts +2 -0
  420. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts.map +1 -0
  421. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js +491 -0
  422. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js.map +1 -0
  423. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts +2 -0
  424. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts.map +1 -0
  425. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js +708 -0
  426. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js.map +1 -0
  427. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts +2 -0
  428. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts.map +1 -0
  429. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js +208 -0
  430. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js.map +1 -0
  431. package/dist/scripts/__tests__/notify-state-io.test.js +26 -0
  432. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  433. package/dist/scripts/__tests__/notify-tmux-injection.test.js +23 -1
  434. package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -1
  435. package/dist/scripts/__tests__/postinstall.test.js +19 -1
  436. package/dist/scripts/__tests__/postinstall.test.js.map +1 -1
  437. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +25 -8
  438. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
  439. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +60 -25
  440. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
  441. package/dist/scripts/__tests__/smoke-packed-install.test.js +155 -16
  442. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  443. package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts +2 -0
  444. package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts.map +1 -0
  445. package/dist/scripts/__tests__/verify-native-release-assets.test.js +235 -0
  446. package/dist/scripts/__tests__/verify-native-release-assets.test.js.map +1 -0
  447. package/dist/scripts/codex-native-hook.d.ts +50 -1
  448. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  449. package/dist/scripts/codex-native-hook.js +3559 -451
  450. package/dist/scripts/codex-native-hook.js.map +1 -1
  451. package/dist/scripts/codex-native-pre-post.d.ts +9 -0
  452. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  453. package/dist/scripts/codex-native-pre-post.js +35 -12
  454. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  455. package/dist/scripts/generate-native-release-manifest.js +80 -51
  456. package/dist/scripts/generate-native-release-manifest.js.map +1 -1
  457. package/dist/scripts/notify-fallback-watcher.js +28 -39
  458. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  459. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  460. package/dist/scripts/notify-hook/state-io.js +5 -0
  461. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  462. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  463. package/dist/scripts/notify-hook/team-leader-nudge.js +32 -3
  464. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  465. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  466. package/dist/scripts/notify-hook/team-worker-stop.js +23 -3
  467. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  468. package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
  469. package/dist/scripts/notify-hook/team-worker.js +45 -5
  470. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  471. package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -0
  472. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  473. package/dist/scripts/notify-hook/tmux-injection.js +4 -1
  474. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  475. package/dist/scripts/notify-hook.js +78 -82
  476. package/dist/scripts/notify-hook.js.map +1 -1
  477. package/dist/scripts/postinstall-advisory.d.ts +14 -0
  478. package/dist/scripts/postinstall-advisory.d.ts.map +1 -0
  479. package/dist/scripts/postinstall-advisory.js +49 -0
  480. package/dist/scripts/postinstall-advisory.js.map +1 -0
  481. package/dist/scripts/postinstall.d.ts +1 -1
  482. package/dist/scripts/postinstall.d.ts.map +1 -1
  483. package/dist/scripts/postinstall.js +1 -1
  484. package/dist/scripts/postinstall.js.map +1 -1
  485. package/dist/scripts/smoke-packed-install.d.ts +32 -1
  486. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  487. package/dist/scripts/smoke-packed-install.js +413 -60
  488. package/dist/scripts/smoke-packed-install.js.map +1 -1
  489. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  490. package/dist/scripts/sync-plugin-mirror.js +16 -1
  491. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  492. package/dist/scripts/verify-native-release-assets.js +37 -23
  493. package/dist/scripts/verify-native-release-assets.js.map +1 -1
  494. package/dist/state/__tests__/mode-state-context.test.js +12 -5
  495. package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
  496. package/dist/state/__tests__/operations.test.js +460 -850
  497. package/dist/state/__tests__/operations.test.js.map +1 -1
  498. package/dist/state/__tests__/skill-active.test.js +400 -2
  499. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  500. package/dist/state/mode-state-context.d.ts +3 -0
  501. package/dist/state/mode-state-context.d.ts.map +1 -1
  502. package/dist/state/mode-state-context.js +27 -13
  503. package/dist/state/mode-state-context.js.map +1 -1
  504. package/dist/state/operations.d.ts +3 -0
  505. package/dist/state/operations.d.ts.map +1 -1
  506. package/dist/state/operations.js +144 -40
  507. package/dist/state/operations.js.map +1 -1
  508. package/dist/state/skill-active.d.ts +18 -1
  509. package/dist/state/skill-active.d.ts.map +1 -1
  510. package/dist/state/skill-active.js +420 -23
  511. package/dist/state/skill-active.js.map +1 -1
  512. package/dist/state/workflow-transition-reconcile.d.ts +3 -0
  513. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  514. package/dist/state/workflow-transition-reconcile.js +10 -4
  515. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  516. package/dist/state/workflow-transition.d.ts.map +1 -1
  517. package/dist/state/workflow-transition.js +3 -1
  518. package/dist/state/workflow-transition.js.map +1 -1
  519. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +27 -0
  520. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
  521. package/dist/subagents/__tests__/tracker.test.js +837 -1048
  522. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  523. package/dist/subagents/tracker.d.ts +64 -60
  524. package/dist/subagents/tracker.d.ts.map +1 -1
  525. package/dist/subagents/tracker.js +809 -358
  526. package/dist/subagents/tracker.js.map +1 -1
  527. package/dist/team/__tests__/delivery-e2e-smoke.test.js +6 -7
  528. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  529. package/dist/team/__tests__/mcp-comm.test.js +104 -3
  530. package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
  531. package/dist/team/__tests__/notice-ledger.test.d.ts +2 -0
  532. package/dist/team/__tests__/notice-ledger.test.d.ts.map +1 -0
  533. package/dist/team/__tests__/notice-ledger.test.js +131 -0
  534. package/dist/team/__tests__/notice-ledger.test.js.map +1 -0
  535. package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts +2 -0
  536. package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts.map +1 -0
  537. package/dist/team/__tests__/pane-teardown-residue-e2e.test.js +410 -0
  538. package/dist/team/__tests__/pane-teardown-residue-e2e.test.js.map +1 -0
  539. package/dist/team/__tests__/phase-controller.test.js +20 -0
  540. package/dist/team/__tests__/phase-controller.test.js.map +1 -1
  541. package/dist/team/__tests__/runtime.test.js +725 -59
  542. package/dist/team/__tests__/runtime.test.js.map +1 -1
  543. package/dist/team/__tests__/scaling.test.js +222 -237
  544. package/dist/team/__tests__/scaling.test.js.map +1 -1
  545. package/dist/team/__tests__/state-root.test.js +117 -2
  546. package/dist/team/__tests__/state-root.test.js.map +1 -1
  547. package/dist/team/__tests__/tmux-session.test.js +887 -92
  548. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  549. package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts +2 -0
  550. package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts.map +1 -0
  551. package/dist/team/__tests__/tmux-source-authority-realtmux.test.js +149 -0
  552. package/dist/team/__tests__/tmux-source-authority-realtmux.test.js.map +1 -0
  553. package/dist/team/__tests__/tmux-test-fixture.d.ts +69 -0
  554. package/dist/team/__tests__/tmux-test-fixture.d.ts.map +1 -1
  555. package/dist/team/__tests__/tmux-test-fixture.js +242 -15
  556. package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
  557. package/dist/team/__tests__/tmux-test-fixture.test.js +177 -1
  558. package/dist/team/__tests__/tmux-test-fixture.test.js.map +1 -1
  559. package/dist/team/__tests__/worker-runtime-identity.test.js +45 -72
  560. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  561. package/dist/team/notice-ledger.d.ts +54 -0
  562. package/dist/team/notice-ledger.d.ts.map +1 -0
  563. package/dist/team/notice-ledger.js +259 -0
  564. package/dist/team/notice-ledger.js.map +1 -0
  565. package/dist/team/phase-controller.d.ts.map +1 -1
  566. package/dist/team/phase-controller.js +2 -0
  567. package/dist/team/phase-controller.js.map +1 -1
  568. package/dist/team/runtime.d.ts +3 -1
  569. package/dist/team/runtime.d.ts.map +1 -1
  570. package/dist/team/runtime.js +314 -142
  571. package/dist/team/runtime.js.map +1 -1
  572. package/dist/team/scaling.d.ts.map +1 -1
  573. package/dist/team/scaling.js +161 -91
  574. package/dist/team/scaling.js.map +1 -1
  575. package/dist/team/state/monitor.d.ts +10 -0
  576. package/dist/team/state/monitor.d.ts.map +1 -1
  577. package/dist/team/state/monitor.js +13 -0
  578. package/dist/team/state/monitor.js.map +1 -1
  579. package/dist/team/state/types.d.ts +15 -0
  580. package/dist/team/state/types.d.ts.map +1 -1
  581. package/dist/team/state/types.js.map +1 -1
  582. package/dist/team/state-root.d.ts +12 -0
  583. package/dist/team/state-root.d.ts.map +1 -1
  584. package/dist/team/state-root.js +177 -0
  585. package/dist/team/state-root.js.map +1 -1
  586. package/dist/team/state.d.ts +18 -0
  587. package/dist/team/state.d.ts.map +1 -1
  588. package/dist/team/state.js +36 -0
  589. package/dist/team/state.js.map +1 -1
  590. package/dist/team/tmux-session.d.ts +40 -4
  591. package/dist/team/tmux-session.d.ts.map +1 -1
  592. package/dist/team/tmux-session.js +513 -166
  593. package/dist/team/tmux-session.js.map +1 -1
  594. package/dist/ultragoal/__tests__/artifacts.test.js +950 -2
  595. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  596. package/dist/ultragoal/__tests__/docs-contract.test.js +1 -0
  597. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  598. package/dist/ultragoal/artifacts.d.ts +35 -1
  599. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  600. package/dist/ultragoal/artifacts.js +232 -46
  601. package/dist/ultragoal/artifacts.js.map +1 -1
  602. package/dist/utils/__tests__/file-durability.test.js +58 -1
  603. package/dist/utils/__tests__/file-durability.test.js.map +1 -1
  604. package/dist/utils/__tests__/paths.test.js +21 -1
  605. package/dist/utils/__tests__/paths.test.js.map +1 -1
  606. package/dist/utils/file-durability.d.ts +10 -0
  607. package/dist/utils/file-durability.d.ts.map +1 -1
  608. package/dist/utils/file-durability.js +34 -4
  609. package/dist/utils/file-durability.js.map +1 -1
  610. package/dist/utils/paths.d.ts.map +1 -1
  611. package/dist/utils/paths.js +14 -6
  612. package/dist/utils/paths.js.map +1 -1
  613. package/dist/verification/__tests__/ci-rust-gates.test.js +49 -4
  614. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  615. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +60 -0
  616. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  617. package/dist/verification/__tests__/native-release-manifest.test.js +157 -4
  618. package/dist/verification/__tests__/native-release-manifest.test.js.map +1 -1
  619. package/package.json +13 -5
  620. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  621. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +81 -17
  622. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +13 -5
  623. package/plugins/oh-my-codex/skills/cancel/SKILL.md +30 -85
  624. package/plugins/oh-my-codex/skills/ralph/SKILL.md +5 -5
  625. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +15 -17
  626. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +3 -2
  627. package/skills/autopilot/SKILL.md +13 -5
  628. package/skills/cancel/SKILL.md +30 -85
  629. package/skills/ralph/SKILL.md +5 -5
  630. package/skills/ralplan/SKILL.md +15 -17
  631. package/skills/ultragoal/SKILL.md +3 -2
  632. package/src/scripts/__tests__/codex-native-hook.test.ts +6091 -540
  633. package/src/scripts/__tests__/issue-3293-callsite-parity.test.ts +187 -0
  634. package/src/scripts/__tests__/issue-3293-cancel-transaction.test.ts +189 -0
  635. package/src/scripts/__tests__/issue-3293-hook-owned-cancel.test.ts +187 -0
  636. package/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts +591 -0
  637. package/src/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.ts +701 -0
  638. package/src/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.ts +211 -0
  639. package/src/scripts/__tests__/notify-state-io.test.ts +29 -0
  640. package/src/scripts/__tests__/notify-tmux-injection.test.ts +26 -1
  641. package/src/scripts/__tests__/postinstall.test.ts +23 -1
  642. package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +27 -8
  643. package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +61 -27
  644. package/src/scripts/__tests__/smoke-packed-install.test.ts +209 -18
  645. package/src/scripts/__tests__/verify-native-release-assets.test.ts +240 -0
  646. package/src/scripts/codex-native-hook.ts +3638 -475
  647. package/src/scripts/codex-native-pre-post.ts +43 -12
  648. package/src/scripts/generate-native-release-manifest.ts +83 -79
  649. package/src/scripts/notify-fallback-watcher.ts +38 -38
  650. package/src/scripts/notify-hook/state-io.ts +11 -0
  651. package/src/scripts/notify-hook/team-leader-nudge.ts +32 -4
  652. package/src/scripts/notify-hook/team-worker-stop.ts +23 -4
  653. package/src/scripts/notify-hook/team-worker.ts +43 -5
  654. package/src/scripts/notify-hook/tmux-injection.ts +5 -1
  655. package/src/scripts/notify-hook.ts +83 -82
  656. package/src/scripts/postinstall-advisory.ts +67 -0
  657. package/src/scripts/postinstall.ts +1 -1
  658. package/src/scripts/smoke-packed-install.ts +446 -59
  659. package/src/scripts/sync-plugin-mirror.ts +17 -1
  660. package/src/scripts/verify-native-release-assets.ts +39 -31
  661. package/templates/AGENTS.md +1 -0
@@ -1,15 +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';
3
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { chmod, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
5
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';
11
- import { writeSessionEnd, writeSessionStart } from '../../hooks/session.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';
12
13
  const CLI_SPAWN_TIMEOUT_MS = 60_000;
14
+ const DARWIN_PTY_FINALIZATION_POLL_LIMIT = 600;
13
15
  function buildRunOmxEnv(envOverrides) {
14
16
  const env = { ...process.env };
15
17
  for (const key of Object.keys(env)) {
@@ -51,8 +53,10 @@ function normalizeDarwinTmpPath(value) {
51
53
  function escapeRegExp(value) {
52
54
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
53
55
  }
54
- function shouldSkipForSpawnPermissions(err) {
55
- 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;
56
60
  }
57
61
  async function createGitRepo(wd) {
58
62
  const repo = join(wd, 'repo');
@@ -69,17 +73,167 @@ async function writeExecutable(path, content) {
69
73
  await writeFile(path, content);
70
74
  await chmod(path, 0o755);
71
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
+ }
72
149
  async function createLaunchFixture(wd, tmuxScript) {
73
150
  const home = join(wd, 'home');
74
151
  const fakeBin = join(wd, 'bin');
75
152
  const tmuxLogPath = join(wd, 'tmux.log');
153
+ const leaderDonePath = join(wd, 'leader-done');
76
154
  await mkdir(home, { recursive: true });
77
155
  await mkdir(fakeBin, { recursive: true });
78
- 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');
79
157
  await writeExecutable(join(fakeBin, 'ps'), '#!/bin/sh\nexit 0\n');
80
- 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
+ `);
81
234
  return {
82
235
  tmuxLogPath,
236
+ leaderDonePath,
83
237
  env: {
84
238
  HOME: home,
85
239
  PATH: `${fakeBin}:/usr/bin:/bin`,
@@ -103,8 +257,9 @@ function startHeldOmx(cwd, envOverrides) {
103
257
  stdio: 'inherit',
104
258
  });
105
259
  }
106
- async function waitForPath(path, expectedLines = 1) {
107
- for (let attempt = 0; attempt < 300; attempt += 1) {
260
+ async function waitForPath(path, expectedLines = 1, timeoutMs = 6_000) {
261
+ const deadline = Date.now() + timeoutMs;
262
+ while (Date.now() < deadline) {
108
263
  if (existsSync(path)) {
109
264
  const contents = await readFile(path, 'utf-8').catch(() => '');
110
265
  if (contents.trim().split('\n').filter(Boolean).length >= expectedLines)
@@ -114,6 +269,29 @@ async function waitForPath(path, expectedLines = 1) {
114
269
  }
115
270
  throw new Error(`timed out waiting for ${path}`);
116
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
+ }
117
295
  async function stopHeldOmx(child, releasePath) {
118
296
  await rm(releasePath, { force: true });
119
297
  await new Promise((resolve, reject) => {
@@ -150,6 +328,24 @@ while [ -f "${releasePath}" ]; do sleep 1; done
150
328
  },
151
329
  };
152
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
+ }
153
349
  describe('omx launch fallback when tmux is unavailable', () => {
154
350
  it('surfaces direct Codex startup stderr and preserves the child exit code', async () => {
155
351
  const wd = await mkdtemp(join(tmpdir(), 'omx-launch-child-error-'));
@@ -179,7 +375,7 @@ exit 42
179
375
  assert.match(result.stderr, /\[omx\] codex exited with code 42/);
180
376
  }
181
377
  finally {
182
- await rm(wd, { recursive: true, force: true });
378
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
183
379
  }
184
380
  });
185
381
  it('reports a missing Codex executable instead of exiting silently', async () => {
@@ -206,7 +402,7 @@ exit 42
206
402
  assert.notEqual(result.stderr.trim(), '');
207
403
  }
208
404
  finally {
209
- await rm(wd, { recursive: true, force: true });
405
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
210
406
  }
211
407
  });
212
408
  it('launches codex directly without tmux ENOENT noise', async () => {
@@ -237,7 +433,7 @@ exit 42
237
433
  assert.doesNotMatch(result.stderr, /spawnSync tmux ENOENT/);
238
434
  }
239
435
  finally {
240
- await rm(wd, { recursive: true, force: true });
436
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
241
437
  }
242
438
  });
243
439
  it('passes literal max and ultra after -- with raw config args to Codex launch unchanged', async () => {
@@ -314,7 +510,7 @@ exit 42
314
510
  ]);
315
511
  }
316
512
  finally {
317
- await rm(wd, { recursive: true, force: true });
513
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
318
514
  }
319
515
  });
320
516
  it('treats image values as variadic when detecting resume launches', async () => {
@@ -370,17 +566,22 @@ exec "$NODE_BINARY" -e 'const fs = require("node:fs"); const path = require("nod
370
566
  }
371
567
  }
372
568
  finally {
373
- await rm(wd, { recursive: true, force: true });
569
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
374
570
  }
375
571
  });
376
572
  });
377
573
  describe('ordinary launch root collision guidance', () => {
378
574
  it('keeps the cwd default for the first launch and fails the second and third launches closed with explicit-root guidance', async () => {
379
575
  const wd = await mkdtemp(join(tmpdir(), 'omx-launch-root-conflict-'));
576
+ let binding;
380
577
  try {
381
578
  execFileSync('git', ['init'], { cwd: wd, stdio: 'ignore' });
382
579
  const fixture = await createHeldCodexFixture(wd);
383
- await writeSessionStart(wd, 'first-standard-launch', { pid: process.pid });
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;
384
585
  const second = runOmx(wd, ['--direct', '--version'], fixture.env);
385
586
  const third = runOmx(wd, ['--direct', '--version'], fixture.env);
386
587
  if (shouldSkipForSpawnPermissions(second.error) || shouldSkipForSpawnPermissions(third.error))
@@ -400,10 +601,12 @@ describe('ordinary launch root collision guidance', () => {
400
601
  assert.match(resume.stderr, /session_pointer_owner_conflict/);
401
602
  assert.doesNotMatch(resume.stderr, /concurrent conversations in this checkout require distinct user-specified OMX_ROOT values/);
402
603
  }
403
- await writeSessionEnd(wd, 'first-standard-launch');
604
+ await finalizeBoundOnce(binding, 'test');
404
605
  }
405
606
  finally {
406
- await rm(wd, { recursive: true, force: true });
607
+ if (binding)
608
+ await closeLaunchSessionBindingOnce(binding).catch(() => { });
609
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
407
610
  }
408
611
  });
409
612
  it('keeps explicit roots literal: distinct roots launch independently while a shared root remains fatal without reroute guidance', async () => {
@@ -438,11 +641,13 @@ describe('ordinary launch root collision guidance', () => {
438
641
  first.kill('SIGKILL');
439
642
  if (second)
440
643
  second.kill('SIGKILL');
441
- await rm(wd, { recursive: true, force: true });
644
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
442
645
  }
443
646
  });
444
- it('keeps different checkout defaults independent and relaunches through stale default-pointer evidence', async () => {
647
+ it('keeps different checkout defaults independent and fails closed on stale default-pointer evidence', async () => {
445
648
  const wd = await mkdtemp(join(tmpdir(), 'omx-launch-root-stale-'));
649
+ let firstBinding;
650
+ let secondBinding;
446
651
  try {
447
652
  const firstCheckout = join(wd, 'first-checkout');
448
653
  const secondCheckout = join(wd, 'second-checkout');
@@ -450,23 +655,34 @@ describe('ordinary launch root collision guidance', () => {
450
655
  await mkdir(secondCheckout, { recursive: true });
451
656
  execFileSync('git', ['init'], { cwd: firstCheckout, stdio: 'ignore' });
452
657
  execFileSync('git', ['init'], { cwd: secondCheckout, stdio: 'ignore' });
453
- await writeSessionStart(firstCheckout, 'first-checkout-owner', { pid: process.pid });
454
- await writeSessionStart(secondCheckout, 'second-checkout-owner', { pid: process.pid });
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;
455
666
  assert.match(await readFile(join(firstCheckout, '.omx', 'state', 'session.json'), 'utf-8'), /first-checkout-owner/);
456
667
  assert.match(await readFile(join(secondCheckout, '.omx', 'state', 'session.json'), 'utf-8'), /second-checkout-owner/);
457
- await writeSessionEnd(firstCheckout, 'first-checkout-owner');
458
- await writeSessionEnd(secondCheckout, 'second-checkout-owner');
668
+ await finalizeBoundOnce(firstBinding, 'test');
669
+ await finalizeBoundOnce(secondBinding, 'test');
459
670
  await writeSessionStart(firstCheckout, 'stale-owner', { pid: 2_147_483_647 });
460
671
  const fixture = await createHeldCodexFixture(wd);
461
672
  await rm(fixture.releasePath, { force: true });
462
673
  const relaunch = runOmx(firstCheckout, ['--direct', '--version'], fixture.env);
463
674
  if (shouldSkipForSpawnPermissions(relaunch.error))
464
675
  return;
465
- assert.equal(relaunch.status, 0, relaunch.error || relaunch.stderr || relaunch.stdout);
466
- assert.doesNotMatch(relaunch.stderr, /concurrent conversations in this checkout require distinct user-specified OMX_ROOT values/);
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/);
467
679
  }
468
680
  finally {
469
- await rm(wd, { recursive: true, force: true });
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 });
470
686
  }
471
687
  });
472
688
  });
@@ -504,7 +720,7 @@ printf 'fake-codex:%s\n' "$*"
504
720
  assert.equal(existsSync(join(worktreePath, '.omx')), false);
505
721
  }
506
722
  finally {
507
- await rm(wd, { recursive: true, force: true });
723
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
508
724
  }
509
725
  });
510
726
  it('preserves explicit OMX_ROOT for launch worktree state', async () => {
@@ -540,7 +756,7 @@ printf 'fake-codex:%s\n' "$*"
540
756
  assert.equal(existsSync(join(repo, '.omx')), false);
541
757
  }
542
758
  finally {
543
- await rm(wd, { recursive: true, force: true });
759
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
544
760
  }
545
761
  });
546
762
  it('keeps direct madmax worktree launches bound to the boxed run root', async () => {
@@ -590,7 +806,7 @@ printf 'fake-codex:%s\n' "$*"
590
806
  assert.match(normalizedStdout, /fake-codex-context:[0-9a-f]{32}/);
591
807
  }
592
808
  finally {
593
- await rm(wd, { recursive: true, force: true });
809
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
594
810
  }
595
811
  });
596
812
  });
@@ -649,20 +865,406 @@ exit 0
649
865
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
650
866
  assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
651
867
  assert.match(tmuxLog, /tmux:new-session /);
652
- assert.match(tmuxLog, /tmux:split-window /);
868
+ assert.match(tmuxLog, /tmux:if-shell -F -t %12 .*split-window/);
653
869
  assert.doesNotMatch(tmuxLog, /tmux:attach-session/);
654
870
  assert.doesNotMatch(result.stderr, /failed to attach detached tmux session/);
655
871
  }
656
872
  finally {
657
- await rm(wd, { recursive: true, force: true });
873
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
658
874
  }
659
875
  });
660
876
  });
661
877
  describe('omx launcher when tmux is available', () => {
662
- 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 () => {
663
1257
  const wd = await mkdtemp(join(tmpdir(), 'omx-launch-madmax-reuse-'));
664
1258
  try {
665
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
+ }));
666
1268
  const activeMarker = join(wd, 'active-session');
667
1269
  const instanceMarker = join(wd, 'active-instance');
668
1270
  const { env, tmuxLogPath } = await createLaunchFixture(wd, (logPath) => `#!/bin/sh
@@ -689,6 +1291,12 @@ case "$1" in
689
1291
  printf 'hud-pane\n'
690
1292
  exit 0
691
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
+ ;;
692
1300
  display-message)
693
1301
  if [ "$2" = '-p' ] && [ "$3" = '#{socket_path}' ]; then
694
1302
  printf '/tmp/tmux-test.sock\n'
@@ -724,8 +1332,10 @@ exit 0
724
1332
  const baseEnv = {
725
1333
  ...env,
726
1334
  OMX_RUNS_DIR: runs,
1335
+ OMX_ROOT: madmaxRoot,
727
1336
  OMXBOX_ACTIVE: '1',
728
- OMX_MADMAX_DETACHED_CONTEXT: 'boxed-context-under-test',
1337
+ OMX_SOURCE_CWD: wd,
1338
+ OMX_MADMAX_DETACHED_CONTEXT: madmaxContext,
729
1339
  OMX_LAUNCH_POLICY: 'direct',
730
1340
  TMUX: '',
731
1341
  TMUX_PANE: '',
@@ -738,27 +1348,34 @@ exit 0
738
1348
  if (shouldSkipForSpawnPermissions(second.error))
739
1349
  return;
740
1350
  assert.equal(second.status, 0, second.error || second.stderr || second.stdout);
741
- 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/);
742
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)}`));
743
1355
  assert.equal((tmuxLog.match(/tmux:new-session/g) || []).length, 1);
744
- 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);
745
1358
  assert.equal((tmuxLog.match(/tmux:attach-session/g) || []).length, 2);
746
- const activeRecords = await readFile(join(runs, 'active-detached', 'boxed-context-under-test.json'), 'utf-8');
747
- assert.match(activeRecords, /"tmux_session_name"/);
748
- assert.match(activeRecords, /"session_id"/);
749
- 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"/);
750
1366
  }
751
1367
  finally {
752
- await rm(wd, { recursive: true, force: true });
1368
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
753
1369
  }
754
1370
  });
755
- it('records boxed runtime identity for detached madmax worktree launches', async () => {
1371
+ it('records boxed runtime identity in the leader-owned active record', async () => {
756
1372
  const wd = await mkdtemp(join(tmpdir(), 'omx-launch-madmax-worktree-detached-'));
757
1373
  try {
758
1374
  const repo = await createGitRepo(wd);
759
1375
  const runs = join(wd, 'runs');
760
1376
  const instanceMarker = join(wd, 'active-instance');
761
- 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
762
1379
  printf 'tmux:%s\n' "$*" >> "${logPath}"
763
1380
  case "$1" in
764
1381
  -V)
@@ -769,13 +1386,25 @@ case "$1" in
769
1386
  exit 1
770
1387
  ;;
771
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
772
1394
  printf '%%77\n'
773
1395
  exit 0
774
1396
  ;;
1397
+
775
1398
  split-window)
776
1399
  printf '%%78\n'
777
1400
  exit 0
778
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
+
779
1408
  display-message)
780
1409
  if [ "$2" = '-p' ] && [ "$3" = '#{socket_path}' ]; then
781
1410
  printf '/tmp/tmux-test.sock\n'
@@ -821,19 +1450,17 @@ exit 0
821
1450
  assert.equal(activeFiles.length, 1);
822
1451
  const activeRecord = JSON.parse(await readFile(join(runs, 'active-detached', activeFiles[0]), 'utf-8'));
823
1452
  const worktreePath = join(dirname(repo), `${basename(repo)}.omx-worktrees`, 'launch-detached');
824
- assert.match(activeRecord.run_dir, new RegExp(`^${escapeRegExp(normalizeDarwinTmpPath(runs))}/run-`));
825
- assert.equal(normalizeDarwinTmpPath(activeRecord.source_cwd), normalizeDarwinTmpPath(repo));
826
- assert.equal(normalizeDarwinTmpPath(activeRecord.worktree_cwd), normalizeDarwinTmpPath(worktreePath));
827
- assert.equal(activeRecord.session_id.startsWith('omx-'), true);
828
- 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');
829
1456
  const tmuxLog = normalizeDarwinTmpPath(await readFile(tmuxLogPath, 'utf-8'));
830
- assert.match(tmuxLog, new RegExp(`-e OMX_ROOT=${escapeRegExp(normalizeDarwinTmpPath(activeRecord.run_dir))}`));
1457
+ assert.match(tmuxLog, /__detached-session-leader/);
831
1458
  assert.match(tmuxLog, /-e OMXBOX_ACTIVE=1/);
832
1459
  assert.match(tmuxLog, new RegExp(`-e OMX_SOURCE_CWD=${escapeRegExp(normalizeDarwinTmpPath(repo))}`));
833
- assert.match(tmuxLog, new RegExp(`-e OMX_MADMAX_DETACHED_CONTEXT=${escapeRegExp(activeRecord.context_key)}`));
1460
+ await waitForPath(leaderDonePath);
834
1461
  }
835
1462
  finally {
836
- await rm(wd, { recursive: true, force: true });
1463
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
837
1464
  }
838
1465
  });
839
1466
  it('does not mutate a stale active-detached tmux session without OMX ownership proof', async () => {
@@ -853,7 +1480,7 @@ exit 0
853
1480
  session_id: 'expected-omx-session-id',
854
1481
  tmux_pane_id: '%99',
855
1482
  })}\n`);
856
- const { env, tmuxLogPath } = await createLaunchFixture(wd, (logPath) => `#!/bin/sh
1483
+ const { env, tmuxLogPath, leaderDonePath } = await createLaunchFixture(wd, (logPath) => `#!/bin/sh
857
1484
  printf 'tmux:%s\n' "$*" >> "${logPath}"
858
1485
  case "$1" in
859
1486
  -V)
@@ -907,29 +1534,35 @@ exit 0
907
1534
  if (shouldSkipForSpawnPermissions(result.error))
908
1535
  return;
909
1536
  assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
1537
+ await waitForPath(leaderDonePath);
910
1538
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
911
1539
  assert.doesNotMatch(tmuxLog, /tmux:set-option .* -t user-owned-session .*history-limit/);
912
1540
  assert.doesNotMatch(tmuxLog, /tmux:clear-history .*user-owned-session|tmux:clear-history .*%99/);
913
1541
  assert.match(tmuxLog, /tmux:new-session /);
914
1542
  }
915
1543
  finally {
916
- await rm(wd, { recursive: true, force: true });
1544
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
917
1545
  }
918
1546
  });
919
1547
  it('does not reuse the same active-detached lock for independent --madmax --high launches', async () => {
920
1548
  const wd = await mkdtemp(join(tmpdir(), 'omx-launch-madmax-independent-high-'));
921
1549
  try {
922
1550
  const runs = join(wd, 'runs');
1551
+ const sessionMarker = join(wd, 'independent-session');
1552
+ const instanceMarker = join(wd, 'independent-instance');
923
1553
  const { env, tmuxLogPath } = await createLaunchFixture(wd, (logPath) => `#!/bin/sh
924
1554
  printf 'tmux:%s\n' "$*" >> "${logPath}"
925
1555
  case "$1" in
926
1556
  -V) printf 'tmux 3.4\n'; exit 0 ;;
927
1557
  has-session) exit 1 ;;
928
- 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
+
929
1560
  split-window) printf 'hud-pane\n'; exit 0 ;;
930
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 ;;
931
1563
  show-options) printf 'off\n'; exit 0 ;;
932
- 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 ;;
933
1566
  esac
934
1567
  exit 0
935
1568
  `);
@@ -954,13 +1587,14 @@ exit 0
954
1587
  .map((line) => JSON.parse(line));
955
1588
  assert.equal(registryEntries.length, 2);
956
1589
  assert.notEqual(registryEntries[0].detached_launch_context, registryEntries[1].detached_launch_context, 'independent launches must get distinct active-detached lock identities');
957
- assert.equal(existsSync(join(runs, 'active-detached', `${registryEntries[0].detached_launch_context}.json`)), true);
958
- 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);
959
1593
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
960
1594
  assert.equal((tmuxLog.match(/tmux:new-session/g) || []).length, 2);
961
1595
  }
962
1596
  finally {
963
- await rm(wd, { recursive: true, force: true });
1597
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
964
1598
  }
965
1599
  });
966
1600
  it('allows distinct madmax detached launch contexts to create separate sessions', async () => {
@@ -997,7 +1631,44 @@ exit 0
997
1631
  assert.equal((tmuxLog.match(/tmux:new-session/g) || []).length, 2);
998
1632
  }
999
1633
  finally {
1000
- 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 });
1001
1672
  }
1002
1673
  });
1003
1674
  it('launches --madmax through explicitly requested detached tmux so HUD bootstrap can run', async () => {
@@ -1049,6 +1720,7 @@ esac
1049
1720
  exit 0
1050
1721
  `);
1051
1722
  await chmod(fakeTmuxPath, 0o755);
1723
+ await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
1052
1724
  const result = runOmx(wd, ['--madmax', '--tmux'], {
1053
1725
  HOME: home,
1054
1726
  PATH: `${fakeBin}:/usr/bin:/bin`,
@@ -1065,14 +1737,15 @@ exit 0
1065
1737
  assert.doesNotMatch(tmuxLog, /tmux:show-options -gv history-limit/);
1066
1738
  assert.doesNotMatch(tmuxLog, /tmux:set-option -g[q ]+history-limit/);
1067
1739
  assert.match(tmuxLog, /tmux:new-session .* -s /);
1068
- assert.match(tmuxLog, new RegExp(`tmux:set-option -q -t .* history-limit ${DETACHED_TMUX_HISTORY_LIMIT}`));
1069
- assert.match(tmuxLog, new RegExp(`tmux:set-option -pq -t %12 history-limit ${DETACHED_TMUX_HISTORY_LIMIT}`));
1070
- 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"'/);
1071
- 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`));
1072
1744
  assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
1745
+ await waitForPath(`${fakeTmuxPath}.leader-done`);
1073
1746
  }
1074
1747
  finally {
1075
- await rm(wd, { recursive: true, force: true });
1748
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
1076
1749
  }
1077
1750
  });
1078
1751
  it('preserves parent provider env without replaying terminal state over an OMX-created tmux pane', async () => {
@@ -1124,8 +1797,8 @@ case "$1" in
1124
1797
  TERMINFO=/tmp/server-terminfo \
1125
1798
  TERMINFO_DIRS=/tmp/server-terminfo-dirs \
1126
1799
  TERMCAP=server-termcap \
1127
- sh -c "$last" >/dev/null 2>&1 || true
1128
- printf 'leader-pane\\n'
1800
+ nohup /bin/sh -c "$last" </dev/null >/tmp/omx-test-provider-leader.log 2>&1 &
1801
+ printf '%%12\n'
1129
1802
  exit 0
1130
1803
  ;;
1131
1804
  split-window)
@@ -1150,6 +1823,7 @@ case "$1" in
1150
1823
  esac
1151
1824
  exit 0
1152
1825
  `);
1826
+ await wrapFakeTmuxWithDetachedLeader(join(fakeBin, 'tmux'));
1153
1827
  const result = runOmx(wd, ['--tmux', '--madmax'], {
1154
1828
  HOME: home,
1155
1829
  PATH: `${fakeBin}:/usr/bin:/bin`,
@@ -1173,28 +1847,12 @@ exit 0
1173
1847
  if (shouldSkipForSpawnPermissions(result.error))
1174
1848
  return;
1175
1849
  assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
1176
- assert.equal(await readFile(envLogPath, 'utf-8'), [
1177
- 'custom=fake-provider-key',
1178
- 'marker=1',
1179
- 'term=tmux-256color',
1180
- 'term_program=tmux',
1181
- 'term_program_version=3.4',
1182
- 'colorterm=truecolor',
1183
- 'tmux=/tmp/tmux-test.sock,123,0',
1184
- 'tmux_pane=%12',
1185
- 'columns=211',
1186
- 'lines=77',
1187
- 'terminfo=/tmp/outer-terminfo',
1188
- 'terminfo_dirs=/tmp/outer-terminfo-dirs',
1189
- 'termcap=outer-termcap',
1190
- '',
1191
- ].join('\n'));
1192
1850
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
1193
- assert.doesNotMatch(tmuxLog, /fake-provider-key/);
1194
- 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=/);
1195
1853
  }
1196
1854
  finally {
1197
- await rm(wd, { recursive: true, force: true });
1855
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
1198
1856
  }
1199
1857
  });
1200
1858
  it('launches directly with --direct and skips detached tmux bootstrap', async () => {
@@ -1222,7 +1880,7 @@ exit 0
1222
1880
  assert.doesNotMatch(tmuxLog, /new-session|split-window|attach-session/);
1223
1881
  }
1224
1882
  finally {
1225
- await rm(wd, { recursive: true, force: true });
1883
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
1226
1884
  }
1227
1885
  });
1228
1886
  it('launches directly from OMX_LAUNCH_POLICY=direct and skips detached tmux bootstrap', async () => {
@@ -1246,7 +1904,7 @@ exit 0
1246
1904
  assert.doesNotMatch(tmuxLog, /new-session|split-window|attach-session/);
1247
1905
  }
1248
1906
  finally {
1249
- await rm(wd, { recursive: true, force: true });
1907
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
1250
1908
  }
1251
1909
  });
1252
1910
  it('launches directly inside tmux with --direct and skips HUD/mouse/extended-key tmux calls', async () => {
@@ -1269,35 +1927,68 @@ exit 0
1269
1927
  assert.doesNotMatch(tmuxLog, /split-window|show-options|extended-keys|mouse on/);
1270
1928
  }
1271
1929
  finally {
1272
- await rm(wd, { recursive: true, force: true });
1930
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
1273
1931
  }
1274
1932
  });
1275
- 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 () => {
1276
1934
  const wd = await mkdtemp(join(tmpdir(), 'omx-launch-inside-tmux-managed-'));
1277
1935
  try {
1278
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"
1279
1940
  printf 'tmux:%s\n' "$*" >> "${tmuxLogPath}"
1280
1941
  case "$1" in
1281
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
1282
1954
  exit 0
1283
1955
  ;;
1284
1956
  split-window)
1285
- 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"
1286
1971
  exit 0
1287
1972
  ;;
1288
1973
  display-message)
1289
1974
  case "$*" in
1290
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' ;;
1291
1978
  *'#S'*) printf 'managed-session\n' ;;
1292
1979
  *) printf '0\n' ;;
1293
1980
  esac
1294
1981
  exit 0
1295
1982
  ;;
1296
1983
  show-options)
1297
- printf 'off\n'
1984
+ if [ "$2" = '-g' ] && [ "$3" = '-v' ]; then cat "$proof" 2>/dev/null || true; else printf 'off\n'; fi
1298
1985
  exit 0
1299
1986
  ;;
1300
- 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)
1301
1992
  exit 0
1302
1993
  ;;
1303
1994
  esac
@@ -1313,15 +2004,15 @@ exit 0
1313
2004
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
1314
2005
  assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
1315
2006
  assert.match(result.stdout, /fake-codex:.*--dangerously-bypass-approvals-and-sandbox/);
1316
- 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/);
1317
2008
  assert.match(tmuxLog, /tmux:set-option -t managed-session mouse on/);
1318
2009
  assert.match(tmuxLog, /tmux:set-option -sq extended-keys always/);
1319
2010
  }
1320
2011
  finally {
1321
- await rm(wd, { recursive: true, force: true });
2012
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
1322
2013
  }
1323
2014
  });
1324
- 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 () => {
1325
2016
  const wd = await mkdtemp(join(tmpdir(), 'omx-launch-tmux-missing-socket-'));
1326
2017
  try {
1327
2018
  const home = join(wd, 'home');
@@ -1343,12 +2034,9 @@ case "$1" in
1343
2034
  printf 'tmux 3.6a\n'
1344
2035
  exit 0
1345
2036
  ;;
1346
- list-sessions)
1347
- printf 'error connecting to /private/tmp/tmux-501/default (No such file or directory)\n' >&2
1348
- exit 1
1349
- ;;
2037
+
1350
2038
  new-session)
1351
- printf 'leader-pane\n'
2039
+ printf '%%12\n'
1352
2040
  exit 0
1353
2041
  ;;
1354
2042
  split-window)
@@ -1374,6 +2062,7 @@ esac
1374
2062
  exit 0
1375
2063
  `);
1376
2064
  await chmod(fakeTmuxPath, 0o755);
2065
+ await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
1377
2066
  const result = runOmx(wd, ['--madmax', '--tmux'], {
1378
2067
  HOME: home,
1379
2068
  PATH: `${fakeBin}:/usr/bin:/bin`,
@@ -1387,15 +2076,15 @@ exit 0
1387
2076
  return;
1388
2077
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
1389
2078
  assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
1390
- assert.match(tmuxLog, /tmux:list-sessions/);
2079
+ assert.doesNotMatch(tmuxLog, /tmux:list-sessions/);
1391
2080
  assert.match(tmuxLog, /tmux:new-session .* -s /);
1392
2081
  assert.doesNotMatch(result.stderr, /server\/socket is unusable/);
1393
2082
  }
1394
2083
  finally {
1395
- await rm(wd, { recursive: true, force: true });
2084
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
1396
2085
  }
1397
2086
  });
1398
- 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 () => {
1399
2088
  const wd = await mkdtemp(join(tmpdir(), 'omx-launch-tmux-stale-socket-'));
1400
2089
  try {
1401
2090
  const home = join(wd, 'home');
@@ -1404,6 +2093,7 @@ exit 0
1404
2093
  const fakePsPath = join(fakeBin, 'ps');
1405
2094
  const fakeTmuxPath = join(fakeBin, 'tmux');
1406
2095
  const tmuxLogPath = join(wd, 'tmux.log');
2096
+ const runsPath = join(wd, 'runs');
1407
2097
  await mkdir(home, { recursive: true });
1408
2098
  await mkdir(fakeBin, { recursive: true });
1409
2099
  await writeFile(fakeCodexPath, '#!/bin/sh\nprintf \'fake-codex:%s\\n\' "$*"\n');
@@ -1414,40 +2104,49 @@ exit 0
1414
2104
  printf 'tmux:%s\n' "$*" >> "${tmuxLogPath}"
1415
2105
  case "$1" in
1416
2106
  -V)
2107
+ printf 'tmux:d0-runs:%s\n' "$(test -e "${runsPath}" && printf present || printf missing)" >> "${tmuxLogPath}"
2108
+
1417
2109
  printf 'tmux 3.6a\n'
1418
2110
  exit 0
1419
2111
  ;;
1420
- list-sessions)
1421
- printf 'error connecting to /tmp/tmux-1000/default (Operation not permitted)\n' >&2
2112
+ new-session)
2113
+ printf 'unsafe-tmux-handle-secret\n' >&2
1422
2114
  exit 1
1423
2115
  ;;
2116
+
1424
2117
  esac
1425
2118
  printf 'unexpected tmux command: %s\n' "$*" >&2
1426
2119
  exit 1
1427
2120
  `);
1428
2121
  await chmod(fakeTmuxPath, 0o755);
2122
+ await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
1429
2123
  const result = runOmx(wd, ['--madmax', '--tmux'], {
1430
2124
  HOME: home,
1431
2125
  PATH: `${fakeBin}:/usr/bin:/bin`,
1432
2126
  OMX_AUTO_UPDATE: '0',
1433
2127
  OMX_NOTIFY_FALLBACK: '0',
1434
2128
  OMX_HOOK_DERIVED_SIGNALS: '0',
2129
+ OMX_RUNS_DIR: runsPath,
1435
2130
  TMUX: '',
1436
2131
  TMUX_PANE: '',
1437
2132
  });
1438
2133
  if (shouldSkipForSpawnPermissions(result.error))
1439
2134
  return;
1440
2135
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
1441
- assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
1442
- assert.match(result.stdout, /fake-codex:.*--dangerously-bypass-approvals-and-sandbox/);
1443
- assert.match(result.stderr, /server\/socket is unusable/);
1444
- 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/);
1445
2144
  }
1446
2145
  finally {
1447
- await rm(wd, { recursive: true, force: true });
2146
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
1448
2147
  }
1449
2148
  });
1450
- 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 () => {
1451
2150
  const wd = await mkdtemp(join(tmpdir(), 'omx-launch-tmux-attach-fail-'));
1452
2151
  try {
1453
2152
  const home = join(wd, 'home');
@@ -1469,7 +2168,7 @@ case "$1" in
1469
2168
  exit 0
1470
2169
  ;;
1471
2170
  new-session)
1472
- printf 'leader-pane\n'
2171
+ printf '%%12\n'
1473
2172
  exit 0
1474
2173
  ;;
1475
2174
  split-window)
@@ -1499,6 +2198,7 @@ esac
1499
2198
  exit 0
1500
2199
  `);
1501
2200
  await chmod(fakeTmuxPath, 0o755);
2201
+ await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
1502
2202
  const result = runOmx(wd, ['--madmax', '--tmux'], {
1503
2203
  HOME: home,
1504
2204
  PATH: `${fakeBin}:/usr/bin:/bin`,
@@ -1510,17 +2210,18 @@ exit 0
1510
2210
  });
1511
2211
  if (shouldSkipForSpawnPermissions(result.error))
1512
2212
  return;
2213
+ await waitForPath(`${fakeTmuxPath}.leader-done`);
1513
2214
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
1514
- assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
1515
- 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/);
1516
2217
  assert.match(tmuxLog, /tmux:attach-session -t /);
1517
- assert.match(tmuxLog, /tmux:kill-session -t /);
2218
+ assert.doesNotMatch(tmuxLog, /tmux:kill-session -t /);
1518
2219
  }
1519
2220
  finally {
1520
- await rm(wd, { recursive: true, force: true });
2221
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
1521
2222
  }
1522
2223
  });
1523
- 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 () => {
1524
2225
  const wd = await mkdtemp(join(tmpdir(), 'omx-launch-tmux-attach-noop-'));
1525
2226
  try {
1526
2227
  const { env, tmuxLogPath } = await createLaunchFixture(wd, (tmuxLogPath) => `#!/bin/sh
@@ -1530,7 +2231,7 @@ case "$1" in
1530
2231
  exit 0
1531
2232
  ;;
1532
2233
  new-session)
1533
- printf 'leader-pane\n'
2234
+ printf '%%12\n'
1534
2235
  exit 0
1535
2236
  ;;
1536
2237
  split-window)
@@ -1567,15 +2268,12 @@ exit 0
1567
2268
  return;
1568
2269
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
1569
2270
  assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
1570
- assert.match(result.stdout, /fake-codex:.*--dangerously-bypass-approvals-and-sandbox/);
1571
- assert.match(result.stderr, /attach-session returned immediately without attaching a client/i);
1572
- 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/);
1573
2272
  assert.match(tmuxLog, /tmux:attach-session -t /);
1574
- assert.match(tmuxLog, /tmux:display-message -p -t .* #\{session_attached\}/);
1575
- assert.match(tmuxLog, /tmux:kill-session -t /);
2273
+ assert.doesNotMatch(tmuxLog, /tmux:display-message -p -t .* #\{session_attached\}|tmux:kill-session -t/);
1576
2274
  }
1577
2275
  finally {
1578
- await rm(wd, { recursive: true, force: true });
2276
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
1579
2277
  }
1580
2278
  });
1581
2279
  it('preserves the requested cwd through detached tmux launch when an unsupported SHELL value falls back away from rc-driven cwd drift', async () => {
@@ -1611,11 +2309,7 @@ case "$cmd" in
1611
2309
  exit 0
1612
2310
  ;;
1613
2311
  new-session)
1614
- for last; do :; done
1615
- if [ -n "\${last:-}" ]; then
1616
- /bin/sh -c "$last"
1617
- fi
1618
- printf 'leader-pane\\n'
2312
+ printf '%%12\n'
1619
2313
  exit 0
1620
2314
  ;;
1621
2315
  split-window)
@@ -1641,6 +2335,7 @@ esac
1641
2335
  exit 0
1642
2336
  `);
1643
2337
  await chmod(fakeTmuxPath, 0o755);
2338
+ await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
1644
2339
  const result = runOmx(wd, ['--madmax', '--tmux'], {
1645
2340
  HOME: home,
1646
2341
  SHELL: '/definitely/missing-shell',
@@ -1653,13 +2348,14 @@ exit 0
1653
2348
  });
1654
2349
  if (shouldSkipForSpawnPermissions(result.error))
1655
2350
  return;
1656
- const codexLog = normalizeDarwinTmpPath(await readFile(codexLogPath, 'utf-8'));
1657
- assert.match(codexLog, /codex:.*--dangerously-bypass-approvals-and-sandbox/);
1658
- 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/);
1659
2355
  assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
1660
2356
  }
1661
2357
  finally {
1662
- await rm(wd, { recursive: true, force: true });
2358
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
1663
2359
  }
1664
2360
  });
1665
2361
  it('falls back to /bin/sh for detached tmux launch when SHELL drifts to an unsupported path', async () => {
@@ -1693,11 +2389,7 @@ case "$cmd" in
1693
2389
  exit 0
1694
2390
  ;;
1695
2391
  new-session)
1696
- for last; do :; done
1697
- if [ -n "\${last:-}" ]; then
1698
- /bin/sh -c "$last"
1699
- fi
1700
- printf 'leader-pane\\n'
2392
+ printf '%%12\n'
1701
2393
  exit 0
1702
2394
  ;;
1703
2395
  split-window)
@@ -1723,6 +2415,7 @@ esac
1723
2415
  exit 0
1724
2416
  `);
1725
2417
  await chmod(fakeTmuxPath, 0o755);
2418
+ await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
1726
2419
  const result = runOmx(wd, ['--madmax', '--tmux'], {
1727
2420
  HOME: home,
1728
2421
  SHELL: '/bin/not-a-real-shell',
@@ -1736,15 +2429,199 @@ exit 0
1736
2429
  if (shouldSkipForSpawnPermissions(result.error))
1737
2430
  return;
1738
2431
  const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
1739
- const codexLog = normalizeDarwinTmpPath(await readFile(codexLogPath, 'utf-8'));
1740
2432
  assert.match(tmuxLog, /\/bin\/sh/);
1741
2433
  assert.doesNotMatch(tmuxLog, /not-a-real-shell/);
1742
- assert.match(codexLog, /codex:.*--dangerously-bypass-approvals-and-sandbox/);
1743
- assert.match(codexLog, new RegExp(`codex-pwd:${escapeRegExp(normalizeDarwinTmpPath(wd))}`));
2434
+ assert.match(tmuxLog, /__detached-session-leader/);
1744
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
+ });
1745
2622
  }
1746
2623
  finally {
1747
- await rm(wd, { recursive: true, force: true });
2624
+ await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
1748
2625
  }
1749
2626
  });
1750
2627
  });