oh-my-codex 0.20.2 → 0.20.3

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 (367) hide show
  1. package/Cargo.lock +7 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +26 -0
  4. package/crates/omx-runtime/Cargo.toml +1 -0
  5. package/crates/omx-runtime/src/main.rs +25 -2
  6. package/crates/omx-runtime/tests/execution.rs +219 -0
  7. package/crates/omx-runtime-core/src/dispatch.rs +96 -10
  8. package/crates/omx-runtime-core/src/engine.rs +839 -29
  9. package/crates/omx-runtime-core/src/lib.rs +8 -0
  10. package/dist/agents/__tests__/definitions.test.js +81 -0
  11. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  12. package/dist/agents/__tests__/native-config.test.js +25 -0
  13. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  14. package/dist/agents/native-config.d.ts +2 -1
  15. package/dist/agents/native-config.d.ts.map +1 -1
  16. package/dist/agents/native-config.js.map +1 -1
  17. package/dist/auth/hotswap.d.ts +6 -0
  18. package/dist/auth/hotswap.d.ts.map +1 -1
  19. package/dist/auth/hotswap.js +85 -15
  20. package/dist/auth/hotswap.js.map +1 -1
  21. package/dist/autopilot/__tests__/ralplan-gate.test.js +1 -1
  22. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  23. package/dist/cli/__tests__/auth.test.js +60 -6
  24. package/dist/cli/__tests__/auth.test.js.map +1 -1
  25. package/dist/cli/__tests__/codex-plugin-layout.test.js +80 -0
  26. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  27. package/dist/cli/__tests__/doctor-warning-copy.test.js +103 -1
  28. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  29. package/dist/cli/__tests__/exec.test.js +136 -0
  30. package/dist/cli/__tests__/exec.test.js.map +1 -1
  31. package/dist/cli/__tests__/index.test.js +258 -4
  32. package/dist/cli/__tests__/index.test.js.map +1 -1
  33. package/dist/cli/__tests__/launch-fallback.test.js +287 -1
  34. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  35. package/dist/cli/__tests__/mcp-parity.test.js +4 -1
  36. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
  37. package/dist/cli/__tests__/native-hook-claim-journal.test.js +84 -6
  38. package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
  39. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +3 -3
  40. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  41. package/dist/cli/__tests__/ralph.test.js +3 -3
  42. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  43. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +56 -171
  44. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
  45. package/dist/cli/__tests__/ralplan.test.js +37 -111
  46. package/dist/cli/__tests__/ralplan.test.js.map +1 -1
  47. package/dist/cli/__tests__/resume.test.js +11 -10
  48. package/dist/cli/__tests__/resume.test.js.map +1 -1
  49. package/dist/cli/__tests__/session-search-help.test.js +25 -1
  50. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  51. package/dist/cli/__tests__/setup-install-mode.test.js +42 -4
  52. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  53. package/dist/cli/__tests__/team.test.js +105 -7
  54. package/dist/cli/__tests__/team.test.js.map +1 -1
  55. package/dist/cli/__tests__/uninstall.test.js +46 -0
  56. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  57. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +2 -1
  58. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  59. package/dist/cli/doctor.d.ts +3 -0
  60. package/dist/cli/doctor.d.ts.map +1 -1
  61. package/dist/cli/doctor.js +15 -2
  62. package/dist/cli/doctor.js.map +1 -1
  63. package/dist/cli/index.d.ts +4 -1
  64. package/dist/cli/index.d.ts.map +1 -1
  65. package/dist/cli/index.js +138 -38
  66. package/dist/cli/index.js.map +1 -1
  67. package/dist/cli/mcp-parity.js +2 -2
  68. package/dist/cli/mcp-parity.js.map +1 -1
  69. package/dist/cli/native-hook-claim-journal.d.ts +16 -5
  70. package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
  71. package/dist/cli/native-hook-claim-journal.js +42 -29
  72. package/dist/cli/native-hook-claim-journal.js.map +1 -1
  73. package/dist/cli/ralph.js +2 -2
  74. package/dist/cli/ralph.js.map +1 -1
  75. package/dist/cli/ralplan.d.ts +4 -8
  76. package/dist/cli/ralplan.d.ts.map +1 -1
  77. package/dist/cli/ralplan.js +19 -112
  78. package/dist/cli/ralplan.js.map +1 -1
  79. package/dist/cli/session-search.d.ts +5 -0
  80. package/dist/cli/session-search.d.ts.map +1 -1
  81. package/dist/cli/session-search.js +87 -0
  82. package/dist/cli/session-search.js.map +1 -1
  83. package/dist/cli/setup.d.ts +5 -0
  84. package/dist/cli/setup.d.ts.map +1 -1
  85. package/dist/cli/setup.js +80 -31
  86. package/dist/cli/setup.js.map +1 -1
  87. package/dist/cli/uninstall.d.ts +9 -0
  88. package/dist/cli/uninstall.d.ts.map +1 -1
  89. package/dist/cli/uninstall.js +61 -19
  90. package/dist/cli/uninstall.js.map +1 -1
  91. package/dist/compat/__tests__/rust-runtime-compat.test.js +1 -1
  92. package/dist/config/__tests__/generator-idempotent.test.js +330 -1
  93. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  94. package/dist/config/__tests__/generator-notify.test.js +2 -2
  95. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  96. package/dist/config/__tests__/models.test.js +67 -3
  97. package/dist/config/__tests__/models.test.js.map +1 -1
  98. package/dist/config/generator.d.ts +4 -20
  99. package/dist/config/generator.d.ts.map +1 -1
  100. package/dist/config/generator.js +499 -119
  101. package/dist/config/generator.js.map +1 -1
  102. package/dist/config/models.d.ts +12 -2
  103. package/dist/config/models.d.ts.map +1 -1
  104. package/dist/config/models.js +15 -1
  105. package/dist/config/models.js.map +1 -1
  106. package/dist/hooks/__tests__/agents-overlay.test.js +2 -2
  107. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  108. package/dist/hooks/__tests__/keyword-detector.test.js +4 -4
  109. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  110. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +238 -543
  111. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  112. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +38 -4
  113. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  114. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +96 -9
  115. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  116. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +531 -12
  117. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  118. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +155 -23
  119. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  120. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +482 -32
  121. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  122. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +68 -1
  123. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  124. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +7 -1
  125. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
  126. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +43 -4
  127. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  128. package/dist/hooks/__tests__/session.test.js +348 -2
  129. package/dist/hooks/__tests__/session.test.js.map +1 -1
  130. package/dist/hooks/agents-overlay.js +1 -1
  131. package/dist/hooks/session.d.ts +26 -2
  132. package/dist/hooks/session.d.ts.map +1 -1
  133. package/dist/hooks/session.js +169 -13
  134. package/dist/hooks/session.js.map +1 -1
  135. package/dist/leader/__tests__/contract.test.js +57 -7
  136. package/dist/leader/__tests__/contract.test.js.map +1 -1
  137. package/dist/leader/contract.d.ts +16 -0
  138. package/dist/leader/contract.d.ts.map +1 -1
  139. package/dist/leader/contract.js +79 -4
  140. package/dist/leader/contract.js.map +1 -1
  141. package/dist/mcp/__tests__/code-intel-server.test.js +31 -3
  142. package/dist/mcp/__tests__/code-intel-server.test.js.map +1 -1
  143. package/dist/mcp/__tests__/wiki-server.test.js +18 -0
  144. package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
  145. package/dist/mcp/code-intel-server.d.ts.map +1 -1
  146. package/dist/mcp/code-intel-server.js +13 -3
  147. package/dist/mcp/code-intel-server.js.map +1 -1
  148. package/dist/mcp/wiki-server.d.ts +11 -0
  149. package/dist/mcp/wiki-server.d.ts.map +1 -1
  150. package/dist/mcp/wiki-server.js +9 -2
  151. package/dist/mcp/wiki-server.js.map +1 -1
  152. package/dist/modes/__tests__/base-tmux-pane.test.js +4 -4
  153. package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
  154. package/dist/notifications/temp-contract.d.ts.map +1 -1
  155. package/dist/notifications/temp-contract.js +4 -0
  156. package/dist/notifications/temp-contract.js.map +1 -1
  157. package/dist/pipeline/__tests__/orchestrator.test.js +4 -4
  158. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  159. package/dist/pipeline/__tests__/stages.test.js +8 -8
  160. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  161. package/dist/ralplan/__tests__/consensus-gate.test.js +134 -10
  162. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
  163. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts +2 -0
  164. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts.map +1 -0
  165. package/dist/ralplan/__tests__/documented-leader-preflight.test.js +46 -0
  166. package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -0
  167. package/dist/ralplan/__tests__/runtime.test.js +8 -4
  168. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  169. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  170. package/dist/ralplan/consensus-gate.js +58 -50
  171. package/dist/ralplan/consensus-gate.js.map +1 -1
  172. package/dist/ralplan/documented-leader-preflight.d.ts +34 -0
  173. package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -0
  174. package/dist/ralplan/documented-leader-preflight.js +58 -0
  175. package/dist/ralplan/documented-leader-preflight.js.map +1 -0
  176. package/dist/ralplan/runtime.d.ts +1 -0
  177. package/dist/ralplan/runtime.d.ts.map +1 -1
  178. package/dist/ralplan/runtime.js +11 -7
  179. package/dist/ralplan/runtime.js.map +1 -1
  180. package/dist/runtime/__tests__/bridge.test.js +42 -1
  181. package/dist/runtime/__tests__/bridge.test.js.map +1 -1
  182. package/dist/runtime/bridge.d.ts +12 -0
  183. package/dist/runtime/bridge.d.ts.map +1 -1
  184. package/dist/runtime/bridge.js +52 -1
  185. package/dist/runtime/bridge.js.map +1 -1
  186. package/dist/scripts/__tests__/codex-native-hook.test.js +2620 -487
  187. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  188. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts +2 -0
  189. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts.map +1 -0
  190. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +108 -0
  191. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -0
  192. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +46 -173
  193. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
  194. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +39 -159
  195. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
  196. package/dist/scripts/__tests__/smoke-packed-install.test.js +254 -2
  197. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  198. package/dist/scripts/codex-native-hook.d.ts +4 -1
  199. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  200. package/dist/scripts/codex-native-hook.js +11013 -1153
  201. package/dist/scripts/codex-native-hook.js.map +1 -1
  202. package/dist/scripts/notify-fallback-watcher.js +82 -108
  203. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  204. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  205. package/dist/scripts/notify-hook/auto-nudge.js +74 -3
  206. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  207. package/dist/scripts/notify-hook/team-dispatch.d.ts +9 -0
  208. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  209. package/dist/scripts/notify-hook/team-dispatch.js +234 -79
  210. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  211. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  212. package/dist/scripts/notify-hook/team-leader-nudge.js +39 -13
  213. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  214. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +12 -4
  215. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  216. package/dist/scripts/notify-hook/team-tmux-guard.js +365 -44
  217. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  218. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  219. package/dist/scripts/notify-hook/team-worker-stop.js +19 -18
  220. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  221. package/dist/scripts/notify-hook/team-worker.d.ts +3 -0
  222. package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
  223. package/dist/scripts/notify-hook/team-worker.js +37 -28
  224. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  225. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  226. package/dist/scripts/notify-hook/tmux-injection.js +28 -2
  227. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  228. package/dist/scripts/notify-hook.js +1 -0
  229. package/dist/scripts/notify-hook.js.map +1 -1
  230. package/dist/scripts/smoke-packed-install.d.ts +48 -3
  231. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  232. package/dist/scripts/smoke-packed-install.js +2796 -13
  233. package/dist/scripts/smoke-packed-install.js.map +1 -1
  234. package/dist/state/__tests__/mode-state-context.test.js +54 -0
  235. package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
  236. package/dist/state/__tests__/operations.test.js +2 -0
  237. package/dist/state/__tests__/operations.test.js.map +1 -1
  238. package/dist/state/mode-state-context.d.ts +5 -0
  239. package/dist/state/mode-state-context.d.ts.map +1 -1
  240. package/dist/state/mode-state-context.js +59 -0
  241. package/dist/state/mode-state-context.js.map +1 -1
  242. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +24 -245
  243. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
  244. package/dist/subagents/tracker.d.ts +1 -82
  245. package/dist/subagents/tracker.d.ts.map +1 -1
  246. package/dist/subagents/tracker.js +3 -275
  247. package/dist/subagents/tracker.js.map +1 -1
  248. package/dist/team/__tests__/api-interop.test.js +91 -1
  249. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  250. package/dist/team/__tests__/delivery-e2e-smoke.test.js +52 -10
  251. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  252. package/dist/team/__tests__/idle-nudge.test.js +45 -15
  253. package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
  254. package/dist/team/__tests__/mcp-comm.test.js +93 -27
  255. package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
  256. package/dist/team/__tests__/model-contract.test.js +110 -3
  257. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  258. package/dist/team/__tests__/runtime-cli.test.js +9 -0
  259. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  260. package/dist/team/__tests__/runtime.test.js +3204 -335
  261. package/dist/team/__tests__/runtime.test.js.map +1 -1
  262. package/dist/team/__tests__/scaling.test.js +2359 -142
  263. package/dist/team/__tests__/scaling.test.js.map +1 -1
  264. package/dist/team/__tests__/state.test.js +804 -36
  265. package/dist/team/__tests__/state.test.js.map +1 -1
  266. package/dist/team/__tests__/team-ops-contract.test.js +6 -0
  267. package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
  268. package/dist/team/__tests__/tmux-session.test.js +3049 -784
  269. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  270. package/dist/team/__tests__/worker-runtime-identity.test.js +38 -2
  271. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  272. package/dist/team/__tests__/worktree.test.js +6 -0
  273. package/dist/team/__tests__/worktree.test.js.map +1 -1
  274. package/dist/team/api-interop.d.ts.map +1 -1
  275. package/dist/team/api-interop.js +15 -9
  276. package/dist/team/api-interop.js.map +1 -1
  277. package/dist/team/exact-pane.d.ts +23 -0
  278. package/dist/team/exact-pane.d.ts.map +1 -0
  279. package/dist/team/exact-pane.js +133 -0
  280. package/dist/team/exact-pane.js.map +1 -0
  281. package/dist/team/idle-nudge.d.ts +12 -1
  282. package/dist/team/idle-nudge.d.ts.map +1 -1
  283. package/dist/team/idle-nudge.js +41 -14
  284. package/dist/team/idle-nudge.js.map +1 -1
  285. package/dist/team/model-contract.d.ts +2 -5
  286. package/dist/team/model-contract.d.ts.map +1 -1
  287. package/dist/team/model-contract.js +59 -17
  288. package/dist/team/model-contract.js.map +1 -1
  289. package/dist/team/runtime.d.ts +1 -0
  290. package/dist/team/runtime.d.ts.map +1 -1
  291. package/dist/team/runtime.js +1244 -180
  292. package/dist/team/runtime.js.map +1 -1
  293. package/dist/team/scaling.d.ts +13 -1
  294. package/dist/team/scaling.d.ts.map +1 -1
  295. package/dist/team/scaling.js +1430 -384
  296. package/dist/team/scaling.js.map +1 -1
  297. package/dist/team/state/dispatch.d.ts.map +1 -1
  298. package/dist/team/state/dispatch.js +26 -8
  299. package/dist/team/state/dispatch.js.map +1 -1
  300. package/dist/team/state/locks.d.ts.map +1 -1
  301. package/dist/team/state/locks.js +61 -13
  302. package/dist/team/state/locks.js.map +1 -1
  303. package/dist/team/state/tasks.d.ts.map +1 -1
  304. package/dist/team/state/tasks.js +4 -3
  305. package/dist/team/state/tasks.js.map +1 -1
  306. package/dist/team/state.d.ts +67 -1
  307. package/dist/team/state.d.ts.map +1 -1
  308. package/dist/team/state.js +636 -129
  309. package/dist/team/state.js.map +1 -1
  310. package/dist/team/team-ops.d.ts +5 -0
  311. package/dist/team/team-ops.d.ts.map +1 -1
  312. package/dist/team/team-ops.js +4 -0
  313. package/dist/team/team-ops.js.map +1 -1
  314. package/dist/team/tmux-session.d.ts +123 -23
  315. package/dist/team/tmux-session.d.ts.map +1 -1
  316. package/dist/team/tmux-session.js +1250 -291
  317. package/dist/team/tmux-session.js.map +1 -1
  318. package/dist/team/worktree.d.ts.map +1 -1
  319. package/dist/team/worktree.js +4 -0
  320. package/dist/team/worktree.js.map +1 -1
  321. package/dist/utils/__tests__/file-durability.test.d.ts +2 -0
  322. package/dist/utils/__tests__/file-durability.test.d.ts.map +1 -0
  323. package/dist/utils/__tests__/file-durability.test.js +57 -0
  324. package/dist/utils/__tests__/file-durability.test.js.map +1 -0
  325. package/dist/utils/file-durability.d.ts +15 -0
  326. package/dist/utils/file-durability.d.ts.map +1 -0
  327. package/dist/utils/file-durability.js +37 -0
  328. package/dist/utils/file-durability.js.map +1 -0
  329. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +3 -0
  330. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
  331. package/dist/wiki/lint.d.ts +4 -1
  332. package/dist/wiki/lint.d.ts.map +1 -1
  333. package/dist/wiki/lint.js +3 -2
  334. package/dist/wiki/lint.js.map +1 -1
  335. package/package.json +1 -1
  336. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  337. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +24 -0
  338. package/plugins/oh-my-codex/skills/ralph/SKILL.md +6 -4
  339. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +3 -2
  340. package/plugins/oh-my-codex/skills/team/SKILL.md +13 -9
  341. package/skills/ralph/SKILL.md +6 -4
  342. package/skills/ralplan/SKILL.md +3 -2
  343. package/skills/team/SKILL.md +13 -9
  344. package/src/scripts/__tests__/codex-native-hook.test.ts +2826 -497
  345. package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +129 -0
  346. package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +48 -185
  347. package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +44 -177
  348. package/src/scripts/__tests__/smoke-packed-install.test.ts +320 -2
  349. package/src/scripts/codex-native-hook.ts +10715 -1069
  350. package/src/scripts/notify-fallback-watcher.ts +84 -115
  351. package/src/scripts/notify-hook/auto-nudge.ts +71 -3
  352. package/src/scripts/notify-hook/team-dispatch.ts +242 -75
  353. package/src/scripts/notify-hook/team-leader-nudge.ts +40 -18
  354. package/src/scripts/notify-hook/team-tmux-guard.ts +366 -39
  355. package/src/scripts/notify-hook/team-worker-stop.ts +20 -16
  356. package/src/scripts/notify-hook/team-worker.ts +39 -26
  357. package/src/scripts/notify-hook/tmux-injection.ts +28 -2
  358. package/src/scripts/notify-hook.ts +1 -0
  359. package/src/scripts/smoke-packed-install.ts +3196 -14
  360. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +0 -2
  361. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +0 -1
  362. package/dist/subagents/__tests__/adapted-role-binding.test.js +0 -988
  363. package/dist/subagents/__tests__/adapted-role-binding.test.js.map +0 -1
  364. package/dist/subagents/adapted-role-binding.d.ts +0 -17
  365. package/dist/subagents/adapted-role-binding.d.ts.map +0 -1
  366. package/dist/subagents/adapted-role-binding.js +0 -75
  367. package/dist/subagents/adapted-role-binding.js.map +0 -1
@@ -4,13 +4,14 @@ import fs from 'node:fs';
4
4
  import { spawnSync } from 'node:child_process';
5
5
  import { syncBuiltinESMExports } from 'node:module';
6
6
  import { PassThrough } from 'node:stream';
7
- import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'fs/promises';
7
+ import { chmod, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'fs/promises';
8
8
  import { dirname, join } from 'path';
9
9
  import { tmpdir } from 'os';
10
- import { buildClientAttachedReconcileHookName, assertTeamWorkerCliBinaryAvailable, buildWorkerProcessLaunchSpec, buildReconcileHudResizeArgs, buildRegisterClientAttachedReconcileArgs, buildRegisterResizeHookArgs, buildResizeHookName, buildResizeHookTarget, buildScheduleDelayedHudResizeArgs, buildUnregisterClientAttachedReconcileArgs, buildUnregisterResizeHookArgs, buildWorkerStartupCommand, trustWorkerMiseConfigIfAvailable, writeWorkerStartupScriptCommand, shouldSourceTeamWorkerShellRc, buildHudPaneTarget, chooseTeamLeaderPaneId, createTeamSession, enableMouseScrolling, isMsysOrGitBash, isNativeWindows, isTmuxAvailable, isWorkerPaneOpen, restoreStandaloneHudPane, translatePathForMsys, isWsl2, isWorkerAlive, killWorker, killWorkerByPaneId, teardownWorkerPanes, listTeamSessions, resolveTeamWorkerCli, resolveTeamWorkerLaunchMode, resolveWorkerCliForSend, resolveTeamWorkerCliPlan, buildWorkerSubmitPlan, sanitizeTeamName, shouldAttemptAdaptiveRetry, sendToWorker, sendToWorkerStdin, sleepFractionalSeconds, translateWorkerLaunchArgsForCli, waitForWorkerReady, waitForWorkerReadyAsync, paneIsBootstrapping, classifyWorkerStartupInjectSafety, checkWorkerStartupInjectSafety, dismissTrustPromptIfPresent, evaluateStartupDirectTriggerSafetyCapture, mitigateCopyModeUnderlineArtifacts, } from '../tmux-session.js';
10
+ import { buildClientAttachedReconcileHookName, assertTeamWorkerCliBinaryAvailable, buildWorkerProcessLaunchSpec, buildReconcileHudResizeArgs, buildRegisterClientAttachedReconcileArgs, buildRegisterResizeHookArgs, buildResizeHookName, buildResizeHookTarget, buildScheduleDelayedHudResizeArgs, buildUnregisterClientAttachedReconcileArgs, buildUnregisterResizeHookArgs, buildWorkerStartupCommand, trustWorkerMiseConfigIfAvailable, writeWorkerStartupScriptCommand, shouldSourceTeamWorkerShellRc, buildHudPaneTarget, chooseTeamLeaderPaneId, createTeamSession, CreateTeamSessionPartialError, enableMouseScrolling, isMsysOrGitBash, isNativeWindows, isTmuxAvailable, isWorkerPaneOpen, restoreStandaloneHudPane, finalizeRestoredHudCleanupDebtSync, reconcileRestoredHudCleanupDebtSync, translatePathForMsys, isWsl2, isWorkerAlive, killWorker, getWorkerPanePid, killWorkerByPaneId, killWorkerByPaneIdAsync, teardownWorkerPanes, listTeamSessions, destroyTeamSession, queryDetachedTeamSession, requestDetachedTeamSessionDestroy, resolveTeamWorkerCli, resolveTeamWorkerLaunchMode, resolveWorkerCliForSend, resolveTeamWorkerCliPlan, buildWorkerSubmitPlan, sanitizeTeamName, shouldAttemptAdaptiveRetry, sendToWorker, sendToWorkerStdin, sleepFractionalSeconds, translateWorkerLaunchArgsForCli, waitForWorkerReady, waitForWorkerReadyAsync, paneIsBootstrapping, classifyWorkerStartupInjectSafety, checkWorkerStartupInjectSafety, dismissTrustPromptIfPresent, evaluateStartupDirectTriggerSafetyCapture, mitigateCopyModeUnderlineArtifacts, listPaneIds, } from '../tmux-session.js';
11
11
  import { HUD_RESIZE_RECONCILE_DELAY_SECONDS, HUD_TMUX_TEAM_HEIGHT_LINES } from '../../hud/constants.js';
12
12
  import * as tmuxSessionModule from '../tmux-session.js';
13
13
  import { OMX_ENTRY_PATH_ENV, OMX_STARTUP_CWD_ENV } from '../../utils/paths.js';
14
+ import { readExactPaneProof, readExactPaneProofSync } from '../exact-pane.js';
14
15
  const fsMutable = fs;
15
16
  function withEmptyPath(fn) {
16
17
  const prev = process.env.PATH;
@@ -49,6 +50,18 @@ function withMockedStatSync(mock, fn) {
49
50
  syncBuiltinESMExports();
50
51
  }
51
52
  }
53
+ function withMockedFsyncSync(mock, fn) {
54
+ const original = fsMutable.fsyncSync;
55
+ fsMutable.fsyncSync = mock;
56
+ syncBuiltinESMExports();
57
+ try {
58
+ return fn();
59
+ }
60
+ finally {
61
+ fsMutable.fsyncSync = original;
62
+ syncBuiltinESMExports();
63
+ }
64
+ }
52
65
  function escapeRegExp(value) {
53
66
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
54
67
  }
@@ -87,7 +100,33 @@ async function withMockTmuxFixture(dirPrefix, tmuxScript, run) {
87
100
  const tmuxStubPath = join(fakeBinDir, 'tmux');
88
101
  const previousPath = process.env.PATH;
89
102
  try {
90
- await writeFile(tmuxStubPath, tmuxScript(logPath));
103
+ const fixtureScript = tmuxScript(logPath);
104
+ const needsStandaloneGlobalProof = dirPrefix.includes('standalone') && !fixtureScript.includes('list-panes)');
105
+ const needsTeamOwnerState = !fixtureScript.includes('show-option');
106
+ if (needsStandaloneGlobalProof || needsTeamOwnerState) {
107
+ const fixturePath = `${tmuxStubPath}.fixture`;
108
+ const ownerStateDir = `${tmuxStubPath}.team-owner-state`;
109
+ await writeFile(fixturePath, fixtureScript);
110
+ await chmod(fixturePath, 0o755);
111
+ await writeFile(tmuxStubPath, `#!/bin/sh
112
+ if [ "${'$'}1" = "show-option" ] && [ "${'$'}{2:-}" = "-qv" ] && [ "${'$'}{3:-}" = "-p" ] && [ "${'$'}{4:-}" = "-t" ] && [ "${'$'}{6:-}" = "@omx_team_pane_owner_id" ]; then
113
+ if [ -f "${ownerStateDir}/${'$'}5" ]; then cat "${ownerStateDir}/${'$'}5"; exit 0; fi
114
+ exit 1
115
+ fi
116
+ if [ "${'$'}1" = "set-option" ] && [ "${'$'}{2:-}" = "-p" ] && [ "${'$'}{3:-}" = "-t" ] && [ "${'$'}{5:-}" = "@omx_team_pane_owner_id" ]; then
117
+ mkdir -p "${ownerStateDir}"
118
+ printf '%s' "${'$'}6" > "${ownerStateDir}/${'$'}4"
119
+ fi
120
+ ${needsStandaloneGlobalProof ? `if [ "${'$'}1" = "list-panes" ] && [ "${'$'}{2:-}" = "-a" ]; then
121
+ printf '%%11\\t0\\t2000000011\\n%%44\\t0\\t2000000044\\n'
122
+ exit 0
123
+ fi
124
+ ` : ''}exec "${fixturePath}" "${'$'}@"
125
+ `);
126
+ }
127
+ else {
128
+ await writeFile(tmuxStubPath, fixtureScript);
129
+ }
91
130
  await chmod(tmuxStubPath, 0o755);
92
131
  process.env.PATH = `${fakeBinDir}:${previousPath ?? ''}`;
93
132
  return await run({ logPath });
@@ -153,7 +192,9 @@ describe('HUD resize hook command builders', () => {
153
192
  assert.equal(args[1], '-t');
154
193
  assert.equal(args[2], 'my-session:0');
155
194
  assert.match(args[3] ?? '', /^client-resized\[\d+\]$/);
156
- assert.equal(args[4], `run-shell -b 'tmux resize-pane -t %1 -y ${HUD_TMUX_TEAM_HEIGHT_LINES} >/dev/null 2>&1 || true; sleep ${HUD_RESIZE_RECONCILE_DELAY_SECONDS}; tmux resize-pane -t %1 -y ${HUD_TMUX_TEAM_HEIGHT_LINES} >/dev/null 2>&1 || true'`);
195
+ assert.match(args[4] ?? '', /list-panes -a -F/);
196
+ assert.match(args[4] ?? '', /awk -F/);
197
+ assert.match(args[4] ?? '', new RegExp(`resize-pane -t %1 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
157
198
  });
158
199
  it('buildUnregisterResizeHookArgs removes the exact numeric hook slot', () => {
159
200
  const registered = buildRegisterResizeHookArgs('my-session:0', 'omx_resize_team_session_0_1', '%1');
@@ -170,7 +211,46 @@ describe('HUD resize hook command builders', () => {
170
211
  assert.equal(args[1], '-t');
171
212
  assert.equal(args[2], 'my-session:0');
172
213
  assert.match(args[3] ?? '', /^client-attached\[\d+\]$/);
173
- assert.match(args[4] ?? '', /^run-shell -b 'tmux resize-pane -t %1 -y \d+ >\/dev\/null 2>&1 \|\| true; tmux set-hook -u -t my-session:0 client-attached\[\d+\]'$/);
214
+ assert.match(args[4] ?? '', /list-panes -a -F/);
215
+ assert.match(args[4] ?? '', /awk -F/);
216
+ assert.match(args[4] ?? '', /resize-pane -t %1 -y \d+/);
217
+ assert.match(args[4] ?? '', /set-hook -u -t my-session:0 client-attached\[\d+\]/);
218
+ });
219
+ it('pins Team hook and delayed HUD reconciliation to the created pane PID and owner', () => {
220
+ const expectedPid = 2000000123;
221
+ const expectedOwner = 'team:exact-owner';
222
+ const commands = [
223
+ buildRegisterResizeHookArgs('my-session:0', 'omx_resize_team_session_0_1', '%1', HUD_TMUX_TEAM_HEIGHT_LINES, expectedPid, expectedOwner),
224
+ buildRegisterClientAttachedReconcileArgs('my-session:0', 'omx_attached_team_session_0_1', '%1', HUD_TMUX_TEAM_HEIGHT_LINES, expectedPid, expectedOwner),
225
+ buildScheduleDelayedHudResizeArgs('%1', HUD_RESIZE_RECONCILE_DELAY_SECONDS, HUD_TMUX_TEAM_HEIGHT_LINES, expectedPid, expectedOwner),
226
+ buildReconcileHudResizeArgs('%1', HUD_TMUX_TEAM_HEIGHT_LINES, expectedPid, expectedOwner),
227
+ ];
228
+ for (const args of commands) {
229
+ const command = args.at(-1) ?? '';
230
+ assert.match(command, new RegExp(`\\$3 == "${expectedPid}"`));
231
+ assert.match(command, /show-option -qv -p -t %1 @omx_team_pane_owner_id/);
232
+ assert.match(command, /final_snapshot=/);
233
+ assert.match(command, /team:exact-owner/);
234
+ }
235
+ });
236
+ it('fails closed when a Team HUD hook observes an owner change between PID proofs', async () => {
237
+ await withMockTmuxFixture('omx-hud-hook-owner-change-', (logPath) => `#!/bin/sh
238
+ set -eu
239
+ printf '%s\\n' "$*" >> "${logPath}"
240
+ case "$1" in
241
+ list-panes) printf '%%7\\t0\\t2000000007\\n' ;;
242
+ show-option) printf '%s\\n' 'team:foreign' ;;
243
+ resize-pane) exit 0 ;;
244
+ esac
245
+ `, async ({ logPath }) => {
246
+ const command = buildReconcileHudResizeArgs('%7', HUD_TMUX_TEAM_HEIGHT_LINES, 2000000007, 'team:expected')[1] ?? '';
247
+ const result = spawnSync('/bin/sh', ['-c', command], { encoding: 'utf-8' });
248
+ assert.equal(result.status, 0);
249
+ const commands = await readFile(logPath, 'utf-8');
250
+ assert.match(commands, /list-panes -a -F/);
251
+ assert.match(commands, /show-option -qv -p -t %7 @omx_team_pane_owner_id/);
252
+ assert.doesNotMatch(commands, /resize-pane/);
253
+ });
174
254
  });
175
255
  it('buildUnregisterClientAttachedReconcileArgs removes the exact numeric client-attached slot', () => {
176
256
  const registered = buildRegisterClientAttachedReconcileArgs('my-session:0', 'omx_attached_team_session_0_1', '%1');
@@ -201,13 +281,100 @@ describe('HUD resize hook command builders', () => {
201
281
  const d = buildRegisterClientAttachedReconcileArgs('s:0', name, '%1');
202
282
  assert.equal(c[3], d[3]);
203
283
  });
204
- it('buildScheduleDelayedHudResizeArgs schedules tmux-side delayed reconcile', () => {
205
- assert.deepEqual(buildScheduleDelayedHudResizeArgs('%1'), ['run-shell', '-b', `sleep ${HUD_RESIZE_RECONCILE_DELAY_SECONDS}; tmux resize-pane -t %1 -y ${HUD_TMUX_TEAM_HEIGHT_LINES} >/dev/null 2>&1 || true`]);
284
+ it('buildScheduleDelayedHudResizeArgs schedules a proof-bearing tmux-side delayed reconcile', () => {
285
+ const args = buildScheduleDelayedHudResizeArgs('%1');
286
+ assert.equal(args[0], 'run-shell');
287
+ assert.equal(args[1], '-b');
288
+ assert.match(args[2] ?? '', new RegExp(`sleep ${HUD_RESIZE_RECONCILE_DELAY_SECONDS};`));
289
+ assert.match(args[2] ?? '', /list-panes -a -F/);
290
+ assert.match(args[2] ?? '', /awk -F/);
291
+ assert.match(args[2] ?? '', new RegExp(`resize-pane -t %1 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
206
292
  });
207
- it('buildReconcileHudResizeArgs executes a best-effort quiet resize command', () => {
293
+ it('buildReconcileHudResizeArgs executes only after a global exact-pane proof', () => {
208
294
  const args = buildReconcileHudResizeArgs('%7');
209
295
  assert.equal(args.join(' ').includes('split-window'), false);
210
- assert.deepEqual(args, ['run-shell', `tmux resize-pane -t %7 -y ${HUD_TMUX_TEAM_HEIGHT_LINES} >/dev/null 2>&1 || true`]);
296
+ assert.equal(args[0], 'run-shell');
297
+ assert.match(args[1] ?? '', /list-panes -a -F/);
298
+ assert.match(args[1] ?? '', /awk -F/);
299
+ assert.match(args[1] ?? '', new RegExp(`resize-pane -t %7 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
300
+ });
301
+ it('fails closed in hook shell proofs without resizing malformed, duplicate, dead, absent, or unavailable panes', async () => {
302
+ const previousFixture = process.env.OMX_HUD_HOOK_FIXTURE;
303
+ try {
304
+ await withMockTmuxFixture('omx-hud-hook-proof-', (logPath) => `#!/bin/sh
305
+ set -eu
306
+ printf '%s\\n' "$*" >> "${logPath}"
307
+ case "$1" in
308
+ list-panes)
309
+ case "\${OMX_HUD_HOOK_FIXTURE:-}" in
310
+ missing) printf '%%7\\t0\\n' ;;
311
+ extra) printf '%%7\\t0\\t2000000007\\textra\\n' ;;
312
+ duplicate) printf '%%7\\t0\\t2000000007\\n%%7\\t0\\t2000000007\\n' ;;
313
+ dead) printf '%%7\\t1\\t2000000007\\n' ;;
314
+ absent) printf '%%8\\t0\\t2000000008\\n' ;;
315
+ query-failure) echo 'tmux query failed' >&2; exit 1 ;;
316
+ esac
317
+ ;;
318
+ resize-pane) exit 0 ;;
319
+ esac
320
+ `, async ({ logPath }) => {
321
+ const command = buildReconcileHudResizeArgs('%7')[1] ?? '';
322
+ for (const fixture of ['missing', 'extra', 'duplicate', 'dead', 'absent', 'query-failure']) {
323
+ process.env.OMX_HUD_HOOK_FIXTURE = fixture;
324
+ const result = spawnSync('/bin/sh', ['-c', command], { encoding: 'utf-8' });
325
+ assert.equal(result.status, 0, fixture);
326
+ const commands = await readFile(logPath, 'utf-8');
327
+ assert.doesNotMatch(commands, /resize-pane/, fixture);
328
+ await writeFile(logPath, '');
329
+ }
330
+ });
331
+ }
332
+ finally {
333
+ if (typeof previousFixture === 'string')
334
+ process.env.OMX_HUD_HOOK_FIXTURE = previousFixture;
335
+ else
336
+ delete process.env.OMX_HUD_HOOK_FIXTURE;
337
+ }
338
+ });
339
+ it('tolerates unrelated PID-less rows while rejecting malformed or duplicate HUD targets in generated hook proofs', async () => {
340
+ const previousFixture = process.env.OMX_HUD_HOOK_FIXTURE;
341
+ try {
342
+ await withMockTmuxFixture('omx-hud-hook-unrelated-pidless-', (logPath) => `#!/bin/sh
343
+ set -eu
344
+ printf '%s\\n' "$*" >> "${logPath}"
345
+ case "$1" in
346
+ list-panes)
347
+ case "\${OMX_HUD_HOOK_FIXTURE:-}" in
348
+ unrelated-pidless) printf '%%99\\t0\\t\\n%%7\\t0\\t2000000007\\n' ;;
349
+ target-malformed) printf '%%7\\t0\\t\\n%%99\\t0\\t2000000099\\n' ;;
350
+ target-duplicate) printf '%%7\\t0\\t2000000007\\n%%7\\t0\\t2000000007\\n' ;;
351
+ esac
352
+ ;;
353
+ show-option) printf 'team:expected\\n' ;;
354
+ resize-pane) exit 0 ;;
355
+ esac
356
+ `, async ({ logPath }) => {
357
+ const command = buildReconcileHudResizeArgs('%7', HUD_TMUX_TEAM_HEIGHT_LINES, 2000000007, 'team:expected')[1] ?? '';
358
+ for (const [fixture, shouldResize] of [
359
+ ['unrelated-pidless', true],
360
+ ['target-malformed', false],
361
+ ['target-duplicate', false],
362
+ ]) {
363
+ process.env.OMX_HUD_HOOK_FIXTURE = fixture;
364
+ const result = spawnSync('/bin/sh', ['-c', command], { encoding: 'utf-8' });
365
+ assert.equal(result.status, 0, fixture);
366
+ const commands = await readFile(logPath, 'utf-8');
367
+ assert.equal(/resize-pane/.test(commands), shouldResize, `${fixture}: ${commands}`);
368
+ await writeFile(logPath, '');
369
+ }
370
+ });
371
+ }
372
+ finally {
373
+ if (typeof previousFixture === 'string')
374
+ process.env.OMX_HUD_HOOK_FIXTURE = previousFixture;
375
+ else
376
+ delete process.env.OMX_HUD_HOOK_FIXTURE;
377
+ }
211
378
  });
212
379
  it('resolves the tmux executable for win32 hook shell snippets', async () => {
213
380
  const fakeBin = await mkdtemp(join(tmpdir(), 'omx-win32-hook-tmux-'));
@@ -257,7 +424,7 @@ describe('HUD resize hook command builders', () => {
257
424
  Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
258
425
  const args = buildRegisterClientAttachedReconcileArgs('my-session:0', 'omx_attached_team_session_0_1', '%1');
259
426
  const matches = (args[4] ?? '').match(new RegExp(escapeRegExp(tmuxPath), 'g')) || [];
260
- assert.equal(matches.length, 2, 'client-attached hook should resolve tmux for both resize and unregister commands');
427
+ assert.equal(matches.length, 4, 'client-attached hook should resolve tmux for separate resize and hook-unregister proofs');
261
428
  assert.doesNotMatch(args[4] ?? '', /; tmux set-hook -u -t my-session:0 client-attached/);
262
429
  }
263
430
  finally {
@@ -3158,11 +3325,161 @@ describe('tmux-dependent functions when tmux is unavailable', () => {
3158
3325
  assert.throws(() => createTeamSession('My Team', 1, process.cwd()), /tmux is not available/i);
3159
3326
  });
3160
3327
  });
3161
- it('listTeamSessions returns empty', () => {
3328
+ it('distinguishes a failed session query from a successful empty result', async () => {
3162
3329
  withEmptyPath(() => {
3330
+ assert.equal(listTeamSessions(), null);
3331
+ });
3332
+ await withMockTmuxFixture('omx-empty-team-sessions-', () => `#!/bin/sh
3333
+ case "$1" in
3334
+ list-sessions) exit 0 ;;
3335
+ *) exit 1 ;;
3336
+ esac
3337
+ `, async () => {
3163
3338
  assert.deepEqual(listTeamSessions(), []);
3164
3339
  });
3165
3340
  });
3341
+ it('classifies detached tmux incarnation evidence strictly', async () => {
3342
+ const cases = [
3343
+ { name: 'exact', output: 'omx-team-incarnation\t$41\t1700000001', expected: { status: 'exact', incarnation: { sessionId: '$41', sessionCreated: '1700000001' } } },
3344
+ { name: 'session-id replacement', output: 'omx-team-incarnation\t$42\t1700000001', expected: { status: 'replacement', incarnation: { sessionId: '$42', sessionCreated: '1700000001' } } },
3345
+ { name: 'creation replacement', output: 'omx-team-incarnation\t$41\t1700000002', expected: { status: 'replacement', incarnation: { sessionId: '$41', sessionCreated: '1700000002' } } },
3346
+ ];
3347
+ for (const entry of cases) {
3348
+ await withMockTmuxFixture(`omx-detached-incarnation-${entry.name.replaceAll(' ', '-')}-`, () => `#!/bin/sh
3349
+ case "$1" in
3350
+ list-sessions) printf '%s\\n' '${entry.output}' ;;
3351
+ *) exit 1 ;;
3352
+ esac
3353
+ `, async () => {
3354
+ assert.deepEqual(queryDetachedTeamSession('omx-team-incarnation', { sessionId: '$41', sessionCreated: '1700000001' }), entry.expected);
3355
+ });
3356
+ }
3357
+ });
3358
+ it('queues a stable-ID kill behind the exact server-side leader predicate', async () => {
3359
+ await withMockTmuxFixture('omx-detached-incarnation-stable-kill-', (tmuxLogPath) => `#!/bin/sh
3360
+ printf '%s\\n' "$*" >> "${tmuxLogPath}"
3361
+ case "$1" in
3362
+ list-sessions) printf '%s\\n' 'omx-team-incarnation $41 1700000001' ;;
3363
+ list-panes) case "$*" in *'#{session_name}'*) printf '%s\n' '%1 0 4241 omx-team-incarnation $41 1700000001' ;; *) printf '%s\n' '%1 0 4241' ;; esac ;;
3364
+ show-option|show-options) printf '%s\\n' 'team:incarnation' ;;
3365
+ if-shell) : > "${tmuxLogPath}.session-killed" ;;
3366
+ *) exit 1 ;;
3367
+ esac
3368
+ `, async ({ logPath }) => {
3369
+ assert.equal(requestDetachedTeamSessionDestroy('omx-team-incarnation', {
3370
+ sessionId: '$41', sessionCreated: '1700000001', leaderPaneId: '%1', leaderPanePid: 4241, ownerId: 'team:incarnation',
3371
+ }), true);
3372
+ const log = await readFile(logPath, 'utf-8');
3373
+ assert.match(log, /if-shell -F -t %1/);
3374
+ assert.match(log, /#\{==:#\{pane_dead\},0\}/);
3375
+ assert.match(log, /#\{==:#\{pane_id\},%1\}/);
3376
+ assert.match(log, /#\{==:#\{pane_pid\},4241\}/);
3377
+ assert.match(log, /#\{==:#\{@omx_team_pane_owner_id\},team:incarnation\}/);
3378
+ assert.match(log, /#\{==:#\{session_id\},\$41\}/);
3379
+ assert.match(log, /#\{==:#\{session_created\},1700000001\}/);
3380
+ assert.match(log, /kill-session -t \$41/);
3381
+ assert.equal(await readFile(`${logPath}.session-killed`, 'utf8'), '');
3382
+ assert.doesNotMatch(log, /^kill-session -t /m);
3383
+ });
3384
+ });
3385
+ it('reports server-side predicate denial without retrying or accepting a detached kill', async () => {
3386
+ await withMockTmuxFixture('omx-detached-incarnation-server-denial-', (tmuxLogPath) => `#!/bin/sh
3387
+ printf '%s\\n' "$*" >> "${tmuxLogPath}"
3388
+ case "$1" in
3389
+ list-sessions) printf '%s\\n' 'omx-team-denied $41 1700000001' ;;
3390
+ list-panes) case "$*" in *'#{session_name}'*) printf '%s\\n' '%1 0 4241 omx-team-denied $41 1700000001' ;; *) printf '%s\\n' '%1 0 4241' ;; esac ;;
3391
+ show-option|show-options) printf '%s\\n' 'team:denied' ;;
3392
+ if-shell) : > "${tmuxLogPath}.denied"; exit 1 ;;
3393
+ kill-session) : > "${tmuxLogPath}.killed" ;;
3394
+ *) exit 1 ;;
3395
+ esac
3396
+ `, async ({ logPath }) => {
3397
+ assert.equal(requestDetachedTeamSessionDestroy('omx-team-denied', {
3398
+ sessionId: '$41', sessionCreated: '1700000001', leaderPaneId: '%1', leaderPanePid: 4241, ownerId: 'team:denied',
3399
+ }), false);
3400
+ const log = await readFile(logPath, 'utf8');
3401
+ assert.match(log, /if-shell -F -t %1/);
3402
+ assert.match(log, /run-shell "exit 1"/);
3403
+ assert.equal(fs.existsSync(`${logPath}.denied`), true);
3404
+ assert.equal(fs.existsSync(`${logPath}.killed`), false);
3405
+ assert.equal((log.match(/if-shell/g) ?? []).length, 1);
3406
+ });
3407
+ });
3408
+ it('fails closed when destroy authority has already drifted', async () => {
3409
+ const cases = [
3410
+ { name: 'session', panePid: 4241, owner: 'team:final', session: '$42\t1700000002' },
3411
+ { name: 'pid', panePid: 9999, owner: 'team:final', session: '$41\t1700000001' },
3412
+ { name: 'owner', panePid: 4241, owner: 'foreign-owner', session: '$41\t1700000001' },
3413
+ ];
3414
+ for (const entry of cases) {
3415
+ await withMockTmuxFixture(`omx-detached-final-composite-${entry.name}-`, (tmuxLogPath) => `#!/bin/sh
3416
+ printf '%s\\n' "$*" >> "${tmuxLogPath}"
3417
+ case "$1" in
3418
+ list-sessions) printf 'omx-team-final\\t${entry.session}\\n' ;;
3419
+ list-panes) case "$*" in *'#{session_name}'*) printf '%%1\\t0\\t${entry.panePid}\\tomx-team-final\\t$41\\t1700000001\\n' ;; *) printf '%%1\\t0\\t${entry.panePid}\\n' ;; esac ;;
3420
+ show-option|show-options) printf '%s\\n' '${entry.owner}' ;;
3421
+ if-shell) : > "${tmuxLogPath}.session-killed" ;;
3422
+ *) exit 1 ;;
3423
+ esac
3424
+ `, async ({ logPath }) => {
3425
+ assert.equal(requestDetachedTeamSessionDestroy('omx-team-final', {
3426
+ sessionId: '$41', sessionCreated: '1700000001', leaderPaneId: '%1', leaderPanePid: 4241, ownerId: 'team:final',
3427
+ }), false);
3428
+ const log = await readFile(logPath, 'utf8');
3429
+ assert.doesNotMatch(log, /if-shell/);
3430
+ assert.doesNotMatch(log, /kill-session/);
3431
+ assert.equal(fs.existsSync(`${logPath}.session-killed`), false);
3432
+ });
3433
+ }
3434
+ });
3435
+ it('treats malformed and unavailable detached incarnation evidence as unavailable', async () => {
3436
+ await withMockTmuxFixture('omx-detached-incarnation-malformed-', () => `#!/bin/sh
3437
+ case "$1" in
3438
+ list-sessions) printf '%s\\n' 'omx-team-incarnation\\t$41' ;;
3439
+ *) exit 1 ;;
3440
+ esac
3441
+ `, async () => assert.deepEqual(queryDetachedTeamSession('omx-team-incarnation'), {
3442
+ status: 'unavailable', detail: 'malformed_session_incarnation',
3443
+ }));
3444
+ await withMockTmuxFixture('omx-detached-incarnation-unavailable-', () => `#!/bin/sh
3445
+ case "$1" in
3446
+ list-sessions) printf '%s\\n' 'permission denied' >&2; exit 1 ;;
3447
+ *) exit 1 ;;
3448
+ esac
3449
+ `, async () => assert.deepEqual(queryDetachedTeamSession('omx-team-incarnation'), {
3450
+ status: 'unavailable', detail: 'permission denied',
3451
+ }));
3452
+ });
3453
+ it('recognizes authoritative detached-session absence', async () => {
3454
+ await withMockTmuxFixture('omx-detached-incarnation-absence-', () => `#!/bin/sh
3455
+ case "$1" in
3456
+ list-sessions) exit 0 ;;
3457
+ *) exit 1 ;;
3458
+ esac
3459
+ `, async () => assert.deepEqual(queryDetachedTeamSession('omx-team-incarnation'), { status: 'absent' }));
3460
+ });
3461
+ it('accepts final-session destruction when tmux reports its expected no-server absence', async () => {
3462
+ await withMockTmuxFixture('omx-destroy-final-team-session-', () => `#!/bin/sh
3463
+ case "$1" in
3464
+ kill-session) exit 0 ;;
3465
+ list-sessions) printf '%s\\n' 'no server running on /tmp/tmux-1000/default' >&2; exit 1 ;;
3466
+ *) exit 1 ;;
3467
+ esac
3468
+ `, async () => {
3469
+ assert.equal(destroyTeamSession('omx-team-final-session'), true);
3470
+ });
3471
+ });
3472
+ it('does not accept a session kill when the absence query fails for another reason', async () => {
3473
+ await withMockTmuxFixture('omx-destroy-team-session-query-failure-', () => `#!/bin/sh
3474
+ case "$1" in
3475
+ kill-session) exit 0 ;;
3476
+ list-sessions) printf '%s\\n' 'permission denied' >&2; exit 1 ;;
3477
+ *) exit 1 ;;
3478
+ esac
3479
+ `, async () => {
3480
+ assert.equal(destroyTeamSession('omx-team-query-failure'), false);
3481
+ });
3482
+ });
3166
3483
  it('waitForWorkerReady uses visible capture-pane argv without tail flags', async () => {
3167
3484
  await withMockTmuxFixture('omx-tmux-worker-ready-visible-capture-', (logPath) => `#!/bin/sh
3168
3485
  set -eu
@@ -3626,7 +3943,9 @@ exit 0
3626
3943
  const prevTmuxPane = process.env.TMUX_PANE;
3627
3944
  const prevWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
3628
3945
  try {
3629
- await withMockTmuxFixture('omx-tmux-redraw-leader-', (logPath) => `#!/bin/sh
3946
+ await withMockTmuxFixture('omx-tmux-redraw-leader-', (logPath) => {
3947
+ const proofStatePath = `${logPath}.leader-redraw-proof`;
3948
+ return `#!/bin/sh
3630
3949
  set -eu
3631
3950
  printf '%s\n' "$*" >> "${logPath}"
3632
3951
  case "\${1:-}" in
@@ -3647,11 +3966,22 @@ case "\${1:-}" in
3647
3966
  ;;
3648
3967
  list-panes)
3649
3968
  case "$*" in
3969
+ *"-a -F #{pane_id}"*)
3970
+ proof_count=0
3971
+ if [ -f "${proofStatePath}" ]; then proof_count=$(cat "${proofStatePath}"); fi
3972
+ proof_count=$((proof_count + 1))
3973
+ printf '%s' "$proof_count" > "${proofStatePath}"
3974
+ if [ "$proof_count" -eq 100 ]; then
3975
+ printf 'not-a-pane-snapshot\n'
3976
+ else
3977
+ printf "%%1\t0\t2000000001\n%%2\t0\t2000000002\n%%3\t0\t2000000003\n"
3978
+ fi
3979
+ ;;
3650
3980
  *"pane_current_command"*)
3651
- printf "%%1\\tnode\\t'codex'\\n"
3981
+ if [ -f "${logPath}.hud" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n%%3\\tnode\\texec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE=%%1 node /omx.js hud --watch\\n"; elif [ -f "${logPath}.worker" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n"; else printf "%%1\\tnode\\t'codex'\\n"; fi
3652
3982
  ;;
3653
3983
  *)
3654
- printf "%%1\\n"
3984
+ if [ -f "${logPath}.hud" ]; then printf "%%1\\n%%2\\n%%3\\n"; elif [ -f "${logPath}.worker" ]; then printf "%%1\\n%%2\\n"; else printf "%%1\\n"; fi
3655
3985
  ;;
3656
3986
  esac
3657
3987
  exit 0
@@ -3659,9 +3989,11 @@ case "\${1:-}" in
3659
3989
  split-window)
3660
3990
  case "$*" in
3661
3991
  *" -h "*)
3992
+ : > "${logPath}.worker"
3662
3993
  echo "%2"
3663
3994
  ;;
3664
3995
  *)
3996
+ : > "${logPath}.hud"
3665
3997
  echo "%3"
3666
3998
  ;;
3667
3999
  esac
@@ -3674,7 +4006,8 @@ case "\${1:-}" in
3674
4006
  exit 0
3675
4007
  ;;
3676
4008
  esac
3677
- `, async ({ logPath }) => {
4009
+ `;
4010
+ }, async ({ logPath }) => {
3678
4011
  const fakeBinDir = join(logPath, '..');
3679
4012
  const geminiPath = join(fakeBinDir, 'gemini');
3680
4013
  await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
@@ -3684,10 +4017,17 @@ esac
3684
4017
  process.env.OMX_TEAM_WORKER_CLI = 'gemini';
3685
4018
  createTeamSession('Diff Gutter Redraw', 1, cwd);
3686
4019
  const tmuxLog = await readFile(logPath, 'utf-8');
4020
+ const commands = tmuxLog.trim().split('\n').filter(Boolean);
3687
4021
  assert.match(tmuxLog, /select-layout -t leader:0 main-vertical/);
3688
4022
  assert.match(tmuxLog, /set-window-option -t leader:0 main-pane-width 60/);
3689
- assert.match(tmuxLog, /split-window -v -f -l 3 -t leader:0 -d -P -F #\{pane_id\}/);
3690
- assert.match(tmuxLog, /send-keys -t %1 C-l/, 'leader Codex pane must repaint after team splits/resizes so tmux does not auto-wrap diff continuation rows without the line-number gutter');
4023
+ assert.match(tmuxLog, /split-window -v -f -l 3 -t %1 -d -P -F #\{pane_id\}/);
4024
+ const redrawIndices = commands
4025
+ .map((command, index) => command === 'send-keys -t %1 C-l' ? index : -1)
4026
+ .filter((index) => index >= 0);
4027
+ assert.equal(redrawIndices.length, 1);
4028
+ const redrawIndex = redrawIndices[0];
4029
+ assert.ok(redrawIndex > 0);
4030
+ assert.match(commands[redrawIndex - 1] ?? '', /^list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}$/, 'leader Codex pane redraw must be immediately preceded by exact global live-pane proof');
3691
4031
  });
3692
4032
  }
3693
4033
  finally {
@@ -3706,6 +4046,92 @@ esac
3706
4046
  await rm(cwd, { recursive: true, force: true });
3707
4047
  }
3708
4048
  });
4049
+ it('blocks window-wide layout when an extra pane appears during a later owner check', async () => {
4050
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-final-window-proof-'));
4051
+ const previousTmux = process.env.TMUX;
4052
+ const previousTmuxPane = process.env.TMUX_PANE;
4053
+ const previousWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
4054
+ try {
4055
+ await withMockTmuxFixture('omx-tmux-final-window-proof-', (logPath) => `#!/bin/sh
4056
+ set -eu
4057
+ printf '%s\\n' "$*" >> "${logPath}"
4058
+ case "\${1:-}" in
4059
+ -V) echo 'tmux 3.4' ;;
4060
+ display-message)
4061
+ case "$*" in *'#{window_width}'*) echo 120 ;; *) echo 'leader:0 %1' ;; esac
4062
+ ;;
4063
+ list-panes)
4064
+ case "$*" in
4065
+ *'-a -F #{pane_id}\t#{pane_dead}\t#{pane_pid}'*)
4066
+ if [ -f "${logPath}.replace" ]; then
4067
+ printf '%%1\\t0\\t2000000001\\n%%2\\t0\\t2000000002\\n%%3\\t0\\t2000000003\\n'
4068
+ else
4069
+ printf '%%1\\t0\\t2000000001\\n%%2\\t0\\t2000000002\\n'
4070
+ fi
4071
+ ;;
4072
+ *'pane_current_command'*)
4073
+ if [ -f "${logPath}.replace" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n%%3\\tzsh\\tzsh\\n"; elif [ -f "${logPath}.worker" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n"; else printf "%%1\\tnode\\t'codex'\\n"; fi
4074
+ ;;
4075
+ *)
4076
+ if [ -f "${logPath}.replace" ]; then printf '%%1\\n%%2\\n%%3\\n'; elif [ -f "${logPath}.worker" ]; then printf '%%1\\n%%2\\n'; else printf '%%1\\n'; fi
4077
+ ;;
4078
+ esac
4079
+ ;;
4080
+ split-window) : > "${logPath}.worker"; echo '%2' ;;
4081
+ show-option)
4082
+ case "$*" in
4083
+ *' -t %1 @omx_team_pane_owner_id') echo 'team:final-window-proof' ;;
4084
+ *' -t %2 @omx_team_pane_owner_id') : > "${logPath}.replace"; echo 'team:final-window-proof' ;;
4085
+ *) exit 1 ;;
4086
+ esac
4087
+ ;;
4088
+ set-option|resize-pane|select-pane|set-hook|run-shell|send-keys) ;;
4089
+ kill-pane|select-layout|set-window-option) exit 97 ;;
4090
+ esac
4091
+ `, async ({ logPath }) => {
4092
+ const fakeBinDir = join(logPath, '..');
4093
+ const geminiPath = join(fakeBinDir, 'gemini');
4094
+ await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
4095
+ await chmod(geminiPath, 0o755);
4096
+ process.env.TMUX = 'leader-session,stub,0';
4097
+ process.env.TMUX_PANE = '%1';
4098
+ process.env.OMX_TEAM_WORKER_CLI = 'gemini';
4099
+ assert.throws(() => createTeamSession('Final Window Proof', 1, cwd, [], undefined, {
4100
+ teamPaneOwnerId: 'team:final-window-proof',
4101
+ }), (error) => {
4102
+ assert.ok(error instanceof CreateTeamSessionPartialError);
4103
+ assert.ok(error.originalError instanceof Error);
4104
+ assert.match(error.originalError.message, /tmux window topology changed before layout mutation/);
4105
+ assert.equal(error.partialSession.name, 'leader:0');
4106
+ assert.equal(error.partialSession.teamPaneOwnerId, 'team:final-window-proof');
4107
+ assert.deepEqual(error.partialSession.workerPaneIds, ['%2']);
4108
+ assert.deepEqual(error.partialSession.workerPanePidsByIndex, [2000000002]);
4109
+ assert.deepEqual(error.proofUnavailable, []);
4110
+ assert.ok(error.cleanupErrors.some((message) => /failed to kill tmux pane %2/.test(message)));
4111
+ return true;
4112
+ });
4113
+ const commands = await readFile(logPath, 'utf-8');
4114
+ assert.match(commands, /show-option -qv -p -t %2 @omx_team_pane_owner_id/);
4115
+ assert.doesNotMatch(commands, /select-layout|set-window-option/);
4116
+ assert.match(commands, /kill-pane -t %2/);
4117
+ });
4118
+ }
4119
+ finally {
4120
+ if (typeof previousTmux === 'string')
4121
+ process.env.TMUX = previousTmux;
4122
+ else
4123
+ delete process.env.TMUX;
4124
+ if (typeof previousTmuxPane === 'string')
4125
+ process.env.TMUX_PANE = previousTmuxPane;
4126
+ else
4127
+ delete process.env.TMUX_PANE;
4128
+ if (typeof previousWorkerCli === 'string')
4129
+ process.env.OMX_TEAM_WORKER_CLI = previousWorkerCli;
4130
+ else
4131
+ delete process.env.OMX_TEAM_WORKER_CLI;
4132
+ await rm(cwd, { recursive: true, force: true });
4133
+ }
4134
+ });
3709
4135
  it('tags leader, worker, and HUD panes with pane-scoped instance ownership', async () => {
3710
4136
  const cwd = await mkdtemp(join(tmpdir(), 'omx-team-pane-tags-'));
3711
4137
  const prevTmux = process.env.TMUX;
@@ -3734,16 +4160,20 @@ case "\${1:-}" in
3734
4160
  ;;
3735
4161
  list-panes)
3736
4162
  case "$*" in
4163
+ *"-a -F #{pane_id}"*)
4164
+ printf "%%1\t0\t2000000001\n%%2\t0\t2000000002\n%%3\t0\t2000000003\n"
4165
+ ;;
3737
4166
  *"pane_current_command"*)
3738
- printf "%%1\\tnode\\t'codex'\\n"
4167
+ if [ -f "${logPath}.worker" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n"; else printf "%%1\\tnode\\t'codex'\\n"; fi
3739
4168
  ;;
3740
4169
  *)
3741
- printf "%%1\\n"
4170
+ if [ -f "${logPath}.worker" ]; then printf "%%1\\n%%2\\n"; else printf "%%1\\n"; fi
3742
4171
  ;;
3743
4172
  esac
3744
4173
  exit 0
3745
4174
  ;;
3746
4175
  split-window)
4176
+ : > "${logPath}.worker"
3747
4177
  case "$*" in
3748
4178
  *" -h "*)
3749
4179
  echo "%2"
@@ -3784,6 +4214,20 @@ esac
3784
4214
  assert.match(tmuxLog, /set-option -p -t %2 @omx_team_pane_owner_id team:pane-tags/);
3785
4215
  assert.match(tmuxLog, /set-option -p -t %3 @omx_team_pane_owner_id team:pane-tags/);
3786
4216
  assert.match(tmuxLog, /exec env OMX_SESSION_ID='omx-pane-scope' OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%1' .*hud --watch/);
4217
+ const commands = tmuxLog.trim().split('\n').filter(Boolean);
4218
+ const globalExactPanePidProof = /^list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}$/;
4219
+ const targetScopedExactPaneSetProof = /^list-panes -t shared:0 -F #\{pane_id\}\t#\{pane_current_command\}\t#\{pane_start_command\}$/;
4220
+ const exactPaneEffects = /^(set-option -p -t %|split-window .* -t %|resize-pane -t %|select-pane -t %|send-keys -t %)/;
4221
+ for (const [index, command] of commands.entries()) {
4222
+ if (!exactPaneEffects.test(command))
4223
+ continue;
4224
+ const immediatelyPrevious = commands[index - 1] ?? '';
4225
+ const previousProof = commands[index - 2] ?? '';
4226
+ const hasAdjacentAuthority = globalExactPanePidProof.test(immediatelyPrevious)
4227
+ || (targetScopedExactPaneSetProof.test(immediatelyPrevious)
4228
+ && globalExactPanePidProof.test(previousProof));
4229
+ assert.equal(hasAdjacentAuthority, true, `exact-pane effect must be immediately preceded by an authoritative exact-pane proof: ${command}`);
4230
+ }
3787
4231
  });
3788
4232
  }
3789
4233
  finally {
@@ -3806,6 +4250,71 @@ esac
3806
4250
  await rm(cwd, { recursive: true, force: true });
3807
4251
  }
3808
4252
  });
4253
+ it('fails closed when a tagged worker keeps its PID but loses Team owner continuity before a later split', async () => {
4254
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-owner-change-'));
4255
+ const prevTmux = process.env.TMUX;
4256
+ const prevTmuxPane = process.env.TMUX_PANE;
4257
+ const prevWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
4258
+ try {
4259
+ await withMockTmuxFixture('omx-tmux-owner-change-', (logPath) => `#!/bin/sh
4260
+ set -eu
4261
+ printf '%s\n' "$*" >> "${logPath}"
4262
+ case "\${1:-}" in
4263
+ -V) echo "tmux 3.4" ;;
4264
+ display-message)
4265
+ case "$*" in *"#{window_width}"*) echo "120" ;; *) echo "leader:0 %1" ;; esac
4266
+ ;;
4267
+ list-panes)
4268
+ case "$*" in
4269
+ *"-a -F #{pane_id}"*) printf "%%1\\t0\\t2000000001\\n%%2\\t0\\t2000000002\\n" ;;
4270
+ *"pane_current_command"*)
4271
+ if [ -f "${logPath}.worker" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n"; else printf "%%1\\tnode\\t'codex'\\n"; fi
4272
+ ;;
4273
+ *) if [ -f "${logPath}.worker" ]; then printf "%%1\\n%%2\\n"; else printf "%%1\\n"; fi ;;
4274
+ esac
4275
+ ;;
4276
+ split-window) : > "${logPath}.worker"; echo "%2" ;;
4277
+ set-option)
4278
+ case "$*" in
4279
+ *"-p -t %2 @omx_team_pane_owner_id"*)
4280
+ printf '%s' 'team:foreign' > "$(dirname "${logPath}")/tmux.team-owner-state/%2"
4281
+ ;;
4282
+ esac
4283
+ ;;
4284
+ *) ;;
4285
+ esac
4286
+ `, async ({ logPath }) => {
4287
+ const fakeBinDir = join(logPath, '..');
4288
+ const geminiPath = join(fakeBinDir, 'gemini');
4289
+ await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
4290
+ await chmod(geminiPath, 0o755);
4291
+ process.env.TMUX = 'leader-session,stub,0';
4292
+ process.env.TMUX_PANE = '%1';
4293
+ process.env.OMX_TEAM_WORKER_CLI = 'gemini';
4294
+ assert.throws(() => createTeamSession('Owner Change', 2, cwd), (error) => error instanceof CreateTeamSessionPartialError
4295
+ && error.originalError instanceof Error
4296
+ && /tmux pane team owner changed: %2/.test(error.originalError.message));
4297
+ const effects = await readFile(logPath, 'utf-8');
4298
+ assert.match(effects, /split-window -h -t %1/);
4299
+ assert.doesNotMatch(effects, /split-window -v|select-layout|set-window-option|resize-pane|select-pane|send-keys|set-hook|run-shell/);
4300
+ });
4301
+ }
4302
+ finally {
4303
+ if (typeof prevTmux === 'string')
4304
+ process.env.TMUX = prevTmux;
4305
+ else
4306
+ delete process.env.TMUX;
4307
+ if (typeof prevTmuxPane === 'string')
4308
+ process.env.TMUX_PANE = prevTmuxPane;
4309
+ else
4310
+ delete process.env.TMUX_PANE;
4311
+ if (typeof prevWorkerCli === 'string')
4312
+ process.env.OMX_TEAM_WORKER_CLI = prevWorkerCli;
4313
+ else
4314
+ delete process.env.OMX_TEAM_WORKER_CLI;
4315
+ await rm(cwd, { recursive: true, force: true });
4316
+ }
4317
+ });
3809
4318
  it('does not reuse the Team pane owner token as the HUD logical session id', async () => {
3810
4319
  const cwd = await mkdtemp(join(tmpdir(), 'omx-team-hud-session-boundary-'));
3811
4320
  const prevTmux = process.env.TMUX;
@@ -3834,19 +4343,23 @@ case "\${1:-}" in
3834
4343
  ;;
3835
4344
  list-panes)
3836
4345
  case "$*" in
4346
+ *"-a -F #{pane_id}"*)
4347
+ printf "%%1\t0\t2000000001\n%%2\t0\t2000002222\n%%3\t0\t2000000003\n"
4348
+ ;;
3837
4349
  *"-t %2 -F #{pane_pid}"*)
3838
4350
  echo "2000002222"
3839
4351
  ;;
3840
4352
  *"pane_current_command"*)
3841
- printf "%%1\\tnode\\t'codex'\\n"
4353
+ if [ -f "${logPath}.worker" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n"; else printf "%%1\\tnode\\t'codex'\\n"; fi
3842
4354
  ;;
3843
4355
  *)
3844
- printf "%%1\\n"
4356
+ if [ -f "${logPath}.worker" ]; then printf "%%1\\n%%2\\n"; else printf "%%1\\n"; fi
3845
4357
  ;;
3846
4358
  esac
3847
4359
  exit 0
3848
4360
  ;;
3849
4361
  split-window)
4362
+ : > "${logPath}.worker"
3850
4363
  case "$*" in
3851
4364
  *" -h "*)
3852
4365
  echo "%2"
@@ -3906,7 +4419,7 @@ esac
3906
4419
  await rm(cwd, { recursive: true, force: true });
3907
4420
  }
3908
4421
  });
3909
- it('removes only HUD panes owned by the current leader during team startup', async () => {
4422
+ it('rejects an unowned pane in startup topology before any window-wide mutation', async () => {
3910
4423
  const cwd = await mkdtemp(join(tmpdir(), 'omx-team-owned-hud-startup-'));
3911
4424
  const prevTmux = process.env.TMUX;
3912
4425
  const prevTmuxPane = process.env.TMUX_PANE;
@@ -3934,6 +4447,10 @@ case "\${1:-}" in
3934
4447
  ;;
3935
4448
  list-panes)
3936
4449
  case "$*" in
4450
+ *"-a -F #{pane_id}"*)
4451
+ printf "%%1\t0\t2000000001\n%%3\t0\t2000000003\n%%4\t0\t2000000004\n%%7\t0\t2000000007\n%%8\t0\t2000000008\n"
4452
+ if [ ! -f "${logPath}.killed-%2" ]; then printf "%%2\t0\t2000000002\n"; fi
4453
+ ;;
3937
4454
  *"pane_current_command"*)
3938
4455
  printf "%%1\\tnode\\t'codex'\\n"
3939
4456
  printf "%%7\\tnode\\t'codex neighbor'\\n"
@@ -3957,7 +4474,11 @@ case "\${1:-}" in
3957
4474
  esac
3958
4475
  exit 0
3959
4476
  ;;
3960
- set-option|resize-pane|select-layout|set-window-option|select-pane|set-hook|run-shell|send-keys|kill-pane)
4477
+ kill-pane)
4478
+ : > "${logPath}.killed-$3"
4479
+ exit 0
4480
+ ;;
4481
+ set-option|resize-pane|select-layout|set-window-option|select-pane|set-hook|run-shell|send-keys)
3961
4482
  exit 0
3962
4483
  ;;
3963
4484
  *)
@@ -3973,13 +4494,9 @@ esac
3973
4494
  process.env.TMUX_PANE = '%1';
3974
4495
  process.env.OMX_SESSION_ID = 'leader-session-a';
3975
4496
  process.env.OMX_TEAM_WORKER_CLI = 'gemini';
3976
- const session = createTeamSession('Owned HUD Startup', 1, cwd);
3977
- assert.equal(session.leaderPaneId, '%1');
3978
- assert.equal(session.hudPaneId, '%4');
4497
+ assert.throws(() => createTeamSession('Owned HUD Startup', 1, cwd), /tmux window topology changed before layout mutation/);
3979
4498
  const tmuxLog = await readFile(logPath, 'utf-8');
3980
- assert.match(tmuxLog, /kill-pane -t %2/);
3981
- assert.doesNotMatch(tmuxLog, /kill-pane -t %8/);
3982
- assert.match(tmuxLog, /split-window -v -f -l 3 -t shared:0 -d -P -F #\{pane_id\}/);
4499
+ assert.doesNotMatch(tmuxLog, /set-option|kill-pane|split-window|resize-pane|select-layout|set-window-option|select-pane|send-keys/);
3983
4500
  });
3984
4501
  }
3985
4502
  finally {
@@ -4002,71 +4519,64 @@ esac
4002
4519
  await rm(cwd, { recursive: true, force: true });
4003
4520
  }
4004
4521
  });
4005
- it('uses tmux 3.2a-compatible client-resized hook registration for team HUD resize', async () => {
4006
- const cwd = await mkdtemp(join(tmpdir(), 'omx-team-resize-hook-fallback-'));
4007
- const prevTmux = process.env.TMUX;
4008
- const prevTmuxPane = process.env.TMUX_PANE;
4009
- const prevWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
4010
- const prevWarn = console.warn;
4011
- const warnings = [];
4522
+ it('preserves rollback debt when a successfully tagged pane owner changes before cleanup', async () => {
4523
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-partial-rollback-proof-'));
4524
+ const previousTmux = process.env.TMUX;
4525
+ const previousTmuxPane = process.env.TMUX_PANE;
4526
+ const previousWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
4012
4527
  try {
4013
- await withMockTmuxFixture('omx-tmux-resize-hook-fallback-', (logPath) => `#!/bin/sh
4528
+ await withMockTmuxFixture('omx-tmux-partial-rollback-proof-', (logPath) => `#!/bin/sh
4014
4529
  set -eu
4015
- printf '%s\n' "$*" >> "${logPath}"
4016
- case "\${1:-}" in
4530
+ printf '%s\\n' "$*" >> "${logPath}"
4531
+ case "$1" in
4017
4532
  -V)
4018
- echo "tmux 3.2a"
4533
+ echo "tmux 3.4"
4019
4534
  exit 0
4020
4535
  ;;
4021
4536
  display-message)
4022
4537
  case "$*" in
4023
- *"#{window_width}"*)
4024
- echo "120"
4025
- ;;
4026
- *)
4027
- echo "leader:0 %1"
4028
- ;;
4538
+ *"#{window_width}"*) echo "120" ;;
4539
+ *) echo "leader:0 %1" ;;
4029
4540
  esac
4030
4541
  exit 0
4031
4542
  ;;
4032
4543
  list-panes)
4033
4544
  case "$*" in
4545
+ *"-a -F #{pane_id}"*)
4546
+ proof_count=0
4547
+ if [ -f "${logPath}.proof-count" ]; then proof_count=$(cat "${logPath}.proof-count"); fi
4548
+ proof_count=$((proof_count + 1))
4549
+ printf '%s' "$proof_count" > "${logPath}.proof-count"
4550
+ if [ "$proof_count" -gt 11 ]; then printf 'not-a-pane-snapshot\n'; else printf "%%1\t0\t2000000001\n%%2\t0\t2000000002\n"; fi
4551
+
4552
+ ;;
4034
4553
  *"pane_current_command"*)
4035
- printf "%%1\tnode\t'codex'\n"
4554
+ if [ -f "${logPath}.worker" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n"; else printf "%%1\\tnode\\t'codex'\\n"; fi
4036
4555
  ;;
4037
4556
  *)
4038
- printf "%%1\n"
4557
+ if [ -f "${logPath}.worker" ]; then printf "%%1\\n%%2\\n"; else printf "%%1\\n"; fi
4039
4558
  ;;
4040
4559
  esac
4041
4560
  exit 0
4042
4561
  ;;
4043
- split-window)
4044
- case "$*" in
4045
- *" -h "*)
4046
- echo "%2"
4047
- ;;
4048
- *)
4049
- echo "%3"
4050
- ;;
4051
- esac
4562
+ show-option)
4563
+ if [ -f "${logPath}.worker" ]; then echo "team:foreign"; else echo "team:partial-rollback-proof"; fi
4052
4564
  exit 0
4053
4565
  ;;
4054
- set-hook)
4055
- case "$*" in
4056
- *"window-resized["*)
4057
- echo "invalid option: window-resized[]" >&2
4058
- exit 1
4059
- ;;
4060
- *" -w "*)
4061
- echo "invalid option: -w" >&2
4062
- exit 1
4063
- ;;
4064
- *)
4065
- exit 0
4066
- ;;
4067
- esac
4566
+ split-window)
4567
+ : > "${logPath}.worker"
4568
+ is_horizontal=0
4569
+ for arg in "$@"; do
4570
+ if [ "$arg" = "-h" ]; then is_horizontal=1; fi
4571
+ done
4572
+ if [ "$is_horizontal" = "1" ]; then
4573
+ echo "%2"
4574
+ else
4575
+ echo "second worker rejected" >&2
4576
+ exit 1
4577
+ fi
4068
4578
  ;;
4069
- set-option|resize-pane|select-layout|set-window-option|select-pane|run-shell)
4579
+ set-option|select-layout|set-window-option|select-pane|set-hook|run-shell|kill-pane)
4070
4580
  exit 0
4071
4581
  ;;
4072
4582
  *)
@@ -4081,82 +4591,164 @@ esac
4081
4591
  process.env.TMUX = 'leader-session,stub,0';
4082
4592
  process.env.TMUX_PANE = '%1';
4083
4593
  process.env.OMX_TEAM_WORKER_CLI = 'gemini';
4084
- console.warn = (...args) => { warnings.push(args.map(String).join(' ')); };
4085
- const session = createTeamSession('Resize Hook Fallback', 1, cwd);
4086
- assert.equal(session.hudPaneId, '%3');
4087
- assert.ok(session.resizeHookName);
4088
- assert.equal(session.resizeHookTarget, 'leader:0');
4089
- assert.equal(warnings.join('\n'), '');
4594
+ assert.throws(() => createTeamSession('Partial Rollback Proof', 2, cwd), (error) => {
4595
+ assert.ok(error instanceof Error);
4596
+ assert.equal(error.name, 'CreateTeamSessionPartialError');
4597
+ const partial = error;
4598
+ assert.equal(partial.partialSession.name, 'leader:0');
4599
+ assert.equal(partial.partialSession.workerCount, 2);
4600
+ assert.deepEqual(partial.partialSession.workerPaneIds, ['%2']);
4601
+ assert.ok(partial.proofUnavailable.length > 0 || partial.cleanupErrors.length > 0);
4602
+ return true;
4603
+ });
4090
4604
  const tmuxLog = await readFile(logPath, 'utf-8');
4091
- assert.match(tmuxLog, /set-hook -t leader:0 client-resized\[\d+\]/);
4092
- assert.doesNotMatch(tmuxLog, /window-resized\[/);
4093
- assert.doesNotMatch(tmuxLog, /set-hook -w /);
4094
- assert.match(tmuxLog, /set-hook -t leader:0 client-attached\[\d+\]/);
4095
- assert.match(tmuxLog, new RegExp(`run-shell -b sleep ${HUD_RESIZE_RECONCILE_DELAY_SECONDS}; .*resize-pane -t %3 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
4096
- assert.match(tmuxLog, new RegExp(`run-shell .*resize-pane -t %3 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
4605
+ assert.match(tmuxLog, /list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}/);
4097
4606
  assert.doesNotMatch(tmuxLog, /kill-pane -t %2/);
4098
- assert.doesNotMatch(tmuxLog, /kill-pane -t %3/);
4099
4607
  });
4100
4608
  }
4101
4609
  finally {
4102
- console.warn = prevWarn;
4103
- if (typeof prevTmux === 'string')
4104
- process.env.TMUX = prevTmux;
4610
+ if (typeof previousTmux === 'string')
4611
+ process.env.TMUX = previousTmux;
4105
4612
  else
4106
4613
  delete process.env.TMUX;
4107
- if (typeof prevTmuxPane === 'string')
4108
- process.env.TMUX_PANE = prevTmuxPane;
4614
+ if (typeof previousTmuxPane === 'string')
4615
+ process.env.TMUX_PANE = previousTmuxPane;
4109
4616
  else
4110
4617
  delete process.env.TMUX_PANE;
4111
- if (typeof prevWorkerCli === 'string')
4112
- process.env.OMX_TEAM_WORKER_CLI = prevWorkerCli;
4618
+ if (typeof previousWorkerCli === 'string')
4619
+ process.env.OMX_TEAM_WORKER_CLI = previousWorkerCli;
4113
4620
  else
4114
4621
  delete process.env.OMX_TEAM_WORKER_CLI;
4115
4622
  await rm(cwd, { recursive: true, force: true });
4116
4623
  }
4117
4624
  });
4118
- it('degrades HUD run-shell resize failures to warnings during team startup', async () => {
4119
- const cwd = await mkdtemp(join(tmpdir(), 'omx-team-runshell-fallback-'));
4120
- const prevTmux = process.env.TMUX;
4121
- const prevTmuxPane = process.env.TMUX_PANE;
4122
- const prevWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
4123
- const prevWarn = console.warn;
4124
- const warnings = [];
4125
- try {
4126
- await withMockTmuxFixture('omx-tmux-runshell-fallback-', (logPath) => `#!/bin/sh
4625
+ for (const firstProofFailure of ['malformed', 'unavailable']) {
4626
+ it(`preserves unproven split pane debt when its first proof is ${firstProofFailure}`, async () => {
4627
+ const cwd = await mkdtemp(join(tmpdir(), `omx-team-first-split-${firstProofFailure}-`));
4628
+ const previousTmux = process.env.TMUX;
4629
+ const previousTmuxPane = process.env.TMUX_PANE;
4630
+ const previousWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
4631
+ try {
4632
+ await withMockTmuxFixture(`omx-tmux-first-split-${firstProofFailure}-`, (logPath) => `#!/bin/sh
4127
4633
  set -eu
4128
- printf '%s\n' "$*" >> "${logPath}"
4129
- case "\${1:-}" in
4634
+ printf '%s\\n' "$*" >> "${logPath}"
4635
+ case "$1" in
4130
4636
  -V)
4131
4637
  echo "tmux 3.4"
4132
- exit 0
4133
4638
  ;;
4134
4639
  display-message)
4135
4640
  case "$*" in
4136
4641
  *"#{window_width}"*) echo "120" ;;
4137
4642
  *) echo "leader:0 %1" ;;
4138
4643
  esac
4139
- exit 0
4140
4644
  ;;
4141
4645
  list-panes)
4142
4646
  case "$*" in
4647
+ *"-a -F #{pane_id}"*)
4648
+ if [ -f "${logPath}.worker-created" ]; then
4649
+ ${firstProofFailure === 'malformed'
4650
+ ? "printf 'not-a-pane-snapshot\\n'"
4651
+ : "echo 'topology unavailable' >&2; exit 1"}
4652
+ else
4653
+ printf "%%1\\t0\\t2000000001\\n"
4654
+ fi
4655
+ ;;
4143
4656
  *"pane_current_command"*) printf "%%1\\tnode\\t'codex'\\n" ;;
4144
4657
  *) printf "%%1\\n" ;;
4145
4658
  esac
4146
- exit 0
4659
+ ;;
4660
+ show-option)
4661
+ echo "team:first-split-proof"
4147
4662
  ;;
4148
4663
  split-window)
4664
+ : > "${logPath}.worker-created"
4665
+ echo "%2"
4666
+ ;;
4667
+ set-option|select-layout|set-window-option|select-pane|set-hook|run-shell|kill-pane|send-keys|resize-pane)
4668
+ exit 0
4669
+ ;;
4670
+ *)
4671
+ exit 0
4672
+ ;;
4673
+ esac
4674
+ `, async ({ logPath }) => {
4675
+ const fakeBinDir = join(logPath, '..');
4676
+ const geminiPath = join(fakeBinDir, 'gemini');
4677
+ await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
4678
+ await chmod(geminiPath, 0o755);
4679
+ process.env.TMUX = 'leader-session,stub,0';
4680
+ process.env.TMUX_PANE = '%1';
4681
+ process.env.OMX_TEAM_WORKER_CLI = 'gemini';
4682
+ assert.throws(() => createTeamSession('First Split Proof', 1, cwd, [], undefined, {
4683
+ teamPaneOwnerId: 'team:first-split-proof',
4684
+ }), (error) => {
4685
+ assert.ok(error instanceof CreateTeamSessionPartialError);
4686
+ assert.deepEqual(error.partialSession.workerPaneIds, ['%2']);
4687
+ assert.deepEqual(error.partialSession.workerPaneIdsByIndex, ['%2']);
4688
+ assert.deepEqual(error.partialSession.workerPanePidsByIndex, [null]);
4689
+ assert.ok(error.proofUnavailable.length > 0);
4690
+ return true;
4691
+ });
4692
+ const tmuxLog = await readFile(logPath, 'utf-8');
4693
+ assert.match(tmuxLog, /split-window .* -t %1 .* -P -F #\{pane_id\}/);
4694
+ assert.doesNotMatch(tmuxLog, /(?:kill-pane|send-keys|resize-pane|select-pane|set-option) .* -t %2/, tmuxLog);
4695
+ });
4696
+ }
4697
+ finally {
4698
+ if (typeof previousTmux === 'string')
4699
+ process.env.TMUX = previousTmux;
4700
+ else
4701
+ delete process.env.TMUX;
4702
+ if (typeof previousTmuxPane === 'string')
4703
+ process.env.TMUX_PANE = previousTmuxPane;
4704
+ else
4705
+ delete process.env.TMUX_PANE;
4706
+ if (typeof previousWorkerCli === 'string')
4707
+ process.env.OMX_TEAM_WORKER_CLI = previousWorkerCli;
4708
+ else
4709
+ delete process.env.OMX_TEAM_WORKER_CLI;
4710
+ await rm(cwd, { recursive: true, force: true });
4711
+ }
4712
+ });
4713
+ }
4714
+ it('does not report partial session metadata when proof loss occurs before a worker pane exists', async () => {
4715
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-no-resource-proof-'));
4716
+ const previousTmux = process.env.TMUX;
4717
+ const previousTmuxPane = process.env.TMUX_PANE;
4718
+ const previousWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
4719
+ const previousEntryPath = process.env[OMX_ENTRY_PATH_ENV];
4720
+ const previousArgv = process.argv;
4721
+ try {
4722
+ await withMockTmuxFixture('omx-tmux-no-resource-proof-', (logPath) => `#!/bin/sh
4723
+ set -eu
4724
+ printf '%s\\n' "$*" >> "${logPath}"
4725
+ case "$1" in
4726
+ -V)
4727
+ echo "tmux 3.4"
4728
+ exit 0
4729
+ ;;
4730
+ display-message)
4149
4731
  case "$*" in
4150
- *" -h "*) echo "%2" ;;
4151
- *) echo "%3" ;;
4732
+ *"#{window_width}"*) echo "120" ;;
4733
+ *) echo "leader:0 %1" ;;
4152
4734
  esac
4153
4735
  exit 0
4154
4736
  ;;
4155
- run-shell)
4156
- echo "Unsupported tmux compatibility command: run-shell" >&2
4157
- exit 1
4737
+ list-panes)
4738
+ case "$*" in
4739
+ *"-a -F #{pane_id}"*)
4740
+ proof_count=0
4741
+ if [ -f "${logPath}.proof-count" ]; then proof_count=$(cat "${logPath}.proof-count"); fi
4742
+ proof_count=$((proof_count + 1))
4743
+ printf '%s' "$proof_count" > "${logPath}.proof-count"
4744
+ if [ "$proof_count" -le 4 ]; then printf "%%1\t0\t2000000001\n"; else printf 'not-a-pane-snapshot\n'; fi
4745
+ ;;
4746
+ *"pane_current_command"*) printf '%s\n' "%1\tnode\tcodex" ;;
4747
+ *) printf "%%1\n" ;;
4748
+ esac
4749
+ exit 0
4158
4750
  ;;
4159
- set-option|resize-pane|select-layout|set-window-option|select-pane|set-hook|send-keys)
4751
+ set-option)
4160
4752
  exit 0
4161
4753
  ;;
4162
4754
  *)
@@ -4164,299 +4756,495 @@ case "\${1:-}" in
4164
4756
  ;;
4165
4757
  esac
4166
4758
  `, async ({ logPath }) => {
4167
- const fakeBinDir = join(logPath, '..');
4168
- const geminiPath = join(fakeBinDir, 'gemini');
4169
- await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
4170
- await chmod(geminiPath, 0o755);
4759
+ const codexStubPath = join(dirname(logPath), 'codex');
4760
+ await writeFile(codexStubPath, '#!/bin/sh\nexit 0\n');
4761
+ await chmod(codexStubPath, 0o755);
4171
4762
  process.env.TMUX = 'leader-session,stub,0';
4172
4763
  process.env.TMUX_PANE = '%1';
4173
- process.env.OMX_TEAM_WORKER_CLI = 'gemini';
4174
- console.warn = (...args) => { warnings.push(args.map(String).join(' ')); };
4175
- const session = createTeamSession('Run Shell Fallback', 1, cwd);
4176
- assert.equal(session.hudPaneId, '%3');
4177
- assert.match(warnings.join('\n'), /HUD resize/);
4178
- assert.match(warnings.join('\n'), /Unsupported tmux compatibility command: run-shell/);
4764
+ process.env.OMX_TEAM_WORKER_CLI = 'codex';
4765
+ process.env[OMX_ENTRY_PATH_ENV] = join(cwd, 'omx.js');
4766
+ process.argv = [previousArgv[0] || 'node', join(cwd, 'omx.js')];
4767
+ let caught;
4768
+ try {
4769
+ createTeamSession('No Resource Proof Loss', 1, cwd);
4770
+ }
4771
+ catch (error) {
4772
+ caught = error;
4773
+ }
4179
4774
  const tmuxLog = await readFile(logPath, 'utf-8');
4180
- assert.match(tmuxLog, /run-shell -b sleep/);
4181
- assert.doesNotMatch(tmuxLog, /kill-pane -t %2/);
4182
- assert.doesNotMatch(tmuxLog, /kill-pane -t %3/);
4775
+ assert.equal(caught.message, 'exact_pane_proof_unavailable:%1:malformed_snapshot', tmuxLog);
4776
+ assert.ok(!(caught instanceof CreateTeamSessionPartialError));
4777
+ assert.match(tmuxLog, /list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}/);
4778
+ assert.doesNotMatch(tmuxLog, /split-window|resize-pane|select-pane|send-keys/);
4183
4779
  });
4184
4780
  }
4185
4781
  finally {
4186
- console.warn = prevWarn;
4187
- if (typeof prevTmux === 'string')
4188
- process.env.TMUX = prevTmux;
4782
+ if (typeof previousTmux === 'string')
4783
+ process.env.TMUX = previousTmux;
4189
4784
  else
4190
4785
  delete process.env.TMUX;
4191
- if (typeof prevTmuxPane === 'string')
4192
- process.env.TMUX_PANE = prevTmuxPane;
4786
+ if (typeof previousTmuxPane === 'string')
4787
+ process.env.TMUX_PANE = previousTmuxPane;
4193
4788
  else
4194
4789
  delete process.env.TMUX_PANE;
4195
- if (typeof prevWorkerCli === 'string')
4196
- process.env.OMX_TEAM_WORKER_CLI = prevWorkerCli;
4790
+ if (typeof previousWorkerCli === 'string')
4791
+ process.env.OMX_TEAM_WORKER_CLI = previousWorkerCli;
4197
4792
  else
4198
4793
  delete process.env.OMX_TEAM_WORKER_CLI;
4794
+ if (typeof previousEntryPath === 'string')
4795
+ process.env[OMX_ENTRY_PATH_ENV] = previousEntryPath;
4796
+ else
4797
+ delete process.env[OMX_ENTRY_PATH_ENV];
4798
+ process.argv = previousArgv;
4199
4799
  await rm(cwd, { recursive: true, force: true });
4200
4800
  }
4201
4801
  });
4202
- });
4203
- describe('native Windows HUD reconciliation', () => {
4204
- it('allows team startup on native Windows when current tmux client is reachable without TMUX env vars', async () => {
4205
- const cwd = await mkdtemp(join(tmpdir(), 'omx-team-win32-no-env-'));
4206
- const prevTmux = process.env.TMUX;
4207
- const prevTmuxPane = process.env.TMUX_PANE;
4208
- const prevWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
4209
- const prevMsystem = process.env.MSYSTEM;
4210
- const prevOstype = process.env.OSTYPE;
4211
- const prevWsl = process.env.WSL_DISTRO_NAME;
4212
- const prevWslInterop = process.env.WSL_INTEROP;
4213
- const origPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
4802
+ it('rejects duplicate pane IDs in successful create topology before mutation', async () => {
4803
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-duplicate-topology-'));
4804
+ const previousTmux = process.env.TMUX;
4805
+ const previousTmuxPane = process.env.TMUX_PANE;
4806
+ const previousWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
4214
4807
  try {
4215
- await withMockTmuxFixture('omx-tmux-win32-no-env-', (logPath) => `#!/bin/sh
4808
+ await withMockTmuxFixture('omx-tmux-duplicate-topology-', (logPath) => `#!/bin/sh
4216
4809
  set -eu
4217
4810
  printf '%s\\n' "$*" >> "${logPath}"
4218
- case "\${1:-}" in
4219
- -V)
4220
- echo "tmux 3.4"
4221
- exit 0
4222
- ;;
4223
- display-message)
4224
- case "$*" in
4225
- *"#{window_width}"*)
4226
- echo "120"
4227
- ;;
4228
- *)
4229
- echo "leader:0 %1"
4230
- ;;
4231
- esac
4232
- exit 0
4233
- ;;
4811
+ case "$1" in
4812
+ -V) echo "tmux 3.4" ;;
4813
+ display-message) echo "leader:0 %1" ;;
4234
4814
  list-panes)
4235
4815
  case "$*" in
4236
- *"pane_current_command"*)
4237
- printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n%%3\\tnode\\t'node omx hud --watch'\\n"
4238
- ;;
4239
- *)
4240
- printf "%%1\\n%%2\\n%%3\\n"
4241
- ;;
4242
- esac
4243
- exit 0
4244
- ;;
4245
- split-window)
4246
- case "$*" in
4247
- *" -h "*)
4248
- echo "%2"
4249
- ;;
4250
- *)
4251
- echo "%3"
4252
- ;;
4816
+ *"pane_current_command"*) printf '%%1\\tnode\\tcodex\\n%%1\\tnode\\tcodex\\n' ;;
4817
+ *) printf '%%1\\n' ;;
4253
4818
  esac
4254
- exit 0
4255
- ;;
4256
- resize-pane|select-layout|set-window-option|select-pane|kill-pane|set-hook|run-shell)
4257
- exit 0
4258
- ;;
4259
- *)
4260
- exit 0
4261
4819
  ;;
4820
+ *) exit 0 ;;
4262
4821
  esac
4263
4822
  `, async ({ logPath }) => {
4264
- const fakeBinDir = join(logPath, '..');
4265
- const geminiPath = join(fakeBinDir, 'gemini');
4823
+ const geminiPath = join(dirname(logPath), 'gemini');
4266
4824
  await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
4267
4825
  await chmod(geminiPath, 0o755);
4268
- delete process.env.TMUX;
4269
- delete process.env.TMUX_PANE;
4826
+ process.env.TMUX = 'leader-session,stub,0';
4827
+ process.env.TMUX_PANE = '%1';
4270
4828
  process.env.OMX_TEAM_WORKER_CLI = 'gemini';
4271
- delete process.env.MSYSTEM;
4272
- delete process.env.OSTYPE;
4273
- delete process.env.WSL_DISTRO_NAME;
4274
- delete process.env.WSL_INTEROP;
4275
- Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
4276
- const session = createTeamSession('Windows Team', 1, cwd);
4277
- assert.equal(session.name, 'leader:0');
4278
- assert.equal(session.leaderPaneId, '%1');
4279
- assert.equal(session.hudPaneId, '%3');
4829
+ assert.throws(() => createTeamSession('Duplicate topology', 1, cwd), /failed to read tmux pane topology: malformed pane topology/);
4280
4830
  const tmuxLog = await readFile(logPath, 'utf-8');
4281
- assert.match(tmuxLog, /display-message -p #\{session_name\}:#\{window_index\} #\{pane_id\}/);
4282
- assert.match(tmuxLog, /powershell\.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -EncodedCommand/);
4283
- assert.doesNotMatch(tmuxLog, /\/bin\/sh -lc/);
4284
- assert.match(tmuxLog, new RegExp(`resize-pane -t %3 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
4831
+ assert.doesNotMatch(tmuxLog, /set-option|split-window|resize-pane|select-pane|send-keys/);
4285
4832
  });
4286
4833
  }
4287
4834
  finally {
4288
- if (origPlatform)
4289
- Object.defineProperty(process, 'platform', origPlatform);
4290
- if (typeof prevTmux === 'string')
4291
- process.env.TMUX = prevTmux;
4835
+ if (typeof previousTmux === 'string')
4836
+ process.env.TMUX = previousTmux;
4292
4837
  else
4293
4838
  delete process.env.TMUX;
4294
- if (typeof prevTmuxPane === 'string')
4295
- process.env.TMUX_PANE = prevTmuxPane;
4839
+ if (typeof previousTmuxPane === 'string')
4840
+ process.env.TMUX_PANE = previousTmuxPane;
4296
4841
  else
4297
4842
  delete process.env.TMUX_PANE;
4298
- if (typeof prevWorkerCli === 'string')
4299
- process.env.OMX_TEAM_WORKER_CLI = prevWorkerCli;
4843
+ if (typeof previousWorkerCli === 'string')
4844
+ process.env.OMX_TEAM_WORKER_CLI = previousWorkerCli;
4300
4845
  else
4301
4846
  delete process.env.OMX_TEAM_WORKER_CLI;
4302
- if (typeof prevMsystem === 'string')
4303
- process.env.MSYSTEM = prevMsystem;
4304
- else
4305
- delete process.env.MSYSTEM;
4306
- if (typeof prevOstype === 'string')
4307
- process.env.OSTYPE = prevOstype;
4308
- else
4309
- delete process.env.OSTYPE;
4310
- if (typeof prevWsl === 'string')
4311
- process.env.WSL_DISTRO_NAME = prevWsl;
4312
- else
4313
- delete process.env.WSL_DISTRO_NAME;
4314
- if (typeof prevWslInterop === 'string')
4315
- process.env.WSL_INTEROP = prevWslInterop;
4316
- else
4317
- delete process.env.WSL_INTEROP;
4318
4847
  await rm(cwd, { recursive: true, force: true });
4319
4848
  }
4320
4849
  });
4321
- it('avoids nested tmux run-shell hooks during team HUD startup on native Windows', async () => {
4322
- const cwd = await mkdtemp(join(tmpdir(), 'omx-team-win32-hud-'));
4323
- const prevTmux = process.env.TMUX;
4324
- const prevTmuxPane = process.env.TMUX_PANE;
4325
- const prevWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
4326
- const prevMsystem = process.env.MSYSTEM;
4327
- const prevOstype = process.env.OSTYPE;
4328
- const prevWsl = process.env.WSL_DISTRO_NAME;
4329
- const prevWslInterop = process.env.WSL_INTEROP;
4330
- const origPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
4850
+ it('guards duplicate standalone HUD removal with a fresh global pane proof', async () => {
4851
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-duplicate-hud-global-proof-'));
4331
4852
  try {
4332
- await withMockTmuxFixture('omx-tmux-win32-hud-reconcile-', (logPath) => `#!/bin/sh
4853
+ await withMockTmuxFixture('omx-tmux-duplicate-hud-global-proof-', (logPath) => {
4854
+ const proofStatePath = `${logPath}.proof-state`;
4855
+ return `#!/bin/sh
4333
4856
  set -eu
4334
4857
  printf '%s\\n' "$*" >> "${logPath}"
4335
- case "\${1:-}" in
4336
- -V)
4337
- echo "tmux 3.4"
4338
- exit 0
4339
- ;;
4340
- display-message)
4858
+ case "$1" in
4859
+ list-panes)
4341
4860
  case "$*" in
4342
- *"#{window_width}"*)
4343
- echo "120"
4861
+ *"-a -F #{pane_id}"*)
4862
+ proof_count=0
4863
+ if [ -f "${proofStatePath}" ]; then proof_count=$(cat "${proofStatePath}"); fi
4864
+ proof_count=$((proof_count + 1))
4865
+ printf '%s' "$proof_count" > "${proofStatePath}"
4866
+ if [ "$proof_count" -eq 15 ]; then
4867
+ printf 'not-a-pane-snapshot\n'
4868
+ else
4869
+ printf "%%11\t0\t2000000011\n%%44\t0\t2000000044\n"
4870
+ if [ ! -f "${logPath}.killed-%45" ]; then printf "%%45\t0\t2000000045\n"; fi
4871
+ fi
4344
4872
  ;;
4345
4873
  *)
4346
- echo "leader:0 %1"
4874
+ printf "%%11\\tzsh\\tzsh\\n"
4875
+ printf "%%44\\tnode\\texec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%%11' /node /omx.js hud --watch\\n"
4876
+ printf "%%45\\tnode\\texec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%%11' /node /omx.js hud --watch\\n"
4347
4877
  ;;
4348
4878
  esac
4349
4879
  exit 0
4350
4880
  ;;
4351
- list-panes)
4352
- case "$*" in
4353
- *"pane_current_command"*)
4354
- printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n%%3\\tnode\\t'node omx hud --watch'\\n"
4355
- ;;
4356
- *)
4357
- printf "%%1\\n%%2\\n%%3\\n"
4358
- ;;
4359
- esac
4881
+ kill-pane)
4882
+ : > "${logPath}.killed-$3"
4360
4883
  exit 0
4361
4884
  ;;
4362
- split-window)
4363
- case "$*" in
4364
- *" -h "*)
4365
- echo "%2"
4366
- ;;
4367
- *)
4368
- echo "%3"
4369
- ;;
4370
- esac
4885
+ run-shell|select-pane|resize-pane)
4371
4886
  exit 0
4372
4887
  ;;
4373
- resize-pane|select-layout|set-window-option|select-pane|kill-pane)
4888
+ *)
4374
4889
  exit 0
4375
4890
  ;;
4376
- set-hook|run-shell)
4891
+ esac
4892
+ `;
4893
+ }, async ({ logPath }) => {
4894
+ assert.equal(restoreStandaloneHudPane('%11', cwd), '%44');
4895
+ assert.throws(() => restoreStandaloneHudPane('%11', cwd), /exact_pane_proof_unavailable:%45:malformed_snapshot/);
4896
+ const commands = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean);
4897
+ const duplicateHudKill = commands.indexOf('kill-pane -t %45');
4898
+ assert.ok(duplicateHudKill > 0);
4899
+ assert.match(commands[duplicateHudKill - 1] ?? '', /^list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}$/);
4900
+ assert.equal(commands.filter((command) => command === 'kill-pane -t %45').length, 1);
4901
+ });
4902
+ }
4903
+ finally {
4904
+ await rm(cwd, { recursive: true, force: true });
4905
+ }
4906
+ });
4907
+ it('rejects a replaced leader PID or owner before standalone HUD topology discovery', async () => {
4908
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-hud-entry-authorization-'));
4909
+ try {
4910
+ await withMockTmuxFixture('omx-tmux-standalone-hud-entry-authorization-', (logPath) => `#!/bin/sh
4911
+ set -eu
4912
+ printf '%s\\n' "$*" >> "${logPath}"
4913
+ case "$1" in
4914
+ list-panes)
4915
+ if [ "$2" = "-a" ]; then
4916
+ proof_count=0
4917
+ [ -f "${logPath}.proof-count" ] && proof_count=$(cat "${logPath}.proof-count")
4918
+ proof_count=$((proof_count + 1))
4919
+ printf '%s' "$proof_count" > "${logPath}.proof-count"
4920
+ if [ "$proof_count" -eq 1 ]; then printf '%%11\\t0\\t2000000011\\n'; else printf '%%11\\t0\\t2000000099\\n'; fi
4921
+ else
4922
+ printf '%%11\\tzsh\\tzsh\\n%%44\\tnode\\texec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE=%%11 node /omx.js hud --watch\\n'
4923
+ fi
4924
+ ;;
4925
+ split-window) printf '%%45\\n' ;;
4926
+ *) exit 0 ;;
4927
+ esac
4928
+ `, async ({ logPath }) => {
4929
+ let ownerAuthorizationChecks = 0;
4930
+ assert.throws(() => restoreStandaloneHudPane('%11', cwd, {
4931
+ expectedLeaderPanePid: 2000000011,
4932
+ assertLeaderPaneAuthorization: () => {
4933
+ ownerAuthorizationChecks += 1;
4934
+ throw new Error('leader owner changed');
4935
+ },
4936
+ }), /leader owner changed/);
4937
+ assert.equal(ownerAuthorizationChecks, 1);
4938
+ const commands = await readFile(logPath, 'utf-8');
4939
+ assert.doesNotMatch(commands, /list-panes -t %11|kill-pane|split-window|resize-pane|select-pane/);
4940
+ });
4941
+ }
4942
+ finally {
4943
+ await rm(cwd, { recursive: true, force: true });
4944
+ }
4945
+ });
4946
+ it('fails closed before standalone HUD effects when leader topology query fails', async () => {
4947
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-hud-topology-query-failure-'));
4948
+ try {
4949
+ await withMockTmuxFixture('omx-tmux-standalone-hud-topology-query-failure-', (logPath) => `#!/bin/sh
4950
+ set -eu
4951
+ printf '%s\\n' "$*" >> "${logPath}"
4952
+ case "$1" in
4953
+ list-panes)
4954
+ if [ "$2" = "-a" ]; then
4955
+ printf '%%11\\t0\\t2000000011\\n'
4956
+ exit 0
4957
+ fi
4958
+ printf 'leader topology unavailable\\n' >&2
4959
+ exit 1
4960
+ ;;
4961
+ kill-pane|split-window|resize-pane|select-pane)
4962
+ exit 99
4963
+ ;;
4964
+ *) exit 0 ;;
4965
+ esac
4966
+ `, async ({ logPath }) => {
4967
+ assert.throws(() => restoreStandaloneHudPane('%11', cwd), /failed to read tmux pane topology: leader topology unavailable/);
4968
+ const commands = await readFile(logPath, 'utf-8');
4969
+ assert.doesNotMatch(commands, /kill-pane|split-window|resize-pane|select-pane/);
4970
+ });
4971
+ }
4972
+ finally {
4973
+ await rm(cwd, { recursive: true, force: true });
4974
+ }
4975
+ });
4976
+ it('fails closed before standalone HUD effects when leader topology is malformed', async () => {
4977
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-hud-topology-malformed-'));
4978
+ try {
4979
+ await withMockTmuxFixture('omx-tmux-standalone-hud-topology-malformed-', (logPath) => `#!/bin/sh
4980
+ set -eu
4981
+ printf '%s\\n' "$*" >> "${logPath}"
4982
+ case "$1" in
4983
+ list-panes)
4984
+ if [ "$2" = "-a" ]; then
4985
+ printf '%%11\\t0\\t2000000011\\n'
4986
+ else
4987
+ printf '%%11\\tzsh\\n'
4988
+ fi
4377
4989
  exit 0
4378
4990
  ;;
4379
- *)
4991
+ kill-pane|split-window|resize-pane|select-pane)
4992
+ exit 99
4993
+ ;;
4994
+ *) exit 0 ;;
4995
+ esac
4996
+ `, async ({ logPath }) => {
4997
+ assert.throws(() => restoreStandaloneHudPane('%11', cwd), /failed to read tmux pane topology: malformed pane topology/);
4998
+ const commands = await readFile(logPath, 'utf-8');
4999
+ assert.doesNotMatch(commands, /kill-pane|split-window|resize-pane|select-pane/);
5000
+ });
5001
+ }
5002
+ finally {
5003
+ await rm(cwd, { recursive: true, force: true });
5004
+ }
5005
+ });
5006
+ it('fails closed before a standalone HUD split when the adjacent leader proof is unavailable', async () => {
5007
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-hud-proof-loss-'));
5008
+ try {
5009
+ await withMockTmuxFixture('omx-tmux-standalone-hud-proof-loss-', (logPath) => `#!/bin/sh
5010
+ set -eu
5011
+ printf '%s\\n' "$*" >> "${logPath}"
5012
+ case "$1" in
5013
+ list-panes)
5014
+ if [ "$2" = "-a" ]; then
5015
+ count=0
5016
+ if [ -f "${logPath}.proof-count" ]; then count=$(cat "${logPath}.proof-count"); fi
5017
+ count=$((count + 1))
5018
+ printf '%s' "$count" > "${logPath}.proof-count"
5019
+ if [ "$count" -eq 1 ]; then printf '%%11\\t0\\t2000000011\\n'; else printf 'not-a-pane-snapshot\\n'; fi
5020
+ fi
4380
5021
  exit 0
4381
5022
  ;;
5023
+ split-window) echo '%44' ;;
5024
+ *) exit 0 ;;
4382
5025
  esac
4383
5026
  `, async ({ logPath }) => {
4384
- const fakeBinDir = join(logPath, '..');
4385
- const geminiPath = join(fakeBinDir, 'gemini');
5027
+ assert.throws(() => restoreStandaloneHudPane('%11', cwd), /exact_pane_proof_unavailable:%11:malformed_snapshot/);
5028
+ const commands = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean);
5029
+ assert.doesNotMatch(commands.join('\n'), /split-window|resize-pane|select-pane/);
5030
+ });
5031
+ }
5032
+ finally {
5033
+ await rm(cwd, { recursive: true, force: true });
5034
+ }
5035
+ });
5036
+ it('does not adopt a leader PID replacement before standalone HUD topology discovery', async () => {
5037
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-hud-leader-pid-reuse-'));
5038
+ try {
5039
+ await withMockTmuxFixture('omx-tmux-standalone-hud-leader-pid-reuse-', (logPath) => `#!/bin/sh
5040
+ set -eu
5041
+ printf '%s\\n' "$*" >> "${logPath}"
5042
+ case "$1" in
5043
+ list-panes)
5044
+ if [ "$2" = "-a" ]; then
5045
+ case "$*" in
5046
+ *"#{pane_pid}"*)
5047
+ count=0
5048
+ [ -f "${logPath}.proof-count" ] && count=$(cat "${logPath}.proof-count")
5049
+ count=$((count + 1))
5050
+ printf '%s' "$count" > "${logPath}.proof-count"
5051
+ if [ "$count" -eq 1 ]; then printf '%%11\\t0\\t2000000011\\n'; else printf '%%11\\t0\\t2000000099\\n'; fi
5052
+ ;;
5053
+ *) printf '%%11\\tzsh\\tzsh\\n' ;;
5054
+ esac
5055
+ fi
5056
+ exit 0
5057
+ ;;
5058
+ split-window) printf '%%44\\n' ;;
5059
+ *) exit 0 ;;
5060
+ esac
5061
+ `, async ({ logPath }) => {
5062
+ assert.throws(() => restoreStandaloneHudPane('%11', cwd, { expectedLeaderPanePid: 2000000011 }), /tmux pane identity changed: %11/);
5063
+ const commands = await readFile(logPath, 'utf-8');
5064
+ assert.doesNotMatch(commands, /list-panes -t %11|display-message|kill-pane|split-window|resize-pane|select-pane/);
5065
+ });
5066
+ }
5067
+ finally {
5068
+ await rm(cwd, { recursive: true, force: true });
5069
+ }
5070
+ });
5071
+ it('does not resize a PID-reused existing standalone HUD on POSIX', async () => {
5072
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-existing-hud-posix-pid-reuse-'));
5073
+ try {
5074
+ await withMockTmuxFixture('omx-tmux-standalone-existing-hud-posix-pid-reuse-', (logPath) => `#!/bin/sh
5075
+ set -eu
5076
+ printf '%s\\n' "$*" >> "${logPath}"
5077
+ case "$1" in
5078
+ list-panes)
5079
+ if [ "$2" = "-a" ]; then
5080
+ count=0
5081
+ if [ -f "${logPath}.proof-count" ]; then count=$(cat "${logPath}.proof-count"); fi
5082
+ count=$((count + 1))
5083
+ printf '%s' "$count" > "${logPath}.proof-count"
5084
+ if [ "$count" -ge 4 ]; then
5085
+ printf '%%11\\t0\\t2000000011\\n%%44\\t0\\t2000000099\\n'
5086
+ else
5087
+ printf '%%11\\t0\\t2000000011\\n%%44\\t0\\t2000000044\\n'
5088
+ fi
5089
+ else
5090
+ printf '%%11\\tzsh\\tzsh\\n%%44\\tnode\\texec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE=%%11 node /omx.js hud --watch\\n'
5091
+ fi
5092
+ ;;
5093
+ run-shell) /bin/sh -c "\${3:-$2}" ;;
5094
+ resize-pane|select-pane) exit 0 ;;
5095
+ *) exit 0 ;;
5096
+ esac
5097
+ `, async ({ logPath }) => {
5098
+ assert.equal(restoreStandaloneHudPane('%11', cwd), '%44');
5099
+ const commands = await readFile(logPath, 'utf-8');
5100
+ assert.doesNotMatch(commands, /^resize-pane/m);
5101
+ assert.match(commands, /\$3 == "2000000044"/);
5102
+ });
5103
+ }
5104
+ finally {
5105
+ await rm(cwd, { recursive: true, force: true });
5106
+ }
5107
+ });
5108
+ it('does not resize a PID-reused newly created standalone HUD on POSIX', async () => {
5109
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-new-hud-posix-pid-reuse-'));
5110
+ try {
5111
+ await withMockTmuxFixture('omx-tmux-standalone-new-hud-posix-pid-reuse-', (logPath) => `#!/bin/sh
5112
+ set -eu
5113
+ printf '%s\\n' "$*" >> "${logPath}"
5114
+ case "$1" in
5115
+ list-panes)
5116
+ if [ "$2" = "-a" ]; then
5117
+ count=0
5118
+ if [ -f "${logPath}.proof-count" ]; then count=$(cat "${logPath}.proof-count"); fi
5119
+ count=$((count + 1))
5120
+ printf '%s' "$count" > "${logPath}.proof-count"
5121
+ if [ "$count" -ge 6 ]; then
5122
+ printf '%%11\\t0\\t2000000011\\n%%44\\t0\\t2000000099\\n'
5123
+ else
5124
+ printf '%%11\\t0\\t2000000011\\n%%44\\t0\\t2000000044\\n'
5125
+ fi
5126
+ else
5127
+ printf '%%11\\tzsh\\tzsh\\n'
5128
+ fi
5129
+ ;;
5130
+ split-window) printf '%%44\\n' ;;
5131
+ run-shell) /bin/sh -c "\${3:-$2}" ;;
5132
+ resize-pane|select-pane) exit 0 ;;
5133
+ *) exit 0 ;;
5134
+ esac
5135
+ `, async ({ logPath }) => {
5136
+ assert.equal(restoreStandaloneHudPane('%11', cwd), '%44');
5137
+ const commands = await readFile(logPath, 'utf-8');
5138
+ assert.doesNotMatch(commands, /^resize-pane/m);
5139
+ assert.match(commands, /\$3 == "2000000044"/);
5140
+ });
5141
+ }
5142
+ finally {
5143
+ await rm(cwd, { recursive: true, force: true });
5144
+ }
5145
+ });
5146
+ it('rejects standalone HUD PID reuse before native resize', async () => {
5147
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-hud-pid-reuse-'));
5148
+ const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
5149
+ try {
5150
+ await withMockTmuxFixture('omx-tmux-standalone-hud-pid-reuse-', (logPath) => `#!/bin/sh
5151
+ set -eu
5152
+ printf '%s\\n' "$*" >> "${logPath}"
5153
+ case "$1" in
5154
+ list-panes)
5155
+ if [ "$2" = "-a" ]; then
5156
+ count=0
5157
+ if [ -f "${logPath}.proof-count" ]; then count=$(cat "${logPath}.proof-count"); fi
5158
+ count=$((count + 1))
5159
+ printf '%s' "$count" > "${logPath}.proof-count"
5160
+ if [ "$count" -ge 4 ]; then
5161
+ printf '%%11\\t0\\t2000000011\\n%%44\\t0\\t2000000099\\n'
5162
+ else
5163
+ printf '%%11\\t0\\t2000000011\\n%%44\\t0\\t2000000044\\n'
5164
+ fi
5165
+ else
5166
+ printf '%%11\\tzsh\\tzsh\\n%%44\\tnode\\texec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE=%%11 node /omx.js hud --watch\\n'
5167
+ fi
5168
+ exit 0
5169
+ ;;
5170
+ resize-pane|select-pane) exit 0 ;;
5171
+ *) exit 0 ;;
5172
+ esac
5173
+ `, async ({ logPath }) => {
5174
+ Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
5175
+ assert.throws(() => restoreStandaloneHudPane('%11', cwd), /tmux pane identity changed: %44/);
5176
+ const commands = await readFile(logPath, 'utf-8');
5177
+ assert.doesNotMatch(commands, /resize-pane|select-pane/);
5178
+ });
5179
+ }
5180
+ finally {
5181
+ if (originalPlatform)
5182
+ Object.defineProperty(process, 'platform', originalPlatform);
5183
+ await rm(cwd, { recursive: true, force: true });
5184
+ }
5185
+ });
5186
+ it('reports failed rollback pane teardown as retryable partial-session cleanup debt', async () => {
5187
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-kill-failure-partial-'));
5188
+ const previousTmux = process.env.TMUX;
5189
+ const previousTmuxPane = process.env.TMUX_PANE;
5190
+ const previousWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
5191
+ try {
5192
+ await withMockTmuxFixture('omx-tmux-kill-failure-partial-', (logPath) => `#!/bin/sh
5193
+ set -eu
5194
+ printf '%s\\n' "$*" >> "${logPath}"
5195
+ case "$1" in
5196
+ display-message) echo 'leader:0 %1' ;;
5197
+ list-panes)
5198
+ if [ "$2" = "-a" ]; then printf '%%1\\t0\\t2000000001\\n%%2\\t0\\t2000000002\\n'; else printf '%%1\\tnode\\tcodex\\n'; fi
5199
+ ;;
5200
+ split-window)
5201
+ case "$*" in *' -h '*) echo '%2' ;; *) echo 'second split rejected' >&2; exit 1 ;; esac
5202
+ ;;
5203
+ kill-pane) echo 'kill rejected' >&2; exit 1 ;;
5204
+ *) exit 0 ;;
5205
+ esac
5206
+ `, async ({ logPath }) => {
5207
+ const geminiPath = join(dirname(logPath), 'gemini');
4386
5208
  await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
4387
5209
  await chmod(geminiPath, 0o755);
4388
5210
  process.env.TMUX = 'leader-session,stub,0';
4389
5211
  process.env.TMUX_PANE = '%1';
4390
5212
  process.env.OMX_TEAM_WORKER_CLI = 'gemini';
4391
- delete process.env.MSYSTEM;
4392
- delete process.env.OSTYPE;
4393
- delete process.env.WSL_DISTRO_NAME;
4394
- delete process.env.WSL_INTEROP;
4395
- Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
4396
- const session = createTeamSession('Windows Team', 1, cwd);
4397
- assert.equal(session.hudPaneId, '%3');
4398
- assert.equal(session.resizeHookName, null);
4399
- assert.equal(session.resizeHookTarget, null);
4400
- const tmuxLog = await readFile(logPath, 'utf-8');
4401
- assert.match(tmuxLog, new RegExp(`resize-pane -t %3 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
4402
- assert.doesNotMatch(tmuxLog, /set-hook -w /);
4403
- assert.doesNotMatch(tmuxLog, /window-resized\[/);
4404
- assert.doesNotMatch(tmuxLog, /set-hook -t leader:0 client-attached\[\d+\]/);
4405
- assert.doesNotMatch(tmuxLog, /run-shell -b sleep \d+; tmux resize-pane -t %3 -y \d+ >/);
4406
- assert.doesNotMatch(tmuxLog, /run-shell tmux resize-pane -t %3 -y \d+ >/);
5213
+ assert.throws(() => createTeamSession('Kill Failure Partial', 2, cwd), (error) => error instanceof CreateTeamSessionPartialError
5214
+ && error.partialSession.workerPaneIds.includes('%2')
5215
+ && error.cleanupErrors.some((message) => message.includes('failed to kill tmux pane %2')));
4407
5216
  });
4408
5217
  }
4409
5218
  finally {
4410
- if (origPlatform)
4411
- Object.defineProperty(process, 'platform', origPlatform);
4412
- if (typeof prevTmux === 'string')
4413
- process.env.TMUX = prevTmux;
5219
+ if (typeof previousTmux === 'string')
5220
+ process.env.TMUX = previousTmux;
4414
5221
  else
4415
5222
  delete process.env.TMUX;
4416
- if (typeof prevTmuxPane === 'string')
4417
- process.env.TMUX_PANE = prevTmuxPane;
5223
+ if (typeof previousTmuxPane === 'string')
5224
+ process.env.TMUX_PANE = previousTmuxPane;
4418
5225
  else
4419
5226
  delete process.env.TMUX_PANE;
4420
- if (typeof prevWorkerCli === 'string')
4421
- process.env.OMX_TEAM_WORKER_CLI = prevWorkerCli;
5227
+ if (typeof previousWorkerCli === 'string')
5228
+ process.env.OMX_TEAM_WORKER_CLI = previousWorkerCli;
4422
5229
  else
4423
5230
  delete process.env.OMX_TEAM_WORKER_CLI;
4424
- if (typeof prevMsystem === 'string')
4425
- process.env.MSYSTEM = prevMsystem;
4426
- else
4427
- delete process.env.MSYSTEM;
4428
- if (typeof prevOstype === 'string')
4429
- process.env.OSTYPE = prevOstype;
4430
- else
4431
- delete process.env.OSTYPE;
4432
- if (typeof prevWsl === 'string')
4433
- process.env.WSL_DISTRO_NAME = prevWsl;
4434
- else
4435
- delete process.env.WSL_DISTRO_NAME;
4436
- if (typeof prevWslInterop === 'string')
4437
- process.env.WSL_INTEROP = prevWslInterop;
4438
- else
4439
- delete process.env.WSL_INTEROP;
4440
5231
  await rm(cwd, { recursive: true, force: true });
4441
5232
  }
4442
5233
  });
4443
- it('rejects synthetic worker and HUD pane ids that never materialize on native Windows', async () => {
4444
- const cwd = await mkdtemp(join(tmpdir(), 'omx-team-win32-synthetic-pane-'));
5234
+ it('uses tmux 3.2a-compatible client-resized hook registration for team HUD resize', async () => {
5235
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-resize-hook-fallback-'));
4445
5236
  const prevTmux = process.env.TMUX;
4446
5237
  const prevTmuxPane = process.env.TMUX_PANE;
4447
5238
  const prevWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
4448
- const prevMsystem = process.env.MSYSTEM;
4449
- const prevOstype = process.env.OSTYPE;
4450
- const prevWsl = process.env.WSL_DISTRO_NAME;
4451
- const prevWslInterop = process.env.WSL_INTEROP;
4452
- const origPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
5239
+ const prevWarn = console.warn;
5240
+ const warnings = [];
4453
5241
  try {
4454
- await withMockTmuxFixture('omx-tmux-win32-synthetic-pane-', (logPath) => `#!/bin/sh
5242
+ await withMockTmuxFixture('omx-tmux-resize-hook-fallback-', (logPath) => `#!/bin/sh
4455
5243
  set -eu
4456
- printf '%s\\n' "$*" >> "${logPath}"
5244
+ printf '%s\n' "$*" >> "${logPath}"
4457
5245
  case "\${1:-}" in
4458
5246
  -V)
4459
- echo "tmux 3.3.2"
5247
+ echo "tmux 3.2a"
4460
5248
  exit 0
4461
5249
  ;;
4462
5250
  display-message)
@@ -4472,16 +5260,20 @@ case "\${1:-}" in
4472
5260
  ;;
4473
5261
  list-panes)
4474
5262
  case "$*" in
5263
+ *"-a -F #{pane_id}"*)
5264
+ printf "%%1\t0\t2000000001\n%%2\t0\t2000000002\n%%3\t0\t2000000003\n"
5265
+ ;;
4475
5266
  *"pane_current_command"*)
4476
- printf "%%1\\tnode\\t'codex'\\n"
5267
+ if [ -f "${logPath}.worker" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n"; else printf "%%1\\tnode\\t'codex'\\n"; fi
4477
5268
  ;;
4478
5269
  *)
4479
- printf "%%1\\n"
5270
+ if [ -f "${logPath}.worker" ]; then printf "%%1\\n%%2\\n"; else printf "%%1\\n"; fi
4480
5271
  ;;
4481
5272
  esac
4482
5273
  exit 0
4483
5274
  ;;
4484
5275
  split-window)
5276
+ : > "${logPath}.worker"
4485
5277
  case "$*" in
4486
5278
  *" -h "*)
4487
5279
  echo "%2"
@@ -4492,9 +5284,24 @@ case "\${1:-}" in
4492
5284
  esac
4493
5285
  exit 0
4494
5286
  ;;
4495
- kill-pane|select-layout|set-window-option|select-pane|resize-pane|set-hook|run-shell)
4496
- exit 0
4497
- ;;
5287
+ set-hook)
5288
+ case "$*" in
5289
+ *"window-resized["*)
5290
+ echo "invalid option: window-resized[]" >&2
5291
+ exit 1
5292
+ ;;
5293
+ *" -w "*)
5294
+ echo "invalid option: -w" >&2
5295
+ exit 1
5296
+ ;;
5297
+ *)
5298
+ exit 0
5299
+ ;;
5300
+ esac
5301
+ ;;
5302
+ set-option|resize-pane|select-layout|set-window-option|select-pane|run-shell)
5303
+ exit 0
5304
+ ;;
4498
5305
  *)
4499
5306
  exit 0
4500
5307
  ;;
@@ -4502,28 +5309,30 @@ esac
4502
5309
  `, async ({ logPath }) => {
4503
5310
  const fakeBinDir = join(logPath, '..');
4504
5311
  const geminiPath = join(fakeBinDir, 'gemini');
4505
- const powershellExePath = join(fakeBinDir, 'powershell.exe');
4506
5312
  await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
4507
5313
  await chmod(geminiPath, 0o755);
4508
- await writeFile(powershellExePath, '');
4509
5314
  process.env.TMUX = 'leader-session,stub,0';
4510
5315
  process.env.TMUX_PANE = '%1';
4511
5316
  process.env.OMX_TEAM_WORKER_CLI = 'gemini';
4512
- delete process.env.MSYSTEM;
4513
- delete process.env.OSTYPE;
4514
- delete process.env.WSL_DISTRO_NAME;
4515
- delete process.env.WSL_INTEROP;
4516
- Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
4517
- assert.throws(() => createTeamSession('Windows Team', 1, cwd), /worker pane 1 did not remain present/);
5317
+ console.warn = (...args) => { warnings.push(args.map(String).join(' ')); };
5318
+ const session = createTeamSession('Resize Hook Fallback', 1, cwd);
5319
+ assert.equal(session.hudPaneId, '%3');
5320
+ assert.ok(session.resizeHookName);
5321
+ assert.equal(session.resizeHookTarget, 'leader:0');
5322
+ assert.equal(warnings.join('\n'), '');
4518
5323
  const tmuxLog = await readFile(logPath, 'utf-8');
4519
- const listPaneCalls = tmuxLog.match(/list-panes -t leader:0 -F #\{pane_id\}\t#\{pane_current_command\}\t#\{pane_start_command\}/g) || [];
4520
- assert.ok(listPaneCalls.length >= 2, tmuxLog);
4521
- assert.match(tmuxLog, /kill-pane -t %2/);
5324
+ assert.match(tmuxLog, /set-hook -t leader:0 client-resized\[\d+\]/);
5325
+ assert.doesNotMatch(tmuxLog, /window-resized\[/);
5326
+ assert.doesNotMatch(tmuxLog, /set-hook -w /);
5327
+ assert.match(tmuxLog, /set-hook -t leader:0 client-attached\[\d+\]/);
5328
+ assert.match(tmuxLog, new RegExp(`run-shell -b sleep ${HUD_RESIZE_RECONCILE_DELAY_SECONDS}; .*resize-pane -t %3 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
5329
+ assert.match(tmuxLog, new RegExp(`run-shell .*resize-pane -t %3 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
5330
+ assert.doesNotMatch(tmuxLog, /kill-pane -t %2/);
5331
+ assert.doesNotMatch(tmuxLog, /kill-pane -t %3/);
4522
5332
  });
4523
5333
  }
4524
5334
  finally {
4525
- if (origPlatform)
4526
- Object.defineProperty(process, 'platform', origPlatform);
5335
+ console.warn = prevWarn;
4527
5336
  if (typeof prevTmux === 'string')
4528
5337
  process.env.TMUX = prevTmux;
4529
5338
  else
@@ -4536,162 +5345,257 @@ esac
4536
5345
  process.env.OMX_TEAM_WORKER_CLI = prevWorkerCli;
4537
5346
  else
4538
5347
  delete process.env.OMX_TEAM_WORKER_CLI;
4539
- if (typeof prevMsystem === 'string')
4540
- process.env.MSYSTEM = prevMsystem;
4541
- else
4542
- delete process.env.MSYSTEM;
4543
- if (typeof prevOstype === 'string')
4544
- process.env.OSTYPE = prevOstype;
4545
- else
4546
- delete process.env.OSTYPE;
4547
- if (typeof prevWsl === 'string')
4548
- process.env.WSL_DISTRO_NAME = prevWsl;
4549
- else
4550
- delete process.env.WSL_DISTRO_NAME;
4551
- if (typeof prevWslInterop === 'string')
4552
- process.env.WSL_INTEROP = prevWslInterop;
4553
- else
4554
- delete process.env.WSL_INTEROP;
4555
5348
  await rm(cwd, { recursive: true, force: true });
4556
5349
  }
4557
5350
  });
4558
- it('restores standalone HUD panes with direct resize on native Windows', async () => {
4559
- const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-win32-hud-'));
4560
- const prevLeaderNodePath = process.env.OMX_LEADER_NODE_PATH;
4561
- const prevMsystem = process.env.MSYSTEM;
4562
- const prevOstype = process.env.OSTYPE;
4563
- const prevWsl = process.env.WSL_DISTRO_NAME;
4564
- const prevWslInterop = process.env.WSL_INTEROP;
4565
- const origPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
5351
+ it('unregisters only the successfully registered HUD hook after partial hook registration failure', async () => {
5352
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-partial-hook-registration-'));
5353
+ const previousTmux = process.env.TMUX;
5354
+ const previousTmuxPane = process.env.TMUX_PANE;
5355
+ const previousWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
5356
+ const previousWarn = console.warn;
4566
5357
  try {
4567
- await withMockTmuxFixture('omx-tmux-win32-standalone-hud-', (logPath) => `#!/bin/sh
5358
+ await withMockTmuxFixture('omx-tmux-partial-hook-registration-', (logPath) => `#!/bin/sh
4568
5359
  set -eu
5360
+ registration_failed_file="${logPath}.registration-failed"
4569
5361
  printf '%s\\n' "$*" >> "${logPath}"
4570
5362
  case "\${1:-}" in
4571
- split-window)
4572
- echo "%44"
4573
- exit 0
5363
+ -V)
5364
+ echo 'tmux 3.4'
4574
5365
  ;;
4575
- resize-pane|select-pane)
4576
- exit 0
5366
+ display-message)
5367
+ case "$*" in
5368
+ *'#{window_width}'*) echo '120' ;;
5369
+ *) echo 'leader:0 %1' ;;
5370
+ esac
4577
5371
  ;;
4578
- set-hook|run-shell)
4579
- exit 0
5372
+ list-panes)
5373
+ case "$*" in
5374
+ *'pane_current_command'*)
5375
+ if [ -f "${logPath}.hud" ]; then
5376
+ printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n%%3\\tnode\\t'hud --watch'\\n"
5377
+ elif [ -f "${logPath}.worker" ]; then
5378
+ printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n"
5379
+ else
5380
+ printf "%%1\\tnode\\t'codex'\\n"
5381
+ fi
5382
+ ;;
5383
+ *'-a -F #{pane_id}'*)
5384
+ if [ -f "$registration_failed_file" ]; then
5385
+ printf 'malformed snapshot\n'
5386
+ else
5387
+ printf '%%1\t0\t2000000001\n%%2\t0\t2000000002\n%%3\t0\t2000000003\n'
5388
+ fi
5389
+ ;;
5390
+ *) if [ -f "${logPath}.hud" ]; then printf '%%1\n%%2\n%%3\n'; elif [ -f "${logPath}.worker" ]; then printf '%%1\n%%2\n'; else printf '%%1\n'; fi ;;
5391
+ esac
4580
5392
  ;;
4581
- *)
4582
- exit 0
5393
+ split-window)
5394
+ case "$*" in
5395
+ *' -h '*) : > "${logPath}.worker"; echo '%2' ;;
5396
+ *) : > "${logPath}.hud"; echo '%3' ;;
5397
+ esac
5398
+ ;;
5399
+ set-hook)
5400
+ case "$*" in
5401
+ *'client-attached['*) : > "$registration_failed_file"; echo 'client-attached hook registration failed' >&2; exit 1 ;;
5402
+ esac
4583
5403
  ;;
4584
5404
  esac
4585
5405
  `, async ({ logPath }) => {
4586
- delete process.env.MSYSTEM;
4587
- delete process.env.OSTYPE;
4588
- delete process.env.WSL_DISTRO_NAME;
4589
- delete process.env.WSL_INTEROP;
4590
- process.env.OMX_LEADER_NODE_PATH = 'C:\\Program Files\\nodejs\\node.exe';
4591
- Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
4592
- const paneId = restoreStandaloneHudPane('%11', cwd);
4593
- assert.equal(paneId, '%44');
4594
- const tmuxLog = await readFile(logPath, 'utf-8');
4595
- assert.match(tmuxLog, /'C:\\Program Files\\nodejs\\node\.exe'/);
4596
- assert.match(tmuxLog, new RegExp(`resize-pane -t %44 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
4597
- assert.match(tmuxLog, /select-pane -t %11/);
4598
- assert.doesNotMatch(tmuxLog, /run-shell -b sleep \d+; tmux resize-pane -t %44 -y \d+ >/);
4599
- assert.doesNotMatch(tmuxLog, /run-shell tmux resize-pane -t %44 -y \d+ >/);
4600
- assert.doesNotMatch(tmuxLog, /set-hook -t /);
5406
+ const geminiPath = join(dirname(logPath), 'gemini');
5407
+ await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
5408
+ await chmod(geminiPath, 0o755);
5409
+ process.env.TMUX = 'leader-session,stub,0';
5410
+ process.env.TMUX_PANE = '%1';
5411
+ process.env.OMX_TEAM_WORKER_CLI = 'gemini';
5412
+ console.warn = () => { };
5413
+ let caught;
5414
+ try {
5415
+ createTeamSession('Partial hook registration', 1, cwd);
5416
+ }
5417
+ catch (error) {
5418
+ caught = error;
5419
+ }
5420
+ assert.ok(caught instanceof CreateTeamSessionPartialError);
5421
+ assert.equal(caught.partialSession.hudPaneId, '%3');
5422
+ assert.equal(caught.partialSession.hudPanePid, 2000000003);
5423
+ assert.ok(caught.partialSession.resizeHookName);
5424
+ const commands = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean);
5425
+ const registeredResize = commands.filter((command) => command.startsWith('set-hook ') && command.includes('client-resized[') && !command.startsWith('set-hook -u '));
5426
+ const registeredAttached = commands.filter((command) => command.startsWith('set-hook ') && command.includes('client-attached[') && !command.startsWith('set-hook -u '));
5427
+ const unregisteredResize = commands.filter((command) => command.startsWith('set-hook -u ') && command.includes('client-resized['));
5428
+ const unregisteredAttached = commands.filter((command) => command.startsWith('set-hook -u ') && command.includes('client-attached['));
5429
+ assert.equal(registeredResize.length, 1);
5430
+ assert.equal(registeredAttached.length, 1);
5431
+ assert.equal(unregisteredResize.length, 0);
5432
+ assert.equal(unregisteredAttached.length, 0);
4601
5433
  });
4602
5434
  }
4603
5435
  finally {
4604
- if (origPlatform)
4605
- Object.defineProperty(process, 'platform', origPlatform);
4606
- if (typeof prevLeaderNodePath === 'string')
4607
- process.env.OMX_LEADER_NODE_PATH = prevLeaderNodePath;
4608
- else
4609
- delete process.env.OMX_LEADER_NODE_PATH;
4610
- if (typeof prevMsystem === 'string')
4611
- process.env.MSYSTEM = prevMsystem;
4612
- else
4613
- delete process.env.MSYSTEM;
4614
- if (typeof prevOstype === 'string')
4615
- process.env.OSTYPE = prevOstype;
5436
+ console.warn = previousWarn;
5437
+ if (typeof previousTmux === 'string')
5438
+ process.env.TMUX = previousTmux;
4616
5439
  else
4617
- delete process.env.OSTYPE;
4618
- if (typeof prevWsl === 'string')
4619
- process.env.WSL_DISTRO_NAME = prevWsl;
5440
+ delete process.env.TMUX;
5441
+ if (typeof previousTmuxPane === 'string')
5442
+ process.env.TMUX_PANE = previousTmuxPane;
4620
5443
  else
4621
- delete process.env.WSL_DISTRO_NAME;
4622
- if (typeof prevWslInterop === 'string')
4623
- process.env.WSL_INTEROP = prevWslInterop;
5444
+ delete process.env.TMUX_PANE;
5445
+ if (typeof previousWorkerCli === 'string')
5446
+ process.env.OMX_TEAM_WORKER_CLI = previousWorkerCli;
4624
5447
  else
4625
- delete process.env.WSL_INTEROP;
5448
+ delete process.env.OMX_TEAM_WORKER_CLI;
4626
5449
  await rm(cwd, { recursive: true, force: true });
4627
5450
  }
4628
5451
  });
4629
- it('reuses an existing standalone HUD pane across repeated restore calls', async () => {
4630
- const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-reuse-hud-'));
5452
+ it('degrades HUD run-shell resize failures to warnings during team startup', async () => {
5453
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-runshell-fallback-'));
5454
+ const prevTmux = process.env.TMUX;
5455
+ const prevTmuxPane = process.env.TMUX_PANE;
5456
+ const prevWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
5457
+ const prevWarn = console.warn;
5458
+ const warnings = [];
4631
5459
  try {
4632
- await withMockTmuxFixture('omx-tmux-reuse-standalone-hud-', (logPath) => {
4633
- const statePath = `${logPath}.state`;
4634
- return `#!/bin/sh
5460
+ await withMockTmuxFixture('omx-tmux-runshell-fallback-', (logPath) => `#!/bin/sh
4635
5461
  set -eu
4636
- printf '%s\\n' "$*" >> "${logPath}"
5462
+ printf '%s\n' "$*" >> "${logPath}"
4637
5463
  case "\${1:-}" in
5464
+ -V)
5465
+ echo "tmux 3.4"
5466
+ exit 0
5467
+ ;;
5468
+ display-message)
5469
+ case "$*" in
5470
+ *"#{window_width}"*) echo "120" ;;
5471
+ *) echo "leader:0 %1" ;;
5472
+ esac
5473
+ exit 0
5474
+ ;;
4638
5475
  list-panes)
4639
- printf '%%11\\tzsh\\tzsh\\n'
4640
- if [ -f "${statePath}" ]; then
4641
- printf "%%44\\tnode\\texec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%%11' /node /omx.js hud --watch\\n"
4642
- fi
5476
+ case "$*" in
5477
+ *"-a -F #{pane_id}"*)
5478
+ printf "%%1\t0\t2000000001\n%%2\t0\t2000000002\n%%3\t0\t2000000003\n"
5479
+ ;;
5480
+ *"pane_current_command"*) if [ -f "${logPath}.worker" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n"; else printf "%%1\\tnode\\t'codex'\\n"; fi ;;
5481
+ *) if [ -f "${logPath}.worker" ]; then printf "%%1\\n%%2\\n"; else printf "%%1\\n"; fi ;;
5482
+ esac
4643
5483
  exit 0
4644
5484
  ;;
4645
5485
  split-window)
4646
- : > "${statePath}"
4647
- echo "%44"
5486
+ : > "${logPath}.worker"
5487
+ case "$*" in
5488
+ *" -h "*) echo "%2" ;;
5489
+ *) echo "%3" ;;
5490
+ esac
4648
5491
  exit 0
4649
5492
  ;;
4650
- run-shell|select-pane|resize-pane|set-hook|kill-pane)
5493
+ run-shell)
5494
+ echo "Unsupported tmux compatibility command: run-shell" >&2
5495
+ exit 1
5496
+ ;;
5497
+ set-option|resize-pane|select-layout|set-window-option|select-pane|set-hook|send-keys)
4651
5498
  exit 0
4652
5499
  ;;
4653
5500
  *)
4654
5501
  exit 0
4655
5502
  ;;
4656
5503
  esac
4657
- `;
4658
- }, async ({ logPath }) => {
4659
- const firstPaneId = restoreStandaloneHudPane('%11', cwd);
4660
- const secondPaneId = restoreStandaloneHudPane('%11', cwd);
4661
- assert.equal(firstPaneId, '%44');
4662
- assert.equal(secondPaneId, '%44');
5504
+ `, async ({ logPath }) => {
5505
+ const fakeBinDir = join(logPath, '..');
5506
+ const geminiPath = join(fakeBinDir, 'gemini');
5507
+ await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
5508
+ await chmod(geminiPath, 0o755);
5509
+ process.env.TMUX = 'leader-session,stub,0';
5510
+ process.env.TMUX_PANE = '%1';
5511
+ process.env.OMX_TEAM_WORKER_CLI = 'gemini';
5512
+ console.warn = (...args) => { warnings.push(args.map(String).join(' ')); };
5513
+ const session = createTeamSession('Run Shell Fallback', 1, cwd);
5514
+ assert.equal(session.hudPaneId, '%3');
5515
+ assert.match(warnings.join('\n'), /HUD resize/);
5516
+ assert.match(warnings.join('\n'), /Unsupported tmux compatibility command: run-shell/);
4663
5517
  const tmuxLog = await readFile(logPath, 'utf-8');
4664
- const splitCount = (tmuxLog.match(/(^|\n)split-window /g) ?? []).length;
4665
- assert.equal(splitCount, 1);
4666
- assert.doesNotMatch(tmuxLog, /kill-pane -t %44/);
4667
- assert.match(tmuxLog, /list-panes -t %11 -F #\{pane_id\}\t#\{pane_current_command\}\t#\{pane_start_command\}/);
5518
+ assert.match(tmuxLog, /run-shell -b sleep/);
5519
+ assert.doesNotMatch(tmuxLog, /kill-pane -t %2/);
5520
+ assert.doesNotMatch(tmuxLog, /kill-pane -t %3/);
4668
5521
  });
4669
5522
  }
4670
5523
  finally {
5524
+ console.warn = prevWarn;
5525
+ if (typeof prevTmux === 'string')
5526
+ process.env.TMUX = prevTmux;
5527
+ else
5528
+ delete process.env.TMUX;
5529
+ if (typeof prevTmuxPane === 'string')
5530
+ process.env.TMUX_PANE = prevTmuxPane;
5531
+ else
5532
+ delete process.env.TMUX_PANE;
5533
+ if (typeof prevWorkerCli === 'string')
5534
+ process.env.OMX_TEAM_WORKER_CLI = prevWorkerCli;
5535
+ else
5536
+ delete process.env.OMX_TEAM_WORKER_CLI;
4671
5537
  await rm(cwd, { recursive: true, force: true });
4672
5538
  }
4673
5539
  });
4674
- it('restores standalone HUD panes from the live leader pane cwd with explicit session ownership', async () => {
4675
- const teamLaunchCwd = await mkdtemp(join(tmpdir(), 'omx-standalone-team-launch-cwd-'));
4676
- const leaderPaneCwd = await mkdtemp(join(tmpdir(), 'omx-standalone-leader-pane-cwd-'));
5540
+ });
5541
+ describe('native Windows HUD reconciliation', () => {
5542
+ it('allows team startup on native Windows when current tmux client is reachable without TMUX env vars', async () => {
5543
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-win32-no-env-'));
5544
+ const prevTmux = process.env.TMUX;
5545
+ const prevTmuxPane = process.env.TMUX_PANE;
5546
+ const prevWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
5547
+ const prevMsystem = process.env.MSYSTEM;
5548
+ const prevOstype = process.env.OSTYPE;
5549
+ const prevWsl = process.env.WSL_DISTRO_NAME;
5550
+ const prevWslInterop = process.env.WSL_INTEROP;
5551
+ const origPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
4677
5552
  try {
4678
- await withMockTmuxFixture('omx-tmux-leader-cwd-standalone-hud-', (logPath) => `#!/bin/sh
5553
+ await withMockTmuxFixture('omx-tmux-win32-no-env-', (logPath) => `#!/bin/sh
4679
5554
  set -eu
4680
5555
  printf '%s\\n' "$*" >> "${logPath}"
4681
5556
  case "\${1:-}" in
5557
+ -V)
5558
+ echo "tmux 3.4"
5559
+ exit 0
5560
+ ;;
4682
5561
  display-message)
4683
5562
  case "$*" in
4684
- *"#{pane_current_path}"*)
4685
- echo "${leaderPaneCwd}"
5563
+ *"#{window_width}"*)
5564
+ echo "120"
5565
+ ;;
5566
+ *)
5567
+ echo "leader:0 %1"
5568
+ ;;
5569
+ esac
5570
+ exit 0
5571
+ ;;
5572
+ list-panes)
5573
+ case "$*" in
5574
+ *"-a -F #{pane_id}"*)
5575
+ printf "%%1\\t0\\t2000000001\\n%%2\\t0\\t2000000002\\n%%3\\t0\\t2000000003\\n"
5576
+ ;;
5577
+ *"pane_current_command"*)
5578
+ if [ -f "${logPath}.hud" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n%%3\\tnode\\t'node omx hud --watch'\\n"; elif [ -f "${logPath}.worker" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n"; else printf "%%1\\tnode\\t'codex'\\n"; fi
5579
+ ;;
5580
+ *)
5581
+ if [ -f "${logPath}.hud" ]; then printf "%%1\\n%%2\\n%%3\\n"; elif [ -f "${logPath}.worker" ]; then printf "%%1\\n%%2\\n"; else printf "%%1\\n"; fi
4686
5582
  ;;
4687
5583
  esac
4688
5584
  exit 0
4689
5585
  ;;
4690
5586
  split-window)
4691
- echo "%44"
5587
+ if [ "$2" = "-h" ]; then : > "${logPath}.worker"; else : > "${logPath}.hud"; fi
5588
+ case "$*" in
5589
+ *" -h "*)
5590
+ echo "%2"
5591
+ ;;
5592
+ *)
5593
+ echo "%3"
5594
+ ;;
5595
+ esac
4692
5596
  exit 0
4693
5597
  ;;
4694
- run-shell|select-pane|resize-pane|set-hook)
5598
+ resize-pane|select-layout|set-window-option|select-pane|kill-pane|set-hook|run-shell)
4695
5599
  exit 0
4696
5600
  ;;
4697
5601
  *)
@@ -4699,56 +5603,123 @@ case "\${1:-}" in
4699
5603
  ;;
4700
5604
  esac
4701
5605
  `, async ({ logPath }) => {
4702
- const paneId = restoreStandaloneHudPane('%11', teamLaunchCwd, {
4703
- sessionId: 'current-session-for-hud',
4704
- });
4705
- assert.equal(paneId, '%44');
5606
+ const fakeBinDir = join(logPath, '..');
5607
+ const geminiPath = join(fakeBinDir, 'gemini');
5608
+ await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
5609
+ await chmod(geminiPath, 0o755);
5610
+ delete process.env.TMUX;
5611
+ delete process.env.TMUX_PANE;
5612
+ process.env.OMX_TEAM_WORKER_CLI = 'gemini';
5613
+ delete process.env.MSYSTEM;
5614
+ delete process.env.OSTYPE;
5615
+ delete process.env.WSL_DISTRO_NAME;
5616
+ delete process.env.WSL_INTEROP;
5617
+ Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
5618
+ const session = createTeamSession('Windows Team', 1, cwd);
5619
+ assert.equal(session.name, 'leader:0');
5620
+ assert.equal(session.leaderPaneId, '%1');
5621
+ assert.equal(session.hudPaneId, '%3');
4706
5622
  const tmuxLog = await readFile(logPath, 'utf-8');
4707
- assert.match(tmuxLog, /display-message -p -t %11 #\{pane_current_path\}/);
4708
- assert.match(tmuxLog, new RegExp(`split-window -v -l ${HUD_TMUX_TEAM_HEIGHT_LINES} -t %11 -d -P -F #\\{pane_id\\} -c ${escapeRegExp(leaderPaneCwd)} `));
4709
- assert.doesNotMatch(tmuxLog, new RegExp(`split-window .* -c ${escapeRegExp(teamLaunchCwd)} `));
4710
- assert.match(tmuxLog, /exec env OMX_SESSION_ID='current-session-for-hud' OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%11' .*hud --watch/);
5623
+ assert.match(tmuxLog, /display-message -p #\{session_name\}:#\{window_index\} #\{pane_id\}/);
5624
+ assert.match(tmuxLog, /powershell\.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -EncodedCommand/);
5625
+ assert.doesNotMatch(tmuxLog, /\/bin\/sh -lc/);
5626
+ assert.match(tmuxLog, new RegExp(`resize-pane -t %3 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
4711
5627
  });
4712
5628
  }
4713
5629
  finally {
4714
- await rm(teamLaunchCwd, { recursive: true, force: true });
4715
- await rm(leaderPaneCwd, { recursive: true, force: true });
5630
+ if (origPlatform)
5631
+ Object.defineProperty(process, 'platform', origPlatform);
5632
+ if (typeof prevTmux === 'string')
5633
+ process.env.TMUX = prevTmux;
5634
+ else
5635
+ delete process.env.TMUX;
5636
+ if (typeof prevTmuxPane === 'string')
5637
+ process.env.TMUX_PANE = prevTmuxPane;
5638
+ else
5639
+ delete process.env.TMUX_PANE;
5640
+ if (typeof prevWorkerCli === 'string')
5641
+ process.env.OMX_TEAM_WORKER_CLI = prevWorkerCli;
5642
+ else
5643
+ delete process.env.OMX_TEAM_WORKER_CLI;
5644
+ if (typeof prevMsystem === 'string')
5645
+ process.env.MSYSTEM = prevMsystem;
5646
+ else
5647
+ delete process.env.MSYSTEM;
5648
+ if (typeof prevOstype === 'string')
5649
+ process.env.OSTYPE = prevOstype;
5650
+ else
5651
+ delete process.env.OSTYPE;
5652
+ if (typeof prevWsl === 'string')
5653
+ process.env.WSL_DISTRO_NAME = prevWsl;
5654
+ else
5655
+ delete process.env.WSL_DISTRO_NAME;
5656
+ if (typeof prevWslInterop === 'string')
5657
+ process.env.WSL_INTEROP = prevWslInterop;
5658
+ else
5659
+ delete process.env.WSL_INTEROP;
5660
+ await rm(cwd, { recursive: true, force: true });
4716
5661
  }
4717
5662
  });
4718
- it('falls back to the team launch cwd when the live leader pane cwd is unusable', async () => {
4719
- const teamLaunchCwd = await mkdtemp(join(tmpdir(), 'omx-standalone-team-launch-cwd-fallback-'));
4720
- const deletedLeaderPaneCwd = await mkdtemp(join(tmpdir(), 'omx-standalone-deleted-leader-pane-cwd-'));
4721
- await rm(deletedLeaderPaneCwd, { recursive: true, force: true });
5663
+ it('avoids nested tmux run-shell hooks during team HUD startup on native Windows', async () => {
5664
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-win32-hud-'));
5665
+ const prevTmux = process.env.TMUX;
5666
+ const prevTmuxPane = process.env.TMUX_PANE;
5667
+ const prevWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
5668
+ const prevMsystem = process.env.MSYSTEM;
5669
+ const prevOstype = process.env.OSTYPE;
5670
+ const prevWsl = process.env.WSL_DISTRO_NAME;
5671
+ const prevWslInterop = process.env.WSL_INTEROP;
5672
+ const origPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
4722
5673
  try {
4723
- await withMockTmuxFixture('omx-tmux-deleted-leader-cwd-standalone-hud-', (logPath) => `#!/bin/sh
5674
+ await withMockTmuxFixture('omx-tmux-win32-hud-reconcile-', (logPath) => `#!/bin/sh
4724
5675
  set -eu
4725
5676
  printf '%s\\n' "$*" >> "${logPath}"
4726
5677
  case "\${1:-}" in
5678
+ -V)
5679
+ echo "tmux 3.4"
5680
+ exit 0
5681
+ ;;
4727
5682
  display-message)
4728
5683
  case "$*" in
4729
- *"#{pane_current_path}"*)
4730
- echo "${deletedLeaderPaneCwd}"
5684
+ *"#{window_width}"*)
5685
+ echo "120"
5686
+ ;;
5687
+ *)
5688
+ echo "leader:0 %1"
4731
5689
  ;;
4732
5690
  esac
4733
5691
  exit 0
4734
5692
  ;;
4735
- split-window)
5693
+ list-panes)
4736
5694
  case "$*" in
4737
- *"-c ${teamLaunchCwd} "*)
4738
- echo "%44"
4739
- exit 0
5695
+ *"-a -F #{pane_id}"*)
5696
+ printf "%%1\\t0\\t2000000001\\n%%2\\t0\\t2000000002\\n%%3\\t0\\t2000000003\\n"
4740
5697
  ;;
4741
- *"-c ${deletedLeaderPaneCwd} "*)
4742
- echo "deleted cwd should not be used" >&2
4743
- exit 1
5698
+ *"pane_current_command"*)
5699
+ if [ -f "${logPath}.hud" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n%%3\\tnode\\t'node omx hud --watch'\\n"; elif [ -f "${logPath}.worker" ]; then printf "%%1\\tnode\\t'codex'\\n%%2\\tgemini\\t'gemini'\\n"; else printf "%%1\\tnode\\t'codex'\\n"; fi
4744
5700
  ;;
4745
5701
  *)
4746
- echo "unexpected cwd: $*" >&2
4747
- exit 1
5702
+ if [ -f "${logPath}.hud" ]; then printf "%%1\\n%%2\\n%%3\\n"; elif [ -f "${logPath}.worker" ]; then printf "%%1\\n%%2\\n"; else printf "%%1\\n"; fi
4748
5703
  ;;
4749
5704
  esac
5705
+ exit 0
4750
5706
  ;;
4751
- run-shell|select-pane|resize-pane|set-hook)
5707
+ split-window)
5708
+ if [ "$2" = "-h" ]; then : > "${logPath}.worker"; else : > "${logPath}.hud"; fi
5709
+ case "$*" in
5710
+ *" -h "*)
5711
+ echo "%2"
5712
+ ;;
5713
+ *)
5714
+ echo "%3"
5715
+ ;;
5716
+ esac
5717
+ exit 0
5718
+ ;;
5719
+ resize-pane|select-layout|set-window-option|select-pane|kill-pane)
5720
+ exit 0
5721
+ ;;
5722
+ set-hook|run-shell)
4752
5723
  exit 0
4753
5724
  ;;
4754
5725
  *)
@@ -4756,71 +5727,126 @@ case "\${1:-}" in
4756
5727
  ;;
4757
5728
  esac
4758
5729
  `, async ({ logPath }) => {
4759
- const paneId = restoreStandaloneHudPane('%11', teamLaunchCwd, {
4760
- sessionId: 'current-session-for-hud',
4761
- });
4762
- assert.equal(paneId, '%44');
5730
+ const fakeBinDir = join(logPath, '..');
5731
+ const geminiPath = join(fakeBinDir, 'gemini');
5732
+ await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
5733
+ await chmod(geminiPath, 0o755);
5734
+ process.env.TMUX = 'leader-session,stub,0';
5735
+ process.env.TMUX_PANE = '%1';
5736
+ process.env.OMX_TEAM_WORKER_CLI = 'gemini';
5737
+ delete process.env.MSYSTEM;
5738
+ delete process.env.OSTYPE;
5739
+ delete process.env.WSL_DISTRO_NAME;
5740
+ delete process.env.WSL_INTEROP;
5741
+ Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
5742
+ const session = createTeamSession('Windows Team', 1, cwd);
5743
+ assert.equal(session.hudPaneId, '%3');
5744
+ assert.equal(session.resizeHookName, null);
5745
+ assert.equal(session.resizeHookTarget, null);
4763
5746
  const tmuxLog = await readFile(logPath, 'utf-8');
4764
- assert.match(tmuxLog, /display-message -p -t %11 #\{pane_current_path\}/);
4765
- assert.match(tmuxLog, new RegExp(`split-window -v -l ${HUD_TMUX_TEAM_HEIGHT_LINES} -t %11 -d -P -F #\\{pane_id\\} -c ${escapeRegExp(teamLaunchCwd)} `));
4766
- assert.doesNotMatch(tmuxLog, new RegExp(`split-window .* -c ${escapeRegExp(deletedLeaderPaneCwd)} `));
4767
- assert.match(tmuxLog, /select-pane -t %11/);
5747
+ assert.match(tmuxLog, new RegExp(`resize-pane -t %3 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
5748
+ assert.doesNotMatch(tmuxLog, /set-hook -w /);
5749
+ assert.doesNotMatch(tmuxLog, /window-resized\[/);
5750
+ assert.doesNotMatch(tmuxLog, /set-hook -t leader:0 client-attached\[\d+\]/);
5751
+ assert.doesNotMatch(tmuxLog, /run-shell -b sleep \d+; tmux resize-pane -t %3 -y \d+ >/);
5752
+ assert.doesNotMatch(tmuxLog, /run-shell tmux resize-pane -t %3 -y \d+ >/);
4768
5753
  });
4769
5754
  }
4770
5755
  finally {
4771
- await rm(teamLaunchCwd, { recursive: true, force: true });
5756
+ if (origPlatform)
5757
+ Object.defineProperty(process, 'platform', origPlatform);
5758
+ if (typeof prevTmux === 'string')
5759
+ process.env.TMUX = prevTmux;
5760
+ else
5761
+ delete process.env.TMUX;
5762
+ if (typeof prevTmuxPane === 'string')
5763
+ process.env.TMUX_PANE = prevTmuxPane;
5764
+ else
5765
+ delete process.env.TMUX_PANE;
5766
+ if (typeof prevWorkerCli === 'string')
5767
+ process.env.OMX_TEAM_WORKER_CLI = prevWorkerCli;
5768
+ else
5769
+ delete process.env.OMX_TEAM_WORKER_CLI;
5770
+ if (typeof prevMsystem === 'string')
5771
+ process.env.MSYSTEM = prevMsystem;
5772
+ else
5773
+ delete process.env.MSYSTEM;
5774
+ if (typeof prevOstype === 'string')
5775
+ process.env.OSTYPE = prevOstype;
5776
+ else
5777
+ delete process.env.OSTYPE;
5778
+ if (typeof prevWsl === 'string')
5779
+ process.env.WSL_DISTRO_NAME = prevWsl;
5780
+ else
5781
+ delete process.env.WSL_DISTRO_NAME;
5782
+ if (typeof prevWslInterop === 'string')
5783
+ process.env.WSL_INTEROP = prevWslInterop;
5784
+ else
5785
+ delete process.env.WSL_INTEROP;
5786
+ await rm(cwd, { recursive: true, force: true });
4772
5787
  }
4773
5788
  });
4774
- it('keeps MSYS drive-style live leader cwd candidates without raw stat prefiltering', async () => {
4775
- const liveLeaderCwd = '/c/live';
4776
- const fallbackCwd = 'C:\\fallback';
4777
- const fakeCygpathDir = await mkdtemp(join(tmpdir(), 'omx-msys-live-cygpath-'));
4778
- const previousPath = process.env.PATH;
4779
- const previousMsystem = process.env.MSYSTEM;
4780
- const previousOstype = process.env.OSTYPE;
4781
- const previousWsl = process.env.WSL_DISTRO_NAME;
4782
- const previousWslInterop = process.env.WSL_INTEROP;
4783
- const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
4784
- const statCalls = [];
5789
+ it('rejects synthetic worker and HUD pane ids that never materialize on native Windows', async () => {
5790
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-win32-synthetic-pane-'));
5791
+ const prevTmux = process.env.TMUX;
5792
+ const prevTmuxPane = process.env.TMUX_PANE;
5793
+ const prevWorkerCli = process.env.OMX_TEAM_WORKER_CLI;
5794
+ const prevMsystem = process.env.MSYSTEM;
5795
+ const prevOstype = process.env.OSTYPE;
5796
+ const prevWsl = process.env.WSL_DISTRO_NAME;
5797
+ const prevWslInterop = process.env.WSL_INTEROP;
5798
+ const origPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
4785
5799
  try {
4786
- const cygpathPath = join(fakeCygpathDir, 'cygpath');
4787
- await writeFile(cygpathPath, '#!/bin/sh\nprintf "%s\\n" "$2"\n');
4788
- await chmod(cygpathPath, 0o755);
4789
- process.env.PATH = `${fakeCygpathDir}:${previousPath ?? ''}`;
4790
- process.env.MSYSTEM = 'MINGW64';
4791
- process.env.OSTYPE = 'msys';
4792
- delete process.env.WSL_DISTRO_NAME;
4793
- delete process.env.WSL_INTEROP;
4794
- Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
4795
- await withMockTmuxFixture('omx-tmux-msys-live-cwd-standalone-hud-', (logPath) => `#!/bin/sh
5800
+ await withMockTmuxFixture('omx-tmux-win32-synthetic-pane-', (logPath) => `#!/bin/sh
4796
5801
  set -eu
4797
5802
  printf '%s\\n' "$*" >> "${logPath}"
4798
5803
  case "\${1:-}" in
5804
+ -V)
5805
+ echo "tmux 3.3.2"
5806
+ exit 0
5807
+ ;;
4799
5808
  display-message)
4800
5809
  case "$*" in
4801
- *"#{pane_current_path}"*)
4802
- echo "${liveLeaderCwd}"
5810
+ *"#{window_width}"*)
5811
+ echo "120"
5812
+ ;;
5813
+ *)
5814
+ echo "leader:0 %1"
4803
5815
  ;;
4804
5816
  esac
4805
5817
  exit 0
4806
5818
  ;;
4807
- split-window)
5819
+ list-panes)
4808
5820
  case "$*" in
4809
- *"-c ${liveLeaderCwd} "*)
4810
- echo "%44"
4811
- exit 0
5821
+ *"-a -F #{pane_id}"*)
5822
+ printf "%%1\t0\t2000000001\n"
5823
+ if [ ! -f "${logPath}.killed-%2" ]; then printf "%%2\t0\t2000000002\n"; fi
4812
5824
  ;;
4813
- *"-c ${fallbackCwd} "*)
4814
- echo "fallback cwd should not be used when MSYS live cwd succeeds" >&2
4815
- exit 1
5825
+ *"pane_current_command"*)
5826
+ printf "%%1\\tnode\\t'codex'\\n"
4816
5827
  ;;
4817
5828
  *)
4818
- echo "unexpected cwd: $*" >&2
4819
- exit 1
5829
+ printf "%%1\\n"
4820
5830
  ;;
4821
5831
  esac
5832
+ exit 0
4822
5833
  ;;
4823
- run-shell|select-pane|resize-pane|set-hook)
5834
+ split-window)
5835
+ case "$*" in
5836
+ *" -h "*)
5837
+ echo "%2"
5838
+ ;;
5839
+ *)
5840
+ echo "%3"
5841
+ ;;
5842
+ esac
5843
+ exit 0
5844
+ ;;
5845
+ kill-pane)
5846
+ : > "${logPath}.killed-$3"
5847
+ exit 0
5848
+ ;;
5849
+ select-layout|set-window-option|select-pane|resize-pane|set-hook|run-shell)
4824
5850
  exit 0
4825
5851
  ;;
4826
5852
  *)
@@ -4828,99 +5854,86 @@ case "\${1:-}" in
4828
5854
  ;;
4829
5855
  esac
4830
5856
  `, async ({ logPath }) => {
4831
- const paneId = withMockedStatSync(((pathLike) => {
4832
- const value = String(pathLike);
4833
- statCalls.push(value);
4834
- if (value === fallbackCwd)
4835
- return { isDirectory: () => true };
4836
- return { isDirectory: () => false };
4837
- }), () => restoreStandaloneHudPane('%11', fallbackCwd, { sessionId: 'current-session-for-hud' }));
4838
- assert.equal(paneId, '%44');
4839
- assert.equal(statCalls.includes(liveLeaderCwd), false);
4840
- assert.equal(statCalls.includes(fallbackCwd), true);
5857
+ const fakeBinDir = join(logPath, '..');
5858
+ const geminiPath = join(fakeBinDir, 'gemini');
5859
+ const powershellExePath = join(fakeBinDir, 'powershell.exe');
5860
+ await writeFile(geminiPath, '#!/bin/sh\nexit 0\n');
5861
+ await chmod(geminiPath, 0o755);
5862
+ await writeFile(powershellExePath, '');
5863
+ process.env.TMUX = 'leader-session,stub,0';
5864
+ process.env.TMUX_PANE = '%1';
5865
+ process.env.OMX_TEAM_WORKER_CLI = 'gemini';
5866
+ delete process.env.MSYSTEM;
5867
+ delete process.env.OSTYPE;
5868
+ delete process.env.WSL_DISTRO_NAME;
5869
+ delete process.env.WSL_INTEROP;
5870
+ Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
5871
+ assert.throws(() => createTeamSession('Windows Team', 1, cwd), /worker pane 1 did not remain present/);
4841
5872
  const tmuxLog = await readFile(logPath, 'utf-8');
4842
- assert.match(tmuxLog, /display-message -p -t %11 #\{pane_current_path\}/);
4843
- assert.match(tmuxLog, new RegExp(`split-window -v -l ${HUD_TMUX_TEAM_HEIGHT_LINES} -t %11 -d -P -F #\\{pane_id\\} -c ${escapeRegExp(liveLeaderCwd)} `));
4844
- assert.doesNotMatch(tmuxLog, new RegExp(`split-window .* -c ${escapeRegExp(fallbackCwd)} `));
4845
- assert.match(tmuxLog, /select-pane -t %11/);
5873
+ const listPaneCalls = tmuxLog.match(/list-panes -t leader:0 -F #\{pane_id\}\t#\{pane_current_command\}\t#\{pane_start_command\}/g) || [];
5874
+ assert.ok(listPaneCalls.length >= 2, tmuxLog);
5875
+ assert.match(tmuxLog, /kill-pane -t %2/);
4846
5876
  });
4847
5877
  }
4848
5878
  finally {
4849
- if (originalPlatform)
4850
- Object.defineProperty(process, 'platform', originalPlatform);
4851
- if (typeof previousPath === 'string')
4852
- process.env.PATH = previousPath;
5879
+ if (origPlatform)
5880
+ Object.defineProperty(process, 'platform', origPlatform);
5881
+ if (typeof prevTmux === 'string')
5882
+ process.env.TMUX = prevTmux;
4853
5883
  else
4854
- delete process.env.PATH;
4855
- if (typeof previousMsystem === 'string')
4856
- process.env.MSYSTEM = previousMsystem;
5884
+ delete process.env.TMUX;
5885
+ if (typeof prevTmuxPane === 'string')
5886
+ process.env.TMUX_PANE = prevTmuxPane;
5887
+ else
5888
+ delete process.env.TMUX_PANE;
5889
+ if (typeof prevWorkerCli === 'string')
5890
+ process.env.OMX_TEAM_WORKER_CLI = prevWorkerCli;
5891
+ else
5892
+ delete process.env.OMX_TEAM_WORKER_CLI;
5893
+ if (typeof prevMsystem === 'string')
5894
+ process.env.MSYSTEM = prevMsystem;
4857
5895
  else
4858
5896
  delete process.env.MSYSTEM;
4859
- if (typeof previousOstype === 'string')
4860
- process.env.OSTYPE = previousOstype;
5897
+ if (typeof prevOstype === 'string')
5898
+ process.env.OSTYPE = prevOstype;
4861
5899
  else
4862
5900
  delete process.env.OSTYPE;
4863
- if (typeof previousWsl === 'string')
4864
- process.env.WSL_DISTRO_NAME = previousWsl;
5901
+ if (typeof prevWsl === 'string')
5902
+ process.env.WSL_DISTRO_NAME = prevWsl;
4865
5903
  else
4866
5904
  delete process.env.WSL_DISTRO_NAME;
4867
- if (typeof previousWslInterop === 'string')
4868
- process.env.WSL_INTEROP = previousWslInterop;
5905
+ if (typeof prevWslInterop === 'string')
5906
+ process.env.WSL_INTEROP = prevWslInterop;
4869
5907
  else
4870
5908
  delete process.env.WSL_INTEROP;
4871
- await rm(fakeCygpathDir, { recursive: true, force: true });
5909
+ await rm(cwd, { recursive: true, force: true });
4872
5910
  }
4873
5911
  });
4874
- it('falls back after an unusable MSYS drive-style live leader cwd split attempt fails', async () => {
4875
- const liveLeaderCwd = '/c/deleted-live';
4876
- const fallbackCwd = 'C:\\fallback';
4877
- const fakeCygpathDir = await mkdtemp(join(tmpdir(), 'omx-msys-deleted-live-cygpath-'));
4878
- const previousPath = process.env.PATH;
4879
- const previousMsystem = process.env.MSYSTEM;
4880
- const previousOstype = process.env.OSTYPE;
4881
- const previousWsl = process.env.WSL_DISTRO_NAME;
4882
- const previousWslInterop = process.env.WSL_INTEROP;
4883
- const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
4884
- const statCalls = [];
5912
+ it('restores standalone HUD panes with direct resize on native Windows', async () => {
5913
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-win32-hud-'));
5914
+ const prevLeaderNodePath = process.env.OMX_LEADER_NODE_PATH;
5915
+ const prevMsystem = process.env.MSYSTEM;
5916
+ const prevOstype = process.env.OSTYPE;
5917
+ const prevWsl = process.env.WSL_DISTRO_NAME;
5918
+ const prevWslInterop = process.env.WSL_INTEROP;
5919
+ const origPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
4885
5920
  try {
4886
- const cygpathPath = join(fakeCygpathDir, 'cygpath');
4887
- await writeFile(cygpathPath, '#!/bin/sh\nprintf "%s\\n" "$2"\n');
4888
- await chmod(cygpathPath, 0o755);
4889
- process.env.PATH = `${fakeCygpathDir}:${previousPath ?? ''}`;
4890
- process.env.MSYSTEM = 'MINGW64';
4891
- process.env.OSTYPE = 'msys';
4892
- delete process.env.WSL_DISTRO_NAME;
4893
- delete process.env.WSL_INTEROP;
4894
- Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
4895
- await withMockTmuxFixture('omx-tmux-msys-deleted-live-cwd-standalone-hud-', (logPath) => `#!/bin/sh
5921
+ await withMockTmuxFixture('omx-tmux-win32-standalone-hud-', (logPath) => `#!/bin/sh
4896
5922
  set -eu
4897
5923
  printf '%s\\n' "$*" >> "${logPath}"
4898
5924
  case "\${1:-}" in
4899
- display-message)
4900
- case "$*" in
4901
- *"#{pane_current_path}"*)
4902
- echo "${liveLeaderCwd}"
4903
- ;;
4904
- esac
5925
+ list-panes)
5926
+ if [ "$2" = "-a" ]; then printf '%%11\\t0\\t2000000011\\n%%44\\t0\\t2000000044\\n'; fi
4905
5927
  exit 0
4906
5928
  ;;
4907
5929
  split-window)
4908
- case "$*" in
4909
- *"-c ${liveLeaderCwd} "*)
4910
- echo "deleted live cwd cannot be used" >&2
4911
- exit 1
4912
- ;;
4913
- *"-c ${fallbackCwd} "*)
4914
- echo "%44"
4915
- exit 0
4916
- ;;
4917
- *)
4918
- echo "unexpected cwd: $*" >&2
4919
- exit 1
4920
- ;;
4921
- esac
5930
+ echo "%44"
5931
+ exit 0
4922
5932
  ;;
4923
- run-shell|select-pane|resize-pane|set-hook)
5933
+ resize-pane|select-pane)
5934
+ exit 0
5935
+ ;;
5936
+ set-hook|run-shell)
4924
5937
  exit 0
4925
5938
  ;;
4926
5939
  *)
@@ -4928,110 +5941,310 @@ case "\${1:-}" in
4928
5941
  ;;
4929
5942
  esac
4930
5943
  `, async ({ logPath }) => {
4931
- const paneId = withMockedStatSync(((pathLike) => {
4932
- const value = String(pathLike);
4933
- statCalls.push(value);
4934
- if (value === fallbackCwd)
4935
- return { isDirectory: () => true };
4936
- return { isDirectory: () => false };
4937
- }), () => restoreStandaloneHudPane('%11', fallbackCwd, { sessionId: 'current-session-for-hud' }));
5944
+ delete process.env.MSYSTEM;
5945
+ delete process.env.OSTYPE;
5946
+ delete process.env.WSL_DISTRO_NAME;
5947
+ delete process.env.WSL_INTEROP;
5948
+ process.env.OMX_LEADER_NODE_PATH = 'C:\\Program Files\\nodejs\\node.exe';
5949
+ Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
5950
+ const paneId = restoreStandaloneHudPane('%11', cwd);
4938
5951
  assert.equal(paneId, '%44');
4939
- assert.equal(statCalls.includes(liveLeaderCwd), false);
4940
- assert.equal(statCalls.includes(fallbackCwd), true);
4941
5952
  const tmuxLog = await readFile(logPath, 'utf-8');
4942
- const liveAttemptIndex = tmuxLog.indexOf(`-c ${liveLeaderCwd} `);
4943
- const fallbackAttemptIndex = tmuxLog.indexOf(`-c ${fallbackCwd} `);
4944
- assert.ok(liveAttemptIndex >= 0, 'deleted MSYS live cwd should still be attempted');
4945
- assert.ok(fallbackAttemptIndex > liveAttemptIndex, 'fallback should be attempted after live split failure');
5953
+ assert.match(tmuxLog, /'C:\\Program Files\\nodejs\\node\.exe'/);
5954
+ assert.match(tmuxLog, new RegExp(`resize-pane -t %44 -y ${HUD_TMUX_TEAM_HEIGHT_LINES}`));
4946
5955
  assert.match(tmuxLog, /select-pane -t %11/);
5956
+ const commands = tmuxLog.trim().split('\n').filter(Boolean);
5957
+ for (const [index, command] of commands.entries()) {
5958
+ if (/^(split-window .* -t %11|resize-pane -t %44|select-pane -t %11)( |$)/.test(command)) {
5959
+ assert.match(commands[index - 1] ?? '', /^list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}$/, `fresh global proof must immediately precede ${command}`);
5960
+ }
5961
+ }
5962
+ assert.doesNotMatch(tmuxLog, /run-shell -b sleep \d+; tmux resize-pane -t %44 -y \d+ >/);
5963
+ assert.doesNotMatch(tmuxLog, /run-shell tmux resize-pane -t %44 -y \d+ >/);
5964
+ assert.doesNotMatch(tmuxLog, /set-hook -t /);
4947
5965
  });
4948
5966
  }
4949
5967
  finally {
4950
- if (originalPlatform)
4951
- Object.defineProperty(process, 'platform', originalPlatform);
4952
- if (typeof previousPath === 'string')
4953
- process.env.PATH = previousPath;
5968
+ if (origPlatform)
5969
+ Object.defineProperty(process, 'platform', origPlatform);
5970
+ if (typeof prevLeaderNodePath === 'string')
5971
+ process.env.OMX_LEADER_NODE_PATH = prevLeaderNodePath;
4954
5972
  else
4955
- delete process.env.PATH;
4956
- if (typeof previousMsystem === 'string')
4957
- process.env.MSYSTEM = previousMsystem;
5973
+ delete process.env.OMX_LEADER_NODE_PATH;
5974
+ if (typeof prevMsystem === 'string')
5975
+ process.env.MSYSTEM = prevMsystem;
4958
5976
  else
4959
5977
  delete process.env.MSYSTEM;
4960
- if (typeof previousOstype === 'string')
4961
- process.env.OSTYPE = previousOstype;
5978
+ if (typeof prevOstype === 'string')
5979
+ process.env.OSTYPE = prevOstype;
4962
5980
  else
4963
5981
  delete process.env.OSTYPE;
4964
- if (typeof previousWsl === 'string')
4965
- process.env.WSL_DISTRO_NAME = previousWsl;
5982
+ if (typeof prevWsl === 'string')
5983
+ process.env.WSL_DISTRO_NAME = prevWsl;
4966
5984
  else
4967
5985
  delete process.env.WSL_DISTRO_NAME;
4968
- if (typeof previousWslInterop === 'string')
4969
- process.env.WSL_INTEROP = previousWslInterop;
5986
+ if (typeof prevWslInterop === 'string')
5987
+ process.env.WSL_INTEROP = prevWslInterop;
4970
5988
  else
4971
5989
  delete process.env.WSL_INTEROP;
4972
- await rm(fakeCygpathDir, { recursive: true, force: true });
5990
+ await rm(cwd, { recursive: true, force: true });
4973
5991
  }
4974
5992
  });
4975
- it('restores standalone HUD panes with an absolute OMX entry path after cwd drift', async () => {
4976
- const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-relative-hud-'));
4977
- const startupCwd = await mkdtemp(join(tmpdir(), 'omx-standalone-relative-start-'));
4978
- const previousEntryPath = process.env[OMX_ENTRY_PATH_ENV];
4979
- const previousStartupCwd = process.env[OMX_STARTUP_CWD_ENV];
4980
- const previousArgv = process.argv;
5993
+ it('rejects restored HUD debt roots outside the canonical direct Team root before splitting', async () => {
5994
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-restored-hud-debt-root-'));
5995
+ const canonicalTeamsRoot = join(cwd, '.omx', 'state', 'team');
5996
+ const externalRoot = await mkdtemp(join(tmpdir(), 'omx-restored-hud-debt-external-'));
5997
+ const symlinkRoot = join(canonicalTeamsRoot, 'linked-team');
4981
5998
  try {
4982
- const launcherDir = join(startupCwd, 'dist', 'cli');
4983
- const launcherPath = join(launcherDir, 'omx.js');
4984
- await mkdir(launcherDir, { recursive: true });
4985
- await writeFile(launcherPath, '#!/usr/bin/env node\n');
4986
- await withMockTmuxFixture('omx-tmux-relative-standalone-hud-', (logPath) => `#!/bin/sh
5999
+ await mkdir(canonicalTeamsRoot, { recursive: true });
6000
+ await symlink(externalRoot, symlinkRoot);
6001
+ const invalidRoots = [
6002
+ join(cwd, '.omx', 'state', 'foreign'),
6003
+ join(canonicalTeamsRoot, 'alpha', '..', '..', 'foreign'),
6004
+ join(canonicalTeamsRoot, 'alpha', 'nested'),
6005
+ symlinkRoot,
6006
+ ];
6007
+ for (const stateRoot of invalidRoots) {
6008
+ assert.throws(() => restoreStandaloneHudPane('%11', cwd, { stateRoot }), /restored_hud_cleanup_debt_state_root_invalid/);
6009
+ }
6010
+ }
6011
+ finally {
6012
+ await rm(cwd, { recursive: true, force: true });
6013
+ await rm(externalRoot, { recursive: true, force: true });
6014
+ }
6015
+ });
6016
+ it('does not suppress a non-Windows parent-directory fsync failure for restored HUD debt', async () => {
6017
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-restored-hud-debt-fsync-'));
6018
+ const originalFsyncSync = fs.fsyncSync;
6019
+ try {
6020
+ await withMockTmuxFixture('omx-restored-hud-debt-fsync-', (logPath) => `#!/bin/sh
6021
+ set -eu
6022
+ printf '%s\\n' "$*" >> "${logPath}"
6023
+ case "$1" in
6024
+ list-panes)
6025
+ if [ "$2" = "-a" ]; then
6026
+ printf '%%11\\t0\\t2000000011\\n%%44\\t0\\t2000000044\\n'
6027
+ else
6028
+ printf '%%11\\tzsh\\tzsh\\n'
6029
+ fi
6030
+ ;;
6031
+ split-window) printf '%%44\\n' ;;
6032
+ *) exit 0 ;;
6033
+ esac
6034
+ `, async ({ logPath }) => {
6035
+ let fsyncCalls = 0;
6036
+ assert.throws(() => withMockedFsyncSync((descriptor) => {
6037
+ fsyncCalls += 1;
6038
+ if (fsyncCalls === 2) {
6039
+ const error = new Error('parent fsync unavailable');
6040
+ error.code = 'EINVAL';
6041
+ throw error;
6042
+ }
6043
+ originalFsyncSync(descriptor);
6044
+ }, () => restoreStandaloneHudPane('%11', cwd)), /parent fsync unavailable/);
6045
+ assert.equal(fsyncCalls, 2);
6046
+ const commands = await readFile(logPath, 'utf-8');
6047
+ assert.match(commands, /split-window/);
6048
+ assert.doesNotMatch(commands, /resize-pane|select-pane/);
6049
+ });
6050
+ }
6051
+ finally {
6052
+ await rm(cwd, { recursive: true, force: true });
6053
+ }
6054
+ });
6055
+ it('reuses an existing standalone HUD pane across repeated restore calls', async () => {
6056
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-reuse-hud-'));
6057
+ try {
6058
+ await withMockTmuxFixture('omx-tmux-reuse-standalone-hud-', (logPath) => {
6059
+ const statePath = `${logPath}.state`;
6060
+ return `#!/bin/sh
4987
6061
  set -eu
4988
6062
  printf '%s\\n' "$*" >> "${logPath}"
4989
6063
  case "\${1:-}" in
6064
+ list-panes)
6065
+ if [ "$2" = "-a" ]; then
6066
+ printf '%%11\t0\t2000000011\n'
6067
+ if [ -f "${statePath}" ]; then printf '%%44\t0\t2000000044\n'; fi
6068
+ else
6069
+ printf '%%11\tzsh\tzsh\n'
6070
+ if [ -f "${statePath}" ]; then
6071
+ printf "%%44\tnode\texec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%%11' /node /omx.js hud --watch\n"
6072
+ fi
6073
+ fi
6074
+ exit 0
6075
+ ;;
4990
6076
  split-window)
6077
+ : > "${statePath}"
4991
6078
  echo "%44"
4992
6079
  exit 0
4993
6080
  ;;
4994
- run-shell|select-pane|resize-pane|set-hook)
6081
+ run-shell|select-pane|resize-pane|set-hook|kill-pane)
4995
6082
  exit 0
4996
6083
  ;;
6084
+ show-option)
6085
+ if [ "$5" = "%11" ] && [ "$6" = "@omx_team_pane_owner_id" ]; then
6086
+ printf 'team:restore-replay\n'
6087
+ else
6088
+ exit 1
6089
+ fi
6090
+ ;;
4997
6091
  *)
4998
6092
  exit 0
4999
6093
  ;;
5000
6094
  esac
5001
- `, async ({ logPath }) => {
5002
- delete process.env[OMX_ENTRY_PATH_ENV];
5003
- process.env[OMX_STARTUP_CWD_ENV] = startupCwd;
5004
- process.argv = [previousArgv[0] || 'node', 'dist/cli/omx.js'];
5005
- const paneId = restoreStandaloneHudPane('%11', cwd);
5006
- assert.equal(paneId, '%44');
6095
+ `;
6096
+ }, async ({ logPath }) => {
6097
+ const firstPaneId = restoreStandaloneHudPane('%11', cwd, {
6098
+ expectedLeaderPanePid: 2000000011,
6099
+ expectedLeaderPaneOwnerId: 'team:restore-replay',
6100
+ });
6101
+ assert.equal(firstPaneId, '%44');
6102
+ const debtPath = join(cwd, '.omx', 'state', '.restored-hud-cleanup-debt.json');
6103
+ const crashWindowDebt = JSON.parse(await readFile(debtPath, 'utf-8'));
6104
+ assert.deepEqual(crashWindowDebt, {
6105
+ schema_version: 1,
6106
+ operation: 'restored_hud_cleanup',
6107
+ pane_id: '%44',
6108
+ pane_pid: 2000000044,
6109
+ leader_pane_id: '%11',
6110
+ leader_pane_pid: 2000000011,
6111
+ leader_pane_owner_id: 'team:restore-replay',
6112
+ hud_owner_leader_pane_id: '%11',
6113
+ });
6114
+ // The split may have survived a crash immediately before the config
6115
+ // transaction; finalization is deliberately explicit and post-commit.
6116
+ finalizeRestoredHudCleanupDebtSync(cwd, '%44', 2000000044);
6117
+ await assert.rejects(() => readFile(debtPath, 'utf-8'));
6118
+ const secondPaneId = restoreStandaloneHudPane('%11', cwd);
6119
+ assert.equal(firstPaneId, '%44');
6120
+ assert.equal(secondPaneId, '%44');
5007
6121
  const tmuxLog = await readFile(logPath, 'utf-8');
5008
- assert.match(tmuxLog, new RegExp(escapeRegExp(launcherPath)));
5009
- assert.doesNotMatch(tmuxLog, /'dist\/cli\/omx\.js' hud --watch/);
5010
- assert.match(tmuxLog, /exec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%11' .*hud --watch/);
6122
+ const splitCount = (tmuxLog.match(/(^|\n)split-window /g) ?? []).length;
6123
+ assert.equal(splitCount, 1);
6124
+ assert.doesNotMatch(tmuxLog, /kill-pane -t %44/);
6125
+ assert.match(tmuxLog, /list-panes -t %11 -F #\{pane_id\}\t#\{pane_current_command\}\t#\{pane_start_command\}/);
5011
6126
  });
5012
6127
  }
5013
6128
  finally {
5014
- process.argv = previousArgv;
5015
- if (typeof previousEntryPath === 'string')
5016
- process.env[OMX_ENTRY_PATH_ENV] = previousEntryPath;
5017
- else
5018
- delete process.env[OMX_ENTRY_PATH_ENV];
5019
- if (typeof previousStartupCwd === 'string')
5020
- process.env[OMX_STARTUP_CWD_ENV] = previousStartupCwd;
5021
- else
5022
- delete process.env[OMX_STARTUP_CWD_ENV];
5023
6129
  await rm(cwd, { recursive: true, force: true });
5024
- await rm(startupCwd, { recursive: true, force: true });
5025
6130
  }
5026
6131
  });
5027
- it('restores standalone HUD panes with the packaged CLI entry when argv1 is not the OMX CLI', async () => {
5028
- const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-noncli-hud-'));
5029
- const previousArgv = process.argv;
6132
+ it('retains restored-HUD debt for a same-ID/PID non-HUD replacement after leader authorization', async () => {
6133
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-restored-hud-final-command-change-'));
5030
6134
  try {
5031
- await withMockTmuxFixture('omx-tmux-noncli-standalone-hud-', (logPath) => `#!/bin/sh
6135
+ await withMockTmuxFixture('omx-tmux-restored-hud-final-command-change-', (logPath) => `#!/bin/sh
6136
+ set -eu
6137
+ printf '%s\\n' "$*" >> "${logPath}"
6138
+ case "\${1:-}" in
6139
+ list-panes)
6140
+ if [ "$2" = '-a' ]; then
6141
+ printf '%%11\\t0\\t2000000011\\n%%44\\t0\\t2000000044\\n'
6142
+ elif [ -f "${logPath}.leader-authorized" ]; then
6143
+ printf "%%11\\tzsh\\tzsh\\n%%44\\tnode\\texec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%%other' /node /omx.js hud --watch\\n"
6144
+ else
6145
+ printf "%%11\\tzsh\\tzsh\\n%%44\\tnode\\texec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%%11' /node /omx.js hud --watch\\n"
6146
+ fi
6147
+ ;;
6148
+ show-option)
6149
+ : > "${logPath}.leader-authorized"
6150
+ echo 'team:restore-final-command-change'
6151
+ ;;
6152
+ esac
6153
+ `, async ({ logPath }) => {
6154
+ const debtPath = join(cwd, '.omx', 'state', '.restored-hud-cleanup-debt.json');
6155
+ await mkdir(dirname(debtPath), { recursive: true });
6156
+ await writeFile(debtPath, `${JSON.stringify({
6157
+ schema_version: 1,
6158
+ operation: 'restored_hud_cleanup',
6159
+ pane_id: '%44',
6160
+ pane_pid: 2000000044,
6161
+ leader_pane_id: '%11',
6162
+ leader_pane_pid: 2000000011,
6163
+ leader_pane_owner_id: 'team:restore-final-command-change',
6164
+ hud_owner_leader_pane_id: '%11',
6165
+ })}\n`);
6166
+ assert.throws(() => finalizeRestoredHudCleanupDebtSync(cwd, '%44', 2000000044), /restored_hud_cleanup_debt_unresolved:%44/);
6167
+ await readFile(debtPath, 'utf-8');
6168
+ const commands = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean);
6169
+ const topologyIndex = commands.findIndex((command) => command.startsWith('list-panes -t %11 -F '));
6170
+ const ownerIndex = commands.findIndex((command) => command === 'show-option -qv -p -t %11 @omx_team_pane_owner_id');
6171
+ assert.ok(ownerIndex >= 0 && topologyIndex > ownerIndex);
6172
+ assert.equal(commands.slice(topologyIndex + 1).filter((command) => command.startsWith('list-panes -a ')).length, 0);
6173
+ });
6174
+ }
6175
+ finally {
6176
+ await rm(cwd, { recursive: true, force: true });
6177
+ }
6178
+ });
6179
+ it('replays only a pinned restored HUD with continuous leader owner and HUD identity', async () => {
6180
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-restored-hud-replay-'));
6181
+ try {
6182
+ await withMockTmuxFixture('omx-restored-hud-replay-', (logPath) => {
6183
+ const statePath = `${logPath}.killed`;
6184
+ return `#!/bin/sh
6185
+ set -eu
6186
+ printf '%s\\n' "$*" >> "${logPath}"
6187
+ case "\${1:-}" in
6188
+ list-panes)
6189
+ if [ "$2" = "-a" ]; then
6190
+ printf '%%11\\t0\\t2000000011\\n'
6191
+ if [ ! -f "${statePath}" ]; then printf '%%44\\t0\\t2000000044\\n'; fi
6192
+ else
6193
+ printf '%%11\\tzsh\\tzsh\\n'
6194
+ if [ ! -f "${statePath}" ]; then printf "%%44\\tnode\\texec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%%11' /node /omx.js hud --watch\\n"; fi
6195
+ fi
6196
+ ;;
6197
+ show-option)
6198
+ if [ "$5" = "%11" ] && [ "$6" = "@omx_team_pane_owner_id" ]; then
6199
+ printf 'team:restore-replay\n'
6200
+ else
6201
+ exit 1
6202
+ fi
6203
+ ;;
6204
+ kill-pane) : > "${statePath}" ;;
6205
+ esac
6206
+ `;
6207
+ }, async ({ logPath }) => {
6208
+ const debtPath = join(cwd, '.omx', 'state', '.restored-hud-cleanup-debt.json');
6209
+ await mkdir(dirname(debtPath), { recursive: true });
6210
+ await writeFile(debtPath, `${JSON.stringify({
6211
+ schema_version: 1,
6212
+ operation: 'restored_hud_cleanup',
6213
+ pane_id: '%44',
6214
+ pane_pid: 2000000044,
6215
+ leader_pane_id: '%11',
6216
+ leader_pane_pid: 2000000011,
6217
+ leader_pane_owner_id: 'team:restore-replay',
6218
+ hud_owner_leader_pane_id: '%11',
6219
+ })}\n`);
6220
+ reconcileRestoredHudCleanupDebtSync(cwd);
6221
+ await assert.rejects(() => readFile(debtPath, 'utf-8'));
6222
+ const tmuxLog = await readFile(logPath, 'utf-8');
6223
+ assert.match(tmuxLog, /kill-pane -t %44/);
6224
+ assert.match(tmuxLog, /list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}\nlist-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}\nshow-option -qv -p -t %11 @omx_team_pane_owner_id\nlist-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}\nlist-panes -t %11 -F #\{pane_id\}\t#\{pane_current_command\}\t#\{pane_start_command\}/);
6225
+ assert.match(tmuxLog, /list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}\nlist-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}\nshow-option -qv -p -t %11 @omx_team_pane_owner_id\nlist-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}\nlist-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}\nkill-pane -t %44\nlist-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}/);
6226
+ });
6227
+ }
6228
+ finally {
6229
+ await rm(cwd, { recursive: true, force: true });
6230
+ }
6231
+ });
6232
+ it('restores standalone HUD panes from the live leader pane cwd with explicit session ownership', async () => {
6233
+ const teamLaunchCwd = await mkdtemp(join(tmpdir(), 'omx-standalone-team-launch-cwd-'));
6234
+ const leaderPaneCwd = await mkdtemp(join(tmpdir(), 'omx-standalone-leader-pane-cwd-'));
6235
+ try {
6236
+ await withMockTmuxFixture('omx-tmux-leader-cwd-standalone-hud-', (logPath) => `#!/bin/sh
5032
6237
  set -eu
5033
6238
  printf '%s\\n' "$*" >> "${logPath}"
5034
6239
  case "\${1:-}" in
6240
+ display-message)
6241
+ case "$*" in
6242
+ *"#{pane_current_path}"*)
6243
+ echo "${leaderPaneCwd}"
6244
+ ;;
6245
+ esac
6246
+ exit 0
6247
+ ;;
5035
6248
  split-window)
5036
6249
  echo "%44"
5037
6250
  exit 0
@@ -5044,159 +6257,991 @@ case "\${1:-}" in
5044
6257
  ;;
5045
6258
  esac
5046
6259
  `, async ({ logPath }) => {
5047
- process.argv = [previousArgv[0] || 'node', '/tmp/codex-host-binary'];
5048
- const paneId = restoreStandaloneHudPane('%11', cwd);
6260
+ const paneId = restoreStandaloneHudPane('%11', teamLaunchCwd, {
6261
+ sessionId: 'current-session-for-hud',
6262
+ });
5049
6263
  assert.equal(paneId, '%44');
5050
6264
  const tmuxLog = await readFile(logPath, 'utf-8');
5051
- assert.match(tmuxLog, /dist\/cli\/omx\.js' hud --watch/);
5052
- assert.doesNotMatch(tmuxLog, /\/tmp\/codex-host-binary' hud --watch/);
5053
- assert.match(tmuxLog, /exec env OMX_TMUX_HUD_OWNER=1 .*hud --watch/);
6265
+ assert.match(tmuxLog, /display-message -p -t %11 #\{pane_current_path\}/);
6266
+ assert.match(tmuxLog, new RegExp(`split-window -v -l ${HUD_TMUX_TEAM_HEIGHT_LINES} -t %11 -d -P -F #\\{pane_id\\} -c ${escapeRegExp(leaderPaneCwd)} `));
6267
+ assert.doesNotMatch(tmuxLog, new RegExp(`split-window .* -c ${escapeRegExp(teamLaunchCwd)} `));
6268
+ assert.match(tmuxLog, /exec env OMX_SESSION_ID='current-session-for-hud' OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%11' .*hud --watch/);
5054
6269
  });
5055
6270
  }
5056
6271
  finally {
5057
- process.argv = previousArgv;
5058
- await rm(cwd, { recursive: true, force: true });
6272
+ await rm(teamLaunchCwd, { recursive: true, force: true });
6273
+ await rm(leaderPaneCwd, { recursive: true, force: true });
5059
6274
  }
5060
6275
  });
5061
- it('restores standalone HUD panes with OMX_ROOT forwarded and shell-escaped', async () => {
5062
- const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-root-hud-'));
5063
- const previousOmxRoot = process.env.OMX_ROOT;
6276
+ it('falls back to the team launch cwd when the live leader pane cwd is unusable', async () => {
6277
+ const teamLaunchCwd = await mkdtemp(join(tmpdir(), 'omx-standalone-team-launch-cwd-fallback-'));
6278
+ const deletedLeaderPaneCwd = await mkdtemp(join(tmpdir(), 'omx-standalone-deleted-leader-pane-cwd-'));
6279
+ await rm(deletedLeaderPaneCwd, { recursive: true, force: true });
5064
6280
  try {
5065
- await withMockTmuxFixture('omx-tmux-root-standalone-hud-', (logPath) => `#!/bin/sh
6281
+ await withMockTmuxFixture('omx-tmux-deleted-leader-cwd-standalone-hud-', (logPath) => `#!/bin/sh
5066
6282
  set -eu
5067
6283
  printf '%s\\n' "$*" >> "${logPath}"
5068
6284
  case "\${1:-}" in
6285
+ display-message)
6286
+ case "$*" in
6287
+ *"#{pane_current_path}"*)
6288
+ echo "${deletedLeaderPaneCwd}"
6289
+ ;;
6290
+ esac
6291
+ exit 0
6292
+ ;;
5069
6293
  split-window)
5070
- echo "%44"
6294
+ case "$*" in
6295
+ *"-c ${teamLaunchCwd} "*)
6296
+ echo "%44"
6297
+ exit 0
6298
+ ;;
6299
+ *"-c ${deletedLeaderPaneCwd} "*)
6300
+ echo "deleted cwd should not be used" >&2
6301
+ exit 1
6302
+ ;;
6303
+ *)
6304
+ echo "unexpected cwd: $*" >&2
6305
+ exit 1
6306
+ ;;
6307
+ esac
6308
+ ;;
6309
+ run-shell|select-pane|resize-pane|set-hook)
6310
+ exit 0
6311
+ ;;
6312
+ *)
6313
+ exit 0
6314
+ ;;
6315
+ esac
6316
+ `, async ({ logPath }) => {
6317
+ const paneId = restoreStandaloneHudPane('%11', teamLaunchCwd, {
6318
+ sessionId: 'current-session-for-hud',
6319
+ });
6320
+ assert.equal(paneId, '%44');
6321
+ const tmuxLog = await readFile(logPath, 'utf-8');
6322
+ assert.match(tmuxLog, /display-message -p -t %11 #\{pane_current_path\}/);
6323
+ assert.match(tmuxLog, new RegExp(`split-window -v -l ${HUD_TMUX_TEAM_HEIGHT_LINES} -t %11 -d -P -F #\\{pane_id\\} -c ${escapeRegExp(teamLaunchCwd)} `));
6324
+ assert.doesNotMatch(tmuxLog, new RegExp(`split-window .* -c ${escapeRegExp(deletedLeaderPaneCwd)} `));
6325
+ assert.match(tmuxLog, /select-pane -t %11/);
6326
+ });
6327
+ }
6328
+ finally {
6329
+ await rm(teamLaunchCwd, { recursive: true, force: true });
6330
+ }
6331
+ });
6332
+ it('keeps MSYS drive-style live leader cwd candidates without raw stat prefiltering', async () => {
6333
+ const liveLeaderCwd = '/c/live';
6334
+ const fallbackCwd = 'C:\\fallback';
6335
+ const fakeCygpathDir = await mkdtemp(join(tmpdir(), 'omx-msys-live-cygpath-'));
6336
+ const previousPath = process.env.PATH;
6337
+ const previousMsystem = process.env.MSYSTEM;
6338
+ const previousOstype = process.env.OSTYPE;
6339
+ const previousWsl = process.env.WSL_DISTRO_NAME;
6340
+ const previousWslInterop = process.env.WSL_INTEROP;
6341
+ const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
6342
+ const statCalls = [];
6343
+ try {
6344
+ const cygpathPath = join(fakeCygpathDir, 'cygpath');
6345
+ await writeFile(cygpathPath, '#!/bin/sh\nprintf "%s\\n" "$2"\n');
6346
+ await chmod(cygpathPath, 0o755);
6347
+ process.env.PATH = `${fakeCygpathDir}:${previousPath ?? ''}`;
6348
+ process.env.MSYSTEM = 'MINGW64';
6349
+ process.env.OSTYPE = 'msys';
6350
+ delete process.env.WSL_DISTRO_NAME;
6351
+ delete process.env.WSL_INTEROP;
6352
+ Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
6353
+ await withMockTmuxFixture('omx-tmux-msys-live-cwd-standalone-hud-', (logPath) => `#!/bin/sh
6354
+ set -eu
6355
+ printf '%s\\n' "$*" >> "${logPath}"
6356
+ case "\${1:-}" in
6357
+ display-message)
6358
+ case "$*" in
6359
+ *"#{pane_current_path}"*)
6360
+ echo "${liveLeaderCwd}"
6361
+ ;;
6362
+ esac
6363
+ exit 0
6364
+ ;;
6365
+ split-window)
6366
+ case "$*" in
6367
+ *"-c ${liveLeaderCwd} "*)
6368
+ echo "%44"
6369
+ exit 0
6370
+ ;;
6371
+ *"-c ${fallbackCwd} "*)
6372
+ echo "fallback cwd should not be used when MSYS live cwd succeeds" >&2
6373
+ exit 1
6374
+ ;;
6375
+ *)
6376
+ echo "unexpected cwd: $*" >&2
6377
+ exit 1
6378
+ ;;
6379
+ esac
6380
+ ;;
6381
+ run-shell|select-pane|resize-pane|set-hook)
6382
+ exit 0
6383
+ ;;
6384
+ *)
5071
6385
  exit 0
5072
6386
  ;;
5073
- run-shell|select-pane|resize-pane|set-hook)
5074
- exit 0
6387
+ esac
6388
+ `, async ({ logPath }) => {
6389
+ const paneId = withMockedStatSync(((pathLike) => {
6390
+ const value = String(pathLike);
6391
+ statCalls.push(value);
6392
+ if (value === fallbackCwd)
6393
+ return { isDirectory: () => true };
6394
+ return { isDirectory: () => false };
6395
+ }), () => restoreStandaloneHudPane('%11', fallbackCwd, { sessionId: 'current-session-for-hud' }));
6396
+ assert.equal(paneId, '%44');
6397
+ assert.equal(statCalls.includes(liveLeaderCwd), false);
6398
+ assert.equal(statCalls.includes(fallbackCwd), true);
6399
+ const tmuxLog = await readFile(logPath, 'utf-8');
6400
+ assert.match(tmuxLog, /display-message -p -t %11 #\{pane_current_path\}/);
6401
+ assert.match(tmuxLog, new RegExp(`split-window -v -l ${HUD_TMUX_TEAM_HEIGHT_LINES} -t %11 -d -P -F #\\{pane_id\\} -c ${escapeRegExp(liveLeaderCwd)} `));
6402
+ assert.doesNotMatch(tmuxLog, new RegExp(`split-window .* -c ${escapeRegExp(fallbackCwd)} `));
6403
+ assert.match(tmuxLog, /select-pane -t %11/);
6404
+ });
6405
+ }
6406
+ finally {
6407
+ if (originalPlatform)
6408
+ Object.defineProperty(process, 'platform', originalPlatform);
6409
+ if (typeof previousPath === 'string')
6410
+ process.env.PATH = previousPath;
6411
+ else
6412
+ delete process.env.PATH;
6413
+ if (typeof previousMsystem === 'string')
6414
+ process.env.MSYSTEM = previousMsystem;
6415
+ else
6416
+ delete process.env.MSYSTEM;
6417
+ if (typeof previousOstype === 'string')
6418
+ process.env.OSTYPE = previousOstype;
6419
+ else
6420
+ delete process.env.OSTYPE;
6421
+ if (typeof previousWsl === 'string')
6422
+ process.env.WSL_DISTRO_NAME = previousWsl;
6423
+ else
6424
+ delete process.env.WSL_DISTRO_NAME;
6425
+ if (typeof previousWslInterop === 'string')
6426
+ process.env.WSL_INTEROP = previousWslInterop;
6427
+ else
6428
+ delete process.env.WSL_INTEROP;
6429
+ await rm(fakeCygpathDir, { recursive: true, force: true });
6430
+ }
6431
+ });
6432
+ it('falls back after an unusable MSYS drive-style live leader cwd split attempt fails', async () => {
6433
+ const liveLeaderCwd = '/c/deleted-live';
6434
+ const fallbackCwd = 'C:\\fallback';
6435
+ const fakeCygpathDir = await mkdtemp(join(tmpdir(), 'omx-msys-deleted-live-cygpath-'));
6436
+ const previousPath = process.env.PATH;
6437
+ const previousMsystem = process.env.MSYSTEM;
6438
+ const previousOstype = process.env.OSTYPE;
6439
+ const previousWsl = process.env.WSL_DISTRO_NAME;
6440
+ const previousWslInterop = process.env.WSL_INTEROP;
6441
+ const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
6442
+ const statCalls = [];
6443
+ try {
6444
+ const cygpathPath = join(fakeCygpathDir, 'cygpath');
6445
+ await writeFile(cygpathPath, '#!/bin/sh\nprintf "%s\\n" "$2"\n');
6446
+ await chmod(cygpathPath, 0o755);
6447
+ process.env.PATH = `${fakeCygpathDir}:${previousPath ?? ''}`;
6448
+ process.env.MSYSTEM = 'MINGW64';
6449
+ process.env.OSTYPE = 'msys';
6450
+ delete process.env.WSL_DISTRO_NAME;
6451
+ delete process.env.WSL_INTEROP;
6452
+ Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
6453
+ await withMockTmuxFixture('omx-tmux-msys-deleted-live-cwd-standalone-hud-', (logPath) => `#!/bin/sh
6454
+ set -eu
6455
+ printf '%s\\n' "$*" >> "${logPath}"
6456
+ case "\${1:-}" in
6457
+ display-message)
6458
+ case "$*" in
6459
+ *"#{pane_current_path}"*)
6460
+ echo "${liveLeaderCwd}"
6461
+ ;;
6462
+ esac
6463
+ exit 0
6464
+ ;;
6465
+ split-window)
6466
+ case "$*" in
6467
+ *"-c ${liveLeaderCwd} "*)
6468
+ echo "deleted live cwd cannot be used" >&2
6469
+ exit 1
6470
+ ;;
6471
+ *"-c ${fallbackCwd} "*)
6472
+ echo "%44"
6473
+ exit 0
6474
+ ;;
6475
+ *)
6476
+ echo "unexpected cwd: $*" >&2
6477
+ exit 1
6478
+ ;;
6479
+ esac
6480
+ ;;
6481
+ run-shell|select-pane|resize-pane|set-hook)
6482
+ exit 0
6483
+ ;;
6484
+ *)
6485
+ exit 0
6486
+ ;;
6487
+ esac
6488
+ `, async ({ logPath }) => {
6489
+ const paneId = withMockedStatSync(((pathLike) => {
6490
+ const value = String(pathLike);
6491
+ statCalls.push(value);
6492
+ if (value === fallbackCwd)
6493
+ return { isDirectory: () => true };
6494
+ return { isDirectory: () => false };
6495
+ }), () => restoreStandaloneHudPane('%11', fallbackCwd, { sessionId: 'current-session-for-hud' }));
6496
+ assert.equal(paneId, '%44');
6497
+ assert.equal(statCalls.includes(liveLeaderCwd), false);
6498
+ assert.equal(statCalls.includes(fallbackCwd), true);
6499
+ const tmuxLog = await readFile(logPath, 'utf-8');
6500
+ const liveAttemptIndex = tmuxLog.indexOf(`-c ${liveLeaderCwd} `);
6501
+ const fallbackAttemptIndex = tmuxLog.indexOf(`-c ${fallbackCwd} `);
6502
+ assert.ok(liveAttemptIndex >= 0, 'deleted MSYS live cwd should still be attempted');
6503
+ assert.ok(fallbackAttemptIndex > liveAttemptIndex, 'fallback should be attempted after live split failure');
6504
+ assert.match(tmuxLog, /select-pane -t %11/);
6505
+ });
6506
+ }
6507
+ finally {
6508
+ if (originalPlatform)
6509
+ Object.defineProperty(process, 'platform', originalPlatform);
6510
+ if (typeof previousPath === 'string')
6511
+ process.env.PATH = previousPath;
6512
+ else
6513
+ delete process.env.PATH;
6514
+ if (typeof previousMsystem === 'string')
6515
+ process.env.MSYSTEM = previousMsystem;
6516
+ else
6517
+ delete process.env.MSYSTEM;
6518
+ if (typeof previousOstype === 'string')
6519
+ process.env.OSTYPE = previousOstype;
6520
+ else
6521
+ delete process.env.OSTYPE;
6522
+ if (typeof previousWsl === 'string')
6523
+ process.env.WSL_DISTRO_NAME = previousWsl;
6524
+ else
6525
+ delete process.env.WSL_DISTRO_NAME;
6526
+ if (typeof previousWslInterop === 'string')
6527
+ process.env.WSL_INTEROP = previousWslInterop;
6528
+ else
6529
+ delete process.env.WSL_INTEROP;
6530
+ await rm(fakeCygpathDir, { recursive: true, force: true });
6531
+ }
6532
+ });
6533
+ it('restores standalone HUD panes with an absolute OMX entry path after cwd drift', async () => {
6534
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-relative-hud-'));
6535
+ const startupCwd = await mkdtemp(join(tmpdir(), 'omx-standalone-relative-start-'));
6536
+ const previousEntryPath = process.env[OMX_ENTRY_PATH_ENV];
6537
+ const previousStartupCwd = process.env[OMX_STARTUP_CWD_ENV];
6538
+ const previousArgv = process.argv;
6539
+ try {
6540
+ const launcherDir = join(startupCwd, 'dist', 'cli');
6541
+ const launcherPath = join(launcherDir, 'omx.js');
6542
+ await mkdir(launcherDir, { recursive: true });
6543
+ await writeFile(launcherPath, '#!/usr/bin/env node\n');
6544
+ await withMockTmuxFixture('omx-tmux-relative-standalone-hud-', (logPath) => `#!/bin/sh
6545
+ set -eu
6546
+ printf '%s\\n' "$*" >> "${logPath}"
6547
+ case "\${1:-}" in
6548
+ split-window)
6549
+ echo "%44"
6550
+ exit 0
6551
+ ;;
6552
+ run-shell|select-pane|resize-pane|set-hook)
6553
+ exit 0
6554
+ ;;
6555
+ *)
6556
+ exit 0
6557
+ ;;
6558
+ esac
6559
+ `, async ({ logPath }) => {
6560
+ delete process.env[OMX_ENTRY_PATH_ENV];
6561
+ process.env[OMX_STARTUP_CWD_ENV] = startupCwd;
6562
+ process.argv = [previousArgv[0] || 'node', 'dist/cli/omx.js'];
6563
+ const paneId = restoreStandaloneHudPane('%11', cwd);
6564
+ assert.equal(paneId, '%44');
6565
+ const tmuxLog = await readFile(logPath, 'utf-8');
6566
+ assert.match(tmuxLog, new RegExp(escapeRegExp(launcherPath)));
6567
+ assert.doesNotMatch(tmuxLog, /'dist\/cli\/omx\.js' hud --watch/);
6568
+ assert.match(tmuxLog, /exec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%11' .*hud --watch/);
6569
+ });
6570
+ }
6571
+ finally {
6572
+ process.argv = previousArgv;
6573
+ if (typeof previousEntryPath === 'string')
6574
+ process.env[OMX_ENTRY_PATH_ENV] = previousEntryPath;
6575
+ else
6576
+ delete process.env[OMX_ENTRY_PATH_ENV];
6577
+ if (typeof previousStartupCwd === 'string')
6578
+ process.env[OMX_STARTUP_CWD_ENV] = previousStartupCwd;
6579
+ else
6580
+ delete process.env[OMX_STARTUP_CWD_ENV];
6581
+ await rm(cwd, { recursive: true, force: true });
6582
+ await rm(startupCwd, { recursive: true, force: true });
6583
+ }
6584
+ });
6585
+ it('restores standalone HUD panes with the packaged CLI entry when argv1 is not the OMX CLI', async () => {
6586
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-noncli-hud-'));
6587
+ const previousArgv = process.argv;
6588
+ try {
6589
+ await withMockTmuxFixture('omx-tmux-noncli-standalone-hud-', (logPath) => `#!/bin/sh
6590
+ set -eu
6591
+ printf '%s\\n' "$*" >> "${logPath}"
6592
+ case "\${1:-}" in
6593
+ split-window)
6594
+ echo "%44"
6595
+ exit 0
6596
+ ;;
6597
+ run-shell|select-pane|resize-pane|set-hook)
6598
+ exit 0
6599
+ ;;
6600
+ *)
6601
+ exit 0
6602
+ ;;
6603
+ esac
6604
+ `, async ({ logPath }) => {
6605
+ process.argv = [previousArgv[0] || 'node', '/tmp/codex-host-binary'];
6606
+ const paneId = restoreStandaloneHudPane('%11', cwd);
6607
+ assert.equal(paneId, '%44');
6608
+ const tmuxLog = await readFile(logPath, 'utf-8');
6609
+ assert.match(tmuxLog, /dist\/cli\/omx\.js' hud --watch/);
6610
+ assert.doesNotMatch(tmuxLog, /\/tmp\/codex-host-binary' hud --watch/);
6611
+ assert.match(tmuxLog, /exec env OMX_TMUX_HUD_OWNER=1 .*hud --watch/);
6612
+ });
6613
+ }
6614
+ finally {
6615
+ process.argv = previousArgv;
6616
+ await rm(cwd, { recursive: true, force: true });
6617
+ }
6618
+ });
6619
+ it('restores standalone HUD panes with OMX_ROOT forwarded and shell-escaped', async () => {
6620
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-standalone-root-hud-'));
6621
+ const previousOmxRoot = process.env.OMX_ROOT;
6622
+ try {
6623
+ await withMockTmuxFixture('omx-tmux-root-standalone-hud-', (logPath) => `#!/bin/sh
6624
+ set -eu
6625
+ printf '%s\\n' "$*" >> "${logPath}"
6626
+ case "\${1:-}" in
6627
+ split-window)
6628
+ echo "%44"
6629
+ exit 0
6630
+ ;;
6631
+ run-shell|select-pane|resize-pane|set-hook)
6632
+ exit 0
6633
+ ;;
6634
+ *)
6635
+ exit 0
6636
+ ;;
6637
+ esac
6638
+ `, async ({ logPath }) => {
6639
+ process.env.OMX_ROOT = "/tmp/boxed root/it's/$(literal)";
6640
+ const paneId = restoreStandaloneHudPane('%11', cwd);
6641
+ assert.equal(paneId, '%44');
6642
+ const tmuxLog = await readFile(logPath, 'utf-8');
6643
+ assert.match(tmuxLog, /exec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%11' OMX_ROOT='\/tmp\/boxed root\/it'\\''s\/\$\(literal\)' .*hud --watch/);
6644
+ });
6645
+ }
6646
+ finally {
6647
+ if (typeof previousOmxRoot === 'string')
6648
+ process.env.OMX_ROOT = previousOmxRoot;
6649
+ else
6650
+ delete process.env.OMX_ROOT;
6651
+ await rm(cwd, { recursive: true, force: true });
6652
+ }
6653
+ });
6654
+ });
6655
+ describe('dismissTrustPromptIfPresent capture shape', () => {
6656
+ it('uses visible capture-pane argv without tail flags', async () => {
6657
+ const previousAutoTrust = process.env.OMX_TEAM_AUTO_TRUST;
6658
+ delete process.env.OMX_TEAM_AUTO_TRUST;
6659
+ try {
6660
+ await withMockTmuxFixture('omx-tmux-dismiss-trust-visible-capture-', (logPath) => `#!/bin/sh
6661
+ set -eu
6662
+ printf '%s\n' "$*" >> "${logPath}"
6663
+ case "$1" in
6664
+ capture-pane)
6665
+ cat <<'EOF'
6666
+ Do you trust the contents of this directory?
6667
+ Press enter to continue
6668
+ EOF
6669
+ exit 0
6670
+ ;;
6671
+ send-keys)
6672
+ exit 0
6673
+ ;;
6674
+ *)
6675
+ exit 0
6676
+ ;;
6677
+ esac
6678
+ `, async ({ logPath }) => {
6679
+ assert.equal(dismissTrustPromptIfPresent('omx-team-x', 1), true);
6680
+ const log = await readFile(logPath, 'utf-8');
6681
+ assert.match(log, /capture-pane -t omx-team-x:1 -p/);
6682
+ assert.doesNotMatch(log, /capture-pane -t omx-team-x:1 -p -S/);
6683
+ });
6684
+ }
6685
+ finally {
6686
+ if (typeof previousAutoTrust === 'string')
6687
+ process.env.OMX_TEAM_AUTO_TRUST = previousAutoTrust;
6688
+ else
6689
+ delete process.env.OMX_TEAM_AUTO_TRUST;
6690
+ }
6691
+ });
6692
+ it('does not send trust controls to a pane ID reused after capture', async () => {
6693
+ const previousAutoTrust = process.env.OMX_TEAM_AUTO_TRUST;
6694
+ delete process.env.OMX_TEAM_AUTO_TRUST;
6695
+ try {
6696
+ await withMockTmuxFixture('omx-tmux-dismiss-trust-pid-reuse-', (logPath) => `#!/bin/sh
6697
+ set -eu
6698
+ state_dir="$(dirname "${logPath}")"
6699
+ proof_count_file="$state_dir/proof-count"
6700
+ printf '%s\n' "$*" >> "${logPath}"
6701
+ case "$1" in
6702
+ list-panes)
6703
+ count=0
6704
+ if [ -f "$proof_count_file" ]; then count=$(cat "$proof_count_file"); fi
6705
+ count=$((count + 1))
6706
+ printf '%s' "$count" > "$proof_count_file"
6707
+ if [ "$count" -eq 1 ]; then
6708
+ printf '%%9\t0\t2000000001\n'
6709
+ elif [ "$count" -le 2 ]; then
6710
+ printf '%%9\t0\t2000000001\n'
6711
+ else
6712
+ printf '%%9\t0\t2000000002\n'
6713
+ fi
6714
+ ;;
6715
+ show-option)
6716
+ printf 'team:test\n'
6717
+ ;;
6718
+ capture-pane)
6719
+ cat <<'EOF'
6720
+ Do you trust the contents of this directory?
6721
+ Press enter to continue
6722
+ EOF
6723
+ ;;
6724
+ -V) exit 0 ;;
6725
+ send-keys) exit 1 ;;
6726
+ *) exit 1 ;;
6727
+ esac
6728
+ `, async ({ logPath }) => {
6729
+ assert.throws(() => dismissTrustPromptIfPresent('ignored-session', 1, '%9', 2000000001, 'team:test', '%10'), /tmux pane identity changed: %9/);
6730
+ const log = await readFile(logPath, 'utf-8');
6731
+ assert.match(log, /capture-pane -t %9 -p/);
6732
+ assert.doesNotMatch(log, /send-keys -t %9/);
6733
+ });
6734
+ }
6735
+ finally {
6736
+ if (typeof previousAutoTrust === 'string')
6737
+ process.env.OMX_TEAM_AUTO_TRUST = previousAutoTrust;
6738
+ else
6739
+ delete process.env.OMX_TEAM_AUTO_TRUST;
6740
+ }
6741
+ });
6742
+ it('fails closed without emitting trust controls when an explicit pane lacks a PID', async () => {
6743
+ await withMockTmuxFixture('omx-tmux-dismiss-trust-missing-pid-', (logPath) => `#!/bin/sh
6744
+ printf '%s\\n' "$*" >> "${logPath}"
6745
+ [ "$1" = "-V" ] && exit 0
6746
+ exit 1
6747
+ `, async ({ logPath }) => {
6748
+ assert.equal(dismissTrustPromptIfPresent('ignored-session', 1, '%9'), false);
6749
+ const log = await readFile(logPath, 'utf-8');
6750
+ assert.doesNotMatch(log, /capture-pane|send-keys|list-panes/);
6751
+ });
6752
+ });
6753
+ });
6754
+ describe('dismissTrustPromptIfPresent', () => {
6755
+ it('returns false when tmux is unavailable', () => {
6756
+ withEmptyPath(() => {
6757
+ assert.equal(dismissTrustPromptIfPresent('omx-team-x', 1), false);
6758
+ });
6759
+ });
6760
+ it('returns false when OMX_TEAM_AUTO_TRUST is disabled', () => {
6761
+ const prev = process.env.OMX_TEAM_AUTO_TRUST;
6762
+ process.env.OMX_TEAM_AUTO_TRUST = '0';
6763
+ try {
6764
+ assert.equal(dismissTrustPromptIfPresent('omx-team-x', 1), false);
6765
+ }
6766
+ finally {
6767
+ if (typeof prev === 'string')
6768
+ process.env.OMX_TEAM_AUTO_TRUST = prev;
6769
+ else
6770
+ delete process.env.OMX_TEAM_AUTO_TRUST;
6771
+ }
6772
+ });
6773
+ it('returns false when OMX_TEAM_AUTO_TRUST is unset (auto-trust enabled) but tmux unavailable', () => {
6774
+ const prev = process.env.OMX_TEAM_AUTO_TRUST;
6775
+ delete process.env.OMX_TEAM_AUTO_TRUST;
6776
+ try {
6777
+ withEmptyPath(() => {
6778
+ assert.equal(dismissTrustPromptIfPresent('omx-team-x', 1), false);
6779
+ });
6780
+ }
6781
+ finally {
6782
+ if (typeof prev === 'string')
6783
+ process.env.OMX_TEAM_AUTO_TRUST = prev;
6784
+ }
6785
+ });
6786
+ });
6787
+ describe('isWorkerAlive', () => {
6788
+ it('does not require pane_current_command to match "codex"', () => {
6789
+ // This was a real failure mode: tmux reports pane_current_command=node for the Codex TUI,
6790
+ // which caused workers to be treated as dead and the leader to clean up state too early.
6791
+ withEmptyPath(() => {
6792
+ assert.equal(isWorkerAlive('omx-team-x', 1), false);
6793
+ });
6794
+ });
6795
+ it('treats blank persisted pane IDs as absent and uses the compatibility target', async () => {
6796
+ await withMockTmuxFixture('omx-blank-pane-fallback-', () => `#!/bin/sh
6797
+ if [ "$1" = "list-panes" ] && [ "$2" = "-t" ]; then printf '0 %s\n' "$PPID"; exit 0; fi
6798
+ exit 1
6799
+ `, async () => {
6800
+ assert.equal(isWorkerAlive('compat-session', 1, ''), true);
6801
+ assert.equal(isWorkerAlive('compat-session', 1, ' '), true);
6802
+ });
6803
+ });
6804
+ it('treats EPERM liveness probes as unknown and emits no async kill controls', async () => {
6805
+ await withMockTmuxFixture('omx-worker-liveness-eperm-', (logPath) => `#!/bin/sh
6806
+ set -eu
6807
+ printf '%s\n' "$*" >> "${logPath}"
6808
+ case "$1" in
6809
+ list-panes)
6810
+ if [ "$2" = "-a" ]; then printf '%%77\t0\t%s\n' "$PPID"; exit 0; fi
6811
+ if [ "$2" = "-t" ]; then printf '0 %s\n' "$PPID"; exit 0; fi
6812
+ exit 1
6813
+ ;;
6814
+ show-option) printf 'team:liveness\n'; exit 0 ;;
6815
+ *) exit 1 ;;
6816
+ esac
6817
+ `, async ({ logPath }) => {
6818
+ const originalProcessKill = process.kill;
6819
+ process.kill = ((pid, signal) => {
6820
+ if (pid === process.pid && signal === 0) {
6821
+ const error = new Error('permission denied');
6822
+ error.code = 'EPERM';
6823
+ throw error;
6824
+ }
6825
+ return originalProcessKill(pid, signal);
6826
+ });
6827
+ try {
6828
+ assert.equal(isWorkerAlive('compat-session', 1), true);
6829
+ assert.equal(isWorkerAlive('ignored-session', 1, '%77', process.pid, 'team:liveness'), true);
6830
+ await killWorker('ignored-session', 1, '%77', undefined, process.pid, 'team:liveness');
6831
+ }
6832
+ finally {
6833
+ process.kill = originalProcessKill;
6834
+ }
6835
+ const log = await readFile(logPath, 'utf-8');
6836
+ assert.doesNotMatch(log, /send-keys -t %77|kill-pane -t %77/);
6837
+ });
6838
+ });
6839
+ it('treats only ESRCH as a gone process', async () => {
6840
+ await withMockTmuxFixture('omx-worker-liveness-esrch-', () => `#!/bin/sh
6841
+ case "$1" in
6842
+ list-panes)
6843
+ if [ "$2" = "-a" ]; then printf '%%77\t0\t%s\n' "$PPID"; exit 0; fi
6844
+ if [ "$2" = "-t" ]; then printf '0 %s\n' "$PPID"; exit 0; fi
6845
+ exit 1
6846
+ ;;
6847
+ show-option) printf 'team:liveness\n'; exit 0 ;;
6848
+ *) exit 1 ;;
6849
+ esac
6850
+ `, async () => {
6851
+ const originalProcessKill = process.kill;
6852
+ process.kill = ((pid, signal) => {
6853
+ if (pid === process.pid && signal === 0) {
6854
+ const error = new Error('no such process');
6855
+ error.code = 'ESRCH';
6856
+ throw error;
6857
+ }
6858
+ return originalProcessKill(pid, signal);
6859
+ });
6860
+ try {
6861
+ assert.equal(isWorkerAlive('compat-session', 1), false);
6862
+ assert.equal(isWorkerAlive('ignored-session', 1, '%77', process.pid, 'team:liveness'), false);
6863
+ }
6864
+ finally {
6865
+ process.kill = originalProcessKill;
6866
+ }
6867
+ });
6868
+ });
6869
+ it('uses the exact global row for explicit pane liveness and fails closed for dead rows', async () => {
6870
+ await withMockTmuxFixture('omx-pane-id-liveness-', (logPath) => `#!/bin/sh
6871
+ set -eu
6872
+ printf '%s\n' "$*" >> "${logPath}"
6873
+ case "\${1:-}" in
6874
+ list-panes)
6875
+ if [ "$2" = "-a" ]; then
6876
+ printf '%%77\t0\t%s\n%%88\t1\t1\n' "$PPID"
6877
+ exit 0
6878
+ fi
6879
+ exit 1
6880
+ ;;
6881
+ show-option)
6882
+ printf 'team:liveness\n'
6883
+ ;;
6884
+ *)
6885
+ exit 1
6886
+ ;;
6887
+ esac
6888
+ `, async () => {
6889
+ assert.equal(isWorkerAlive('ignored-session', 1, '%77', process.pid, 'team:liveness'), true);
6890
+ assert.equal(isWorkerPaneOpen('ignored-session', 1, '%77', process.pid, 'team:liveness'), true);
6891
+ assert.equal(isWorkerAlive('ignored-session', 2, '%88', 1, 'team:liveness'), false);
6892
+ assert.equal(isWorkerPaneOpen('ignored-session', 2, '%88', 1, 'team:liveness'), false);
6893
+ });
6894
+ });
6895
+ });
6896
+ describe('exact global pane proof contract', () => {
6897
+ it('matches only exact pane IDs and distinguishes live, dead, and absent rows', async () => {
6898
+ const pane13Pid = 13013;
6899
+ const pane130Pid = 130130;
6900
+ await withMockTmuxFixture('omx-exact-pane-rows-', () => `#!/bin/sh
6901
+ set -eu
6902
+ case "$1" in
6903
+ list-panes)
6904
+ printf '%%13\t0\t%s\n%%130\t0\t%s\n%%9\t1\t1\n' "${pane13Pid}" "${pane130Pid}"
6905
+ ;;
6906
+ show-option)
6907
+ printf 'team:rows\n'
6908
+ ;;
6909
+ *)
6910
+ exit 1
6911
+ ;;
6912
+ esac
6913
+ `, async () => {
6914
+ assert.deepEqual(readExactPaneProofSync('%1'), { status: 'gone', paneId: '%1', reason: 'absent' });
6915
+ assert.deepEqual(readExactPaneProofSync('%9'), { status: 'gone', paneId: '%9', reason: 'dead' });
6916
+ assert.deepEqual(readExactPaneProofSync('%404'), { status: 'gone', paneId: '%404', reason: 'absent' });
6917
+ assert.deepEqual(await readExactPaneProof('%13'), {
6918
+ status: 'live',
6919
+ paneId: '%13',
6920
+ pid: pane13Pid,
6921
+ });
6922
+ assert.deepEqual(await readExactPaneProof('%130'), {
6923
+ status: 'live',
6924
+ paneId: '%130',
6925
+ pid: pane130Pid,
6926
+ });
6927
+ assert.equal(getWorkerPanePid('ignored-session', 1, '%13', pane13Pid, 'team:rows'), pane13Pid);
6928
+ assert.equal(getWorkerPanePid('ignored-session', 1, '%130', pane130Pid, 'team:rows'), pane130Pid);
6929
+ });
6930
+ });
6931
+ it('fails closed for command failures, malformed snapshots, and invalid IDs', async () => {
6932
+ await withMockTmuxFixture('omx-exact-pane-malformed-', () => `#!/bin/sh
6933
+ set -eu
6934
+ if [ "$1" = "list-panes" ]; then
6935
+ printf '%%1 0 1\n'
6936
+ fi
6937
+ `, async () => {
6938
+ const malformed = readExactPaneProofSync('%1');
6939
+ assert.equal(malformed.status, 'unavailable');
6940
+ if (malformed.status === 'unavailable') {
6941
+ assert.equal(malformed.reason, 'malformed_snapshot');
6942
+ }
6943
+ const invalid = await readExactPaneProof('not-a-pane');
6944
+ assert.equal(invalid.status, 'unavailable');
6945
+ if (invalid.status === 'unavailable') {
6946
+ assert.equal(invalid.reason, 'invalid_pane_id');
6947
+ }
6948
+ });
6949
+ await withMockTmuxFixture('omx-exact-pane-query-failure-', () => `#!/bin/sh
6950
+ set -eu
6951
+ if [ "$1" = "list-panes" ]; then
6952
+ echo "tmux query failed" >&2
6953
+ exit 1
6954
+ fi
6955
+ exit 1
6956
+ `, async () => {
6957
+ const proof = await readExactPaneProof('%1');
6958
+ assert.equal(proof.status, 'unavailable');
6959
+ if (proof.status === 'unavailable') {
6960
+ assert.equal(proof.reason, 'query_failed');
6961
+ }
6962
+ });
6963
+ });
6964
+ it('treats malformed PIDs in dead and unrelated global rows as malformed snapshots', async () => {
6965
+ const proofStatePath = join(tmpdir(), `omx-exact-pane-invalid-pid-${process.pid}-${Date.now()}`);
6966
+ await withMockTmuxFixture('omx-exact-pane-invalid-pid-', () => `#!/bin/sh
6967
+ set -eu
6968
+ if [ "$1" = "list-panes" ]; then
6969
+ if [ -f "${proofStatePath}" ]; then
6970
+ printf '%%1\t0\t2000000001\n%%99\t0\tnot-a-pid\n'
6971
+ else
6972
+ : > "${proofStatePath}"
6973
+ printf '%%1\t0\t2000000001\n%%99\t1\tnot-a-pid\n'
6974
+ fi
6975
+ fi
6976
+ `, async () => {
6977
+ const deadRow = readExactPaneProofSync('%1');
6978
+ assert.equal(deadRow.status, 'unavailable');
6979
+ if (deadRow.status === 'unavailable')
6980
+ assert.equal(deadRow.reason, 'malformed_snapshot');
6981
+ const unrelatedRow = await readExactPaneProof('%1');
6982
+ assert.equal(unrelatedRow.status, 'unavailable');
6983
+ if (unrelatedRow.status === 'unavailable')
6984
+ assert.equal(unrelatedRow.reason, 'malformed_snapshot');
6985
+ });
6986
+ await rm(proofStatePath, { force: true });
6987
+ });
6988
+ it('does not fall back to a session target or issue effects for an unproven explicit ID', async () => {
6989
+ await withMockTmuxFixture('omx-exact-pane-no-fallback-', (logPath) => `#!/bin/sh
6990
+ set -eu
6991
+ printf '%s\n' "$*" >> "${logPath}"
6992
+ case "$1" in
6993
+ list-panes)
6994
+ printf '%%13\t0\t%s\n' "$PPID"
6995
+ ;;
6996
+ *)
6997
+ exit 0
6998
+ ;;
6999
+ esac
7000
+ `, async ({ logPath }) => {
7001
+ assert.equal(getWorkerPanePid('ignored-session', 1, '%1'), null);
7002
+ assert.equal(isWorkerAlive('ignored-session', 1, '%1'), false);
7003
+ assert.equal(isWorkerPaneOpen('ignored-session', 1, '%1'), false);
7004
+ await assert.rejects(() => sendToWorker('ignored-session', 1, 'check inbox', '%1'), /not proven live/);
7005
+ await killWorker('ignored-session', 1, '%1');
7006
+ killWorkerByPaneId('%1', 1);
7007
+ await killWorkerByPaneIdAsync('%1', 1);
7008
+ const log = fs.existsSync(logPath) ? await readFile(logPath, 'utf-8') : '';
7009
+ assert.doesNotMatch(log, /list-panes -t ignored-session:1/);
7010
+ assert.doesNotMatch(log, /send-keys/);
7011
+ assert.doesNotMatch(log, /kill-pane/);
7012
+ });
7013
+ });
7014
+ it('revalidates an explicit pane before every send effect', async () => {
7015
+ await withMockTmuxFixture('omx-exact-pane-send-revalidation-', (logPath) => `#!/bin/sh
7016
+ set -eu
7017
+ state_dir="$(dirname "${logPath}")"
7018
+ count_file="$state_dir/list-count"
7019
+ printf '%s\n' "$*" >> "${logPath}"
7020
+ case "$1" in
7021
+ list-panes)
7022
+ count=0
7023
+ if [ -f "$count_file" ]; then count=$(cat "$count_file"); fi
7024
+ count=$((count + 1))
7025
+ printf '%s' "$count" > "$count_file"
7026
+ if [ "$count" -le 4 ]; then
7027
+ printf '%%9\t0\t%s\n' "$PPID"
7028
+ fi
7029
+ ;;
7030
+ show-option)
7031
+ printf 'team:test\n'
7032
+ ;;
7033
+ capture-pane)
7034
+ cat <<'EOF'
7035
+ ${READY_HELPER_CAPTURE}
7036
+ EOF
7037
+ ;;
7038
+ send-keys)
7039
+ ;;
7040
+ *)
7041
+ exit 1
7042
+ ;;
7043
+ esac
7044
+ `, async ({ logPath }) => {
7045
+ await assert.rejects(() => sendToWorker('ignored-session', 1, 'check inbox', '%9', undefined, process.pid, 'team:test', '%10'), /not proven live/);
7046
+ const commands = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean);
7047
+ const exactGlobalPaneProof = /^list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}$/;
7048
+ const effects = commands
7049
+ .map((command, index) => ({ command, index }))
7050
+ .filter(({ command }) => /^(send-keys|kill-pane) -t %9\b/.test(command));
7051
+ assert.ok(effects.length > 0, `expected an explicit pane effect in log:\n${commands.join('\n')}`);
7052
+ for (const { command, index } of effects) {
7053
+ assert.match(commands[index - 1] ?? '', exactGlobalPaneProof, `fresh proof must immediately precede ${command}`);
7054
+ }
7055
+ assert.match(commands.join('\n'), /send-keys -t %9 -l -- check inbox/);
7056
+ assert.doesNotMatch(commands.join('\n'), /send-keys -t %9 C-m/);
7057
+ });
7058
+ });
7059
+ it('pins an explicit pane PID through an adaptive retry and rejects later pane-ID reuse', async () => {
7060
+ await withMockTmuxFixture('omx-exact-pane-send-retry-pid-pin-', (logPath) => `#!/bin/sh
7061
+ set -eu
7062
+ state_dir="$(dirname "${logPath}")"
7063
+ retry_file="$state_dir/retry"
7064
+ sent_file="$state_dir/sent"
7065
+ printf '%s\n' "$*" >> "${logPath}"
7066
+ case "$1" in
7067
+ list-panes)
7068
+ if [ -f "$retry_file" ]; then
7069
+ printf '%%9\t0\t2000000002\n'
7070
+
7071
+ else
7072
+ printf '%%9\t0\t2000000001\n'
7073
+
7074
+ fi
7075
+ ;;
7076
+ capture-pane)
7077
+ if [ -f "$sent_file" ]; then
7078
+ cat <<'EOF'
7079
+ ${READY_HELPER_CAPTURE}
7080
+
7081
+ › check inbox
7082
+ EOF
7083
+ else
7084
+ cat <<'EOF'
7085
+ ${READY_HELPER_CAPTURE}
7086
+
7087
+ • Running tests (esc to interrupt)
7088
+ EOF
7089
+ fi
7090
+ ;;
7091
+ send-keys)
7092
+ case "$*" in
7093
+ *" -l -- check inbox") : > "$sent_file" ;;
7094
+ *" C-u") : > "$retry_file" ;;
7095
+ esac
5075
7096
  ;;
5076
- *)
5077
- exit 0
7097
+ show-option)
7098
+ printf 'team:test\n'
5078
7099
  ;;
7100
+ *) exit 1 ;;
5079
7101
  esac
5080
7102
  `, async ({ logPath }) => {
5081
- process.env.OMX_ROOT = "/tmp/boxed root/it's/$(literal)";
5082
- const paneId = restoreStandaloneHudPane('%11', cwd);
5083
- assert.equal(paneId, '%44');
5084
- const tmuxLog = await readFile(logPath, 'utf-8');
5085
- assert.match(tmuxLog, /exec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='%11' OMX_ROOT='\/tmp\/boxed root\/it'\\''s\/\$\(literal\)' .*hud --watch/);
5086
- });
5087
- }
5088
- finally {
5089
- if (typeof previousOmxRoot === 'string')
5090
- process.env.OMX_ROOT = previousOmxRoot;
5091
- else
5092
- delete process.env.OMX_ROOT;
5093
- await rm(cwd, { recursive: true, force: true });
5094
- }
7103
+ await assert.rejects(() => sendToWorker('ignored-session', 1, 'check inbox', '%9', undefined, 2000000001, 'team:test', '%10'), /tmux pane identity changed: %9/);
7104
+ const commands = await readFile(logPath, 'utf-8');
7105
+ assert.match(commands, /send-keys -t %9 C-u/);
7106
+ assert.equal((commands.match(/send-keys -t %9 -l -- check inbox/g) || []).length, 1, `the retry must not type into a reused pane:\n${commands}`);
7107
+ });
5095
7108
  });
5096
- });
5097
- describe('dismissTrustPromptIfPresent capture shape', () => {
5098
- it('uses visible capture-pane argv without tail flags', async () => {
5099
- const previousAutoTrust = process.env.OMX_TEAM_AUTO_TRUST;
5100
- delete process.env.OMX_TEAM_AUTO_TRUST;
5101
- try {
5102
- await withMockTmuxFixture('omx-tmux-dismiss-trust-visible-capture-', (logPath) => `#!/bin/sh
7109
+ it('rejects same-ID/PID owner takeover before worker input', async () => {
7110
+ await withMockTmuxFixture('omx-exact-pane-send-owner-takeover-', (logPath) => `#!/bin/sh
5103
7111
  set -eu
7112
+ state_dir="$(dirname "${logPath}")"
7113
+ owner_count="$state_dir/owner-count"
5104
7114
  printf '%s\n' "$*" >> "${logPath}"
5105
7115
  case "$1" in
7116
+ list-panes)
7117
+ printf '%%9\t0\t%s\n' "$PPID"
7118
+ ;;
7119
+ show-option)
7120
+ count=0
7121
+ if [ -f "$owner_count" ]; then count=$(cat "$owner_count"); fi
7122
+ count=$((count + 1))
7123
+ printf '%s' "$count" > "$owner_count"
7124
+ if [ "$count" -eq 1 ]; then printf 'team:expected\n'; else printf 'team:foreign\n'; fi
7125
+ ;;
5106
7126
  capture-pane)
5107
7127
  cat <<'EOF'
5108
- Do you trust the contents of this directory?
5109
- Press enter to continue
7128
+ ${READY_HELPER_CAPTURE}
5110
7129
  EOF
5111
- exit 0
5112
7130
  ;;
5113
7131
  send-keys)
5114
- exit 0
5115
- ;;
5116
- *)
5117
- exit 0
5118
7132
  ;;
7133
+ *) exit 1 ;;
5119
7134
  esac
5120
7135
  `, async ({ logPath }) => {
5121
- assert.equal(dismissTrustPromptIfPresent('omx-team-x', 1), true);
5122
- const log = await readFile(logPath, 'utf-8');
5123
- assert.match(log, /capture-pane -t omx-team-x:1 -p/);
5124
- assert.doesNotMatch(log, /capture-pane -t omx-team-x:1 -p -S/);
5125
- });
5126
- }
5127
- finally {
5128
- if (typeof previousAutoTrust === 'string')
5129
- process.env.OMX_TEAM_AUTO_TRUST = previousAutoTrust;
5130
- else
5131
- delete process.env.OMX_TEAM_AUTO_TRUST;
5132
- }
5133
- });
5134
- });
5135
- describe('dismissTrustPromptIfPresent', () => {
5136
- it('returns false when tmux is unavailable', () => {
5137
- withEmptyPath(() => {
5138
- assert.equal(dismissTrustPromptIfPresent('omx-team-x', 1), false);
7136
+ await assert.rejects(() => sendToWorker('ignored-session', 1, 'check inbox', '%9', undefined, process.pid, 'team:expected', '%10'), /team owner changed: %9/);
7137
+ const commands = await readFile(logPath, 'utf-8');
7138
+ assert.doesNotMatch(commands, /send-keys -t %9/);
5139
7139
  });
5140
7140
  });
5141
- it('returns false when OMX_TEAM_AUTO_TRUST is disabled', () => {
5142
- const prev = process.env.OMX_TEAM_AUTO_TRUST;
5143
- process.env.OMX_TEAM_AUTO_TRUST = '0';
5144
- try {
5145
- assert.equal(dismissTrustPromptIfPresent('omx-team-x', 1), false);
5146
- }
5147
- finally {
5148
- if (typeof prev === 'string')
5149
- process.env.OMX_TEAM_AUTO_TRUST = prev;
5150
- else
5151
- delete process.env.OMX_TEAM_AUTO_TRUST;
5152
- }
5153
- });
5154
- it('returns false when OMX_TEAM_AUTO_TRUST is unset (auto-trust enabled) but tmux unavailable', () => {
5155
- const prev = process.env.OMX_TEAM_AUTO_TRUST;
5156
- delete process.env.OMX_TEAM_AUTO_TRUST;
5157
- try {
5158
- withEmptyPath(() => {
5159
- assert.equal(dismissTrustPromptIfPresent('omx-team-x', 1), false);
5160
- });
5161
- }
5162
- finally {
5163
- if (typeof prev === 'string')
5164
- process.env.OMX_TEAM_AUTO_TRUST = prev;
5165
- }
5166
- });
5167
- });
5168
- describe('isWorkerAlive', () => {
5169
- it('does not require pane_current_command to match "codex"', () => {
5170
- // This was a real failure mode: tmux reports pane_current_command=node for the Codex TUI,
5171
- // which caused workers to be treated as dead and the leader to clean up state too early.
5172
- withEmptyPath(() => {
5173
- assert.equal(isWorkerAlive('omx-team-x', 1), false);
5174
- });
7141
+ it('rejects an explicit worker target equal to the canonical HUD pane', async () => {
7142
+ await assert.rejects(() => sendToWorker('ignored-session', 1, 'check inbox', '%9', undefined, process.pid, 'team:expected', '%9'), /HUD target: %9/);
5175
7143
  });
5176
- it('treats an existing non-dead pane id as live even when pane_pid is unavailable', async () => {
5177
- await withMockTmuxFixture('omx-pane-id-liveness-', (logPath) => `#!/bin/sh
7144
+ it('revalidates an explicit pane before later kill effects', async () => {
7145
+ await withMockTmuxFixture('omx-exact-pane-kill-revalidation-', (logPath) => `#!/bin/sh
5178
7146
  set -eu
7147
+ state_dir="$(dirname "${logPath}")"
7148
+ count_file="$state_dir/list-count"
5179
7149
  printf '%s\n' "$*" >> "${logPath}"
5180
- case "\${1:-}" in
5181
- -V)
5182
- echo "tmux 3.4"
5183
- exit 0
5184
- ;;
7150
+ case "$1" in
5185
7151
  list-panes)
5186
- if [ "$2" = "-a" ]; then
5187
- printf "%%77 0 \\n%%88 1 \\n"
5188
- exit 0
7152
+ count=0
7153
+ if [ -f "$count_file" ]; then count=$(cat "$count_file"); fi
7154
+ count=$((count + 1))
7155
+ printf '%s' "$count" > "$count_file"
7156
+ if [ "$count" -le 9 ]; then
7157
+ printf '%%9\t0\t%s\n' "$PPID"
5189
7158
  fi
5190
- exit 1
7159
+ ;;
7160
+ show-option) printf 'team:kill-test\n' ;;
7161
+ send-keys|kill-pane)
5191
7162
  ;;
5192
7163
  *)
5193
7164
  exit 1
5194
7165
  ;;
5195
7166
  esac
5196
- `, async () => {
5197
- assert.equal(isWorkerAlive('ignored-session', 1, '%77'), true);
5198
- assert.equal(isWorkerPaneOpen('ignored-session', 1, '%77'), true);
5199
- assert.equal(isWorkerAlive('ignored-session', 2, '%88'), false);
7167
+ `, async ({ logPath }) => {
7168
+ await killWorker('ignored-session', 1, '%9', undefined, process.pid, 'team:kill-test');
7169
+ const commands = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean);
7170
+ const exactGlobalPaneProof = /^list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}$/;
7171
+ const effects = commands
7172
+ .map((command, index) => ({ command, index }))
7173
+ .filter(({ command }) => /^(send-keys|kill-pane) -t %9\b/.test(command));
7174
+ assert.ok(effects.length > 0, `expected an explicit pane effect in log:\n${commands.join('\n')}`);
7175
+ for (const { command, index } of effects) {
7176
+ assert.match(commands[index - 1] ?? '', exactGlobalPaneProof, `fresh proof must immediately precede ${command}`);
7177
+ }
7178
+ assert.match(commands.join('\n'), /send-keys -t %9 C-c/);
7179
+ assert.match(commands.join('\n'), /send-keys -t %9 C-d/);
7180
+ assert.doesNotMatch(commands.join('\n'), /kill-pane -t %9/);
7181
+ });
7182
+ });
7183
+ it('pins an explicit pane PID through later control and kill effects', async () => {
7184
+ await withMockTmuxFixture('omx-exact-pane-kill-pid-pin-', (logPath) => `#!/bin/sh
7185
+ set -eu
7186
+ state_dir="$(dirname "${logPath}")"
7187
+ exit_file="$state_dir/exit"
7188
+ printf '%s\n' "$*" >> "${logPath}"
7189
+ case "$1" in
7190
+ list-panes)
7191
+ if [ -f "$exit_file" ]; then
7192
+ printf '%%9\t0\t%s\n' "$((PPID + 1))"
7193
+ else
7194
+ printf '%%9\t0\t%s\n' "$PPID"
7195
+ fi
7196
+ ;;
7197
+ show-option) printf 'team:kill-test\n' ;;
7198
+ send-keys)
7199
+ case "$*" in
7200
+ *" C-d") : > "$exit_file" ;;
7201
+ esac
7202
+ ;;
7203
+ kill-pane) exit 0 ;;
7204
+ *) exit 1 ;;
7205
+ esac
7206
+ `, async ({ logPath }) => {
7207
+ await assert.rejects(() => killWorker('ignored-session', 1, '%9', undefined, process.pid, 'team:kill-test'), /tmux pane identity changed: %9/);
7208
+ const commands = await readFile(logPath, 'utf-8');
7209
+ assert.match(commands, /send-keys -t %9 C-c/);
7210
+ assert.match(commands, /send-keys -t %9 C-d/);
7211
+ assert.doesNotMatch(commands, /kill-pane -t %9/);
7212
+ });
7213
+ });
7214
+ it('uses the pinned liveness proof PID without an unconstrained second proof', async () => {
7215
+ await withMockTmuxFixture('omx-exact-pane-kill-liveness-pid-pin-', (logPath) => `#!/bin/sh
7216
+ set -eu
7217
+ state_dir="$(dirname "${logPath}")"
7218
+ count_file="$state_dir/list-count"
7219
+ printf '%s\n' "$*" >> "${logPath}"
7220
+ case "$1" in
7221
+ list-panes)
7222
+ count=0
7223
+ if [ -f "$count_file" ]; then count=$(cat "$count_file"); fi
7224
+ count=$((count + 1))
7225
+ printf '%s' "$count" > "$count_file"
7226
+ if [ -f "$state_dir/killed" ]; then
7227
+ :
7228
+ elif [ "$count" -le 12 ]; then
7229
+ printf '%%9\t0\t%s\n' "$PPID"
7230
+ else
7231
+ printf '%%9\t0\t999999999\n'
7232
+ fi
7233
+ ;;
7234
+ show-option) printf 'team:kill-test\n' ;;
7235
+ send-keys) ;;
7236
+ kill-pane) : > "$state_dir/killed" ;;
7237
+ *) exit 1 ;;
7238
+ esac
7239
+ `, async ({ logPath }) => {
7240
+ await killWorker('ignored-session', 1, '%9', undefined, process.pid, 'team:kill-test');
7241
+ const commands = await readFile(logPath, 'utf-8');
7242
+ assert.match(commands, /send-keys -t %9 C-c/);
7243
+ assert.match(commands, /send-keys -t %9 C-d/);
7244
+ assert.equal((commands.match(/list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}/g) || []).length, 13, `liveness and effects must each use the pinned proof around owner authorization:\n${commands}`);
5200
7245
  });
5201
7246
  });
5202
7247
  });
@@ -5390,30 +7435,66 @@ describe('enableMouseScrolling', () => {
5390
7435
  }
5391
7436
  });
5392
7437
  });
5393
- describe('killWorkerByPaneId leader pane guard', () => {
5394
- it('skips kill when workerPaneId matches leaderPaneId (guard fires before tmux is called)', () => {
5395
- // With empty PATH tmux is unavailable, so any actual kill-pane call would fail.
5396
- // When the guard fires (paneId === leaderPaneId) the function returns early
5397
- // without invoking tmux, so no error is thrown regardless of PATH.
7438
+ describe('killWorkerByPaneId exact PID guard', () => {
7439
+ it('skips kill when workerPaneId matches leaderPaneId before tmux is called', () => {
5398
7440
  withEmptyPath(() => {
5399
- assert.doesNotThrow(() => killWorkerByPaneId('%5', '%5'));
7441
+ assert.doesNotThrow(() => killWorkerByPaneId('%5', 5, '%5'));
5400
7442
  });
5401
7443
  });
5402
- it('does not skip kill when pane ids differ (falls through to tmux attempt)', () => {
5403
- // Different IDs: guard does not fire. tmux is unavailable but kill errors are swallowed internally.
7444
+ it('requires a positive expected PID for an explicit direct pane kill', () => {
5404
7445
  withEmptyPath(() => {
5405
- assert.doesNotThrow(() => killWorkerByPaneId('%5', '%6'));
7446
+ assert.doesNotThrow(() => killWorkerByPaneId('%5'));
7447
+ assert.doesNotThrow(() => killWorkerByPaneId('%5', 0));
7448
+ assert.doesNotThrow(() => killWorkerByPaneId('%5', -1));
5406
7449
  });
5407
7450
  });
5408
- it('skips kill for non-percent pane id without reaching tmux', () => {
7451
+ it('keeps blank pane IDs as no-effect compatibility values', () => {
5409
7452
  withEmptyPath(() => {
5410
- assert.doesNotThrow(() => killWorkerByPaneId('invalid', '%5'));
7453
+ assert.doesNotThrow(() => killWorkerByPaneId('', 5));
7454
+ assert.doesNotThrow(() => killWorkerByPaneId(' ', 5));
5411
7455
  });
5412
7456
  });
5413
- it('skips kill when no leaderPaneId provided and pane id is valid percent id', () => {
5414
- // Without leaderPaneId the guard is not active; tmux call fails gracefully.
5415
- withEmptyPath(() => {
5416
- assert.doesNotThrow(() => killWorkerByPaneId('%5'));
7457
+ it('does not kill a recycled pane ID when its current PID differs from the frozen PID', async () => {
7458
+ await withMockTmuxFixture('omx-direct-kill-recycled-sync-', (logPath) => `#!/bin/sh
7459
+ printf '%s\\n' "$*" >> "${logPath}"
7460
+ if [ "$1" = "list-panes" ]; then
7461
+ printf '%%5\t0\t222\n'
7462
+ elif [ "$1" = "show-option" ]; then
7463
+ printf 'team:expected\n'
7464
+ fi
7465
+ `, async ({ logPath }) => {
7466
+ killWorkerByPaneId('%5', 111, undefined, 'team:expected', '%6');
7467
+ const log = await readFile(logPath, 'utf-8');
7468
+ assert.match(log, /list-panes -a/);
7469
+ assert.doesNotMatch(log, /kill-pane -t %5/);
7470
+ });
7471
+ });
7472
+ it('does not kill a recycled pane ID asynchronously when its current PID differs from the frozen PID', async () => {
7473
+ await withMockTmuxFixture('omx-direct-kill-recycled-async-', (logPath) => `#!/bin/sh
7474
+ printf '%s\\n' "$*" >> "${logPath}"
7475
+ if [ "$1" = "list-panes" ]; then
7476
+ printf '%%5\t0\t222\n'
7477
+ elif [ "$1" = "show-option" ]; then
7478
+ printf 'team:expected\n'
7479
+ fi
7480
+ `, async ({ logPath }) => {
7481
+ await killWorkerByPaneIdAsync('%5', 111, undefined, 'team:expected', '%6');
7482
+ const log = await readFile(logPath, 'utf-8');
7483
+ assert.match(log, /list-panes -a/);
7484
+ assert.doesNotMatch(log, /kill-pane -t %5/);
7485
+ });
7486
+ });
7487
+ it('rejects unbound, HUD, and owner-taken-over direct kill targets without effects', async () => {
7488
+ await withMockTmuxFixture('omx-direct-kill-owner-hud-', (logPath) => `#!/bin/sh
7489
+ printf '%s\\n' "$*" >> "${logPath}"
7490
+ case "$1" in
7491
+ list-panes) printf '%%5\\t0\\t111\\n' ;;
7492
+ show-option) printf 'team:foreign\\n' ;;
7493
+ esac`, async ({ logPath }) => {
7494
+ killWorkerByPaneId('%5', 111);
7495
+ killWorkerByPaneId('%5', 111, undefined, 'team:expected', '%5');
7496
+ killWorkerByPaneId('%5', 111, undefined, 'team:expected', '%6');
7497
+ assert.doesNotMatch(await readFile(logPath, 'utf8'), /kill-pane/);
5417
7498
  });
5418
7499
  });
5419
7500
  });
@@ -5496,6 +7577,21 @@ esac
5496
7577
  assert.doesNotMatch(tmuxLog, /terminal-overrides/);
5497
7578
  });
5498
7579
  });
7580
+ it('requires a fresh caller authorization before every post-sleep session mouse and copy-mode operation', async () => {
7581
+ await withMockTmuxFixture('omx-tmux-enable-mouse-reproof-', (tmuxLogPath) => `#!/bin/sh
7582
+ printf '%s\n' "$*" >> "${tmuxLogPath}"
7583
+ case "$1" in
7584
+ show-options) printf '%s\n' 'bg=yellow,fg=black,underscore' ;;
7585
+ set-option) exit 0 ;;
7586
+ esac
7587
+ `, async ({ logPath }) => {
7588
+ let authorizations = 0;
7589
+ assert.equal(enableMouseScrolling('omx-team-x', () => { authorizations += 1; }), true);
7590
+ const effects = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean);
7591
+ assert.equal(authorizations, effects.length);
7592
+ assert.ok(authorizations > 2, 'copy-mode reads and mutations must not share a mouse proof');
7593
+ });
7594
+ });
5499
7595
  });
5500
7596
  describe('mitigateCopyModeUnderlineArtifacts', () => {
5501
7597
  it('best-effort sanitizes copy-mode style options without requiring global tmux changes', async () => {
@@ -5539,13 +7635,13 @@ describe('killWorker leader pane guard', () => {
5539
7635
  assert.doesNotThrow(() => killWorker('omx-team-x:0', 1, '%5', '%5'));
5540
7636
  });
5541
7637
  });
5542
- it('proceeds (gracefully) when pane ids differ', () => {
5543
- // Guard does not fire; tmux calls fail gracefully with empty PATH.
7638
+ it('does not send control keys when a differing explicit pane ID cannot be proven live', () => {
7639
+ // The leader guard does not fire, but global proof failure remains fail-closed.
5544
7640
  withEmptyPath(() => {
5545
7641
  assert.doesNotThrow(() => killWorker('omx-team-x:0', 1, '%5', '%6'));
5546
7642
  });
5547
7643
  });
5548
- it('proceeds when leaderPaneId is not provided', () => {
7644
+ it('does not send control keys without a leader guard when proof is unavailable', () => {
5549
7645
  withEmptyPath(() => {
5550
7646
  assert.doesNotThrow(() => killWorker('omx-team-x:0', 1, '%5'));
5551
7647
  });
@@ -5555,13 +7651,25 @@ describe('teardownWorkerPanes shared primitive', () => {
5555
7651
  it('excludes leader and hud panes in shared pane-kill primitive', async () => {
5556
7652
  await withMockTmuxFixture('omx-tmux-teardown-', (logPath) => `#!/bin/sh
5557
7653
  set -eu
5558
- printf '%s\\n' "$*" >> "${logPath}"
5559
- exit 0
5560
- `, async ({ logPath }) => {
7654
+ printf '%s\n' "$*" >> "${logPath}"
7655
+ case "$1" in
7656
+ list-panes)
7657
+ if [ ! -f "${logPath}.dead" ]; then
7658
+ printf '%%3\t0\t%s\n' "$PPID"
7659
+ fi
7660
+ ;;
7661
+ kill-pane)
7662
+ : > "${logPath}.dead"
7663
+ ;;
7664
+ *)
7665
+ exit 0
7666
+ ;;
7667
+ esac`, async ({ logPath }) => {
5561
7668
  const summary = await teardownWorkerPanes(['%1', '%2', '%3'], {
5562
7669
  leaderPaneId: '%1',
5563
7670
  hudPaneId: '%2',
5564
7671
  graceMs: 1,
7672
+ expectedPanePids: { '%3': process.pid },
5565
7673
  });
5566
7674
  assert.equal(summary.excluded.leader, 1);
5567
7675
  assert.equal(summary.excluded.hud, 1);
@@ -5573,28 +7681,89 @@ exit 0
5573
7681
  assert.doesNotMatch(log, /kill-pane -t %2/);
5574
7682
  });
5575
7683
  });
5576
- it('uses pane-id-direct kill semantics without liveness-gated helper calls', async () => {
5577
- const source = await readFile(new URL('../tmux-session.js', import.meta.url), 'utf-8');
5578
- const primitiveBlock = source.split('export async function teardownWorkerPanes')[1] ?? '';
5579
- assert.equal(primitiveBlock.includes('isWorkerAlive'), false);
5580
- assert.equal(primitiveBlock.includes('killWorker('), false);
7684
+ it('treats an exit-zero kill as unresolved when the pane ID is freshly reused', async () => {
7685
+ await withMockTmuxFixture('omx-tmux-teardown-reused-pane-', (logPath) => `#!/bin/sh
7686
+ set -eu
7687
+ printf '%s\n' "$*" >> "${logPath}"
7688
+ case "$1" in
7689
+ list-panes)
7690
+ if [ -f "${logPath}.killed" ]; then
7691
+ printf '%%77\t0\t7702\n'
7692
+ else
7693
+ printf '%%77\t0\t7701\n'
7694
+ fi
7695
+ ;;
7696
+ kill-pane)
7697
+ : > "${logPath}.killed"
7698
+ ;;
7699
+ esac`, async ({ logPath }) => {
7700
+ const summary = await teardownWorkerPanes(['%77'], { graceMs: 1, expectedPanePids: { '%77': 7701 } });
7701
+ assert.equal(summary.kill.attempted, 1);
7702
+ assert.equal(summary.kill.succeeded, 0);
7703
+ assert.equal(summary.kill.failed, 1);
7704
+ assert.deepEqual(summary.kill.failedPaneIds, ['%77']);
7705
+ assert.deepEqual(summary.killedPaneIds, []);
7706
+ const commands = (await readFile(logPath, 'utf-8')).trim().split('\n');
7707
+ assert.deepEqual(commands, [
7708
+ 'list-panes -a -F #{pane_id}\t#{pane_dead}\t#{pane_pid}',
7709
+ 'list-panes -a -F #{pane_id}\t#{pane_dead}\t#{pane_pid}',
7710
+ 'kill-pane -t %77',
7711
+ 'list-panes -a -F #{pane_id}\t#{pane_dead}\t#{pane_pid}',
7712
+ ]);
7713
+ });
5581
7714
  });
5582
- it('continues best-effort when a pane target is missing', async () => {
5583
- await withMockTmuxFixture('omx-tmux-teardown-missing-', (logPath) => `#!/bin/sh
7715
+ it('stops at the first unavailable proof without killing a later live candidate', async () => {
7716
+ await withMockTmuxFixture('omx-tmux-teardown-proof-unavailable-', (logPath) => {
7717
+ const proofStatePath = `${logPath}.proof-state`;
7718
+ return `#!/bin/sh
5584
7719
  set -eu
5585
- printf '%s\\n' "$*" >> "${logPath}"
5586
- if [ "$1" = "kill-pane" ] && [ "\${3:-}" = "%404" ]; then
5587
- echo "missing pane" >&2
5588
- exit 1
7720
+ printf '%s\n' "$*" >> "${logPath}"
7721
+ if [ "$1" = "list-panes" ]; then
7722
+ if [ -f "${proofStatePath}" ]; then
7723
+ printf '%%405\t0\t%s\n' "$$"
7724
+ else
7725
+ : > "${proofStatePath}"
7726
+ echo "tmux unavailable" >&2
7727
+ exit 1
7728
+ fi
5589
7729
  fi
5590
7730
  exit 0
5591
- `, async ({ logPath }) => {
7731
+ `;
7732
+ }, async ({ logPath }) => {
5592
7733
  const summary = await teardownWorkerPanes(['%404', '%405'], { graceMs: 1 });
5593
- assert.equal(summary.kill.attempted, 2);
5594
- assert.equal(summary.kill.succeeded, 1);
7734
+ assert.equal(summary.kill.attempted, 0);
7735
+ assert.deepEqual(summary.provenGonePaneIds, []);
7736
+ assert.deepEqual(summary.proofUnavailable.map((proof) => proof.paneId), ['%404']);
7737
+ const commands = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean);
7738
+ assert.deepEqual(commands, ['list-panes -a -F #{pane_id}\t#{pane_dead}\t#{pane_pid}']);
7739
+ });
7740
+ });
7741
+ it('distinguishes proven-gone panes from target command failures', async () => {
7742
+ await withMockTmuxFixture('omx-tmux-teardown-proof-outcomes-', (logPath) => `#!/bin/sh
7743
+ set -eu
7744
+ printf '%s\n' "$*" >> "${logPath}"
7745
+ case "$1" in
7746
+ list-panes)
7747
+ printf '%%405\t0\t%s\n' "$PPID"
7748
+ ;;
7749
+ kill-pane)
7750
+ echo "target command failed" >&2
7751
+ exit 1
7752
+ ;;
7753
+ *)
7754
+ exit 0
7755
+ ;;
7756
+ esac
7757
+ `, async ({ logPath }) => {
7758
+ const summary = await teardownWorkerPanes(['%404', '%405'], { graceMs: 1, expectedPanePids: { '%405': process.pid } });
7759
+ assert.equal(summary.kill.attempted, 1);
7760
+ assert.equal(summary.kill.succeeded, 0);
5595
7761
  assert.equal(summary.kill.failed, 1);
7762
+ assert.deepEqual(summary.provenGonePaneIds, ['%404']);
7763
+ assert.deepEqual(summary.proofUnavailable, []);
7764
+ assert.deepEqual(summary.kill.failedPaneIds, ['%405']);
5596
7765
  const log = await readFile(logPath, 'utf-8');
5597
- assert.match(log, /kill-pane -t %404/);
7766
+ assert.doesNotMatch(log, /kill-pane -t %404/);
5598
7767
  assert.match(log, /kill-pane -t %405/);
5599
7768
  });
5600
7769
  });
@@ -5604,4 +7773,100 @@ describe('leader mailbox-only boundary', () => {
5604
7773
  assert.equal('sendToLeaderPane' in tmuxSessionModule, false);
5605
7774
  });
5606
7775
  });
7776
+ describe('exact pane PID authority regressions', () => {
7777
+ it('ignores an unrelated PID-less pane while requiring the matched target PID', async () => {
7778
+ await withMockTmuxFixture('omx-exact-pane-empty-unrelated-', () => `#!/bin/sh
7779
+ set -eu
7780
+ if [ "$1" = "list-panes" ]; then
7781
+ printf '%%42\t0\t4242\n%%99\t0\t\n'
7782
+ fi
7783
+ `, async () => {
7784
+ const proof = await readExactPaneProof('%42');
7785
+ assert.deepEqual(proof, { status: 'live', paneId: '%42', pid: 4242 });
7786
+ const emptyTarget = await readExactPaneProof('%99');
7787
+ assert.equal(emptyTarget.status, 'unavailable');
7788
+ if (emptyTarget.status === 'unavailable')
7789
+ assert.equal(emptyTarget.reason, 'malformed_snapshot');
7790
+ });
7791
+ });
7792
+ it('retains a live pane as unresolved teardown debt when no positive PID was persisted', async () => {
7793
+ await withMockTmuxFixture('omx-teardown-pid-missing-', (logPath) => `#!/bin/sh
7794
+ printf '%s\\n' "$*" >> "${logPath}"
7795
+ if [ "$1" = "list-panes" ]; then printf '%%42\\t0\\t4242\\n'; fi`, async ({ logPath }) => {
7796
+ const summary = await teardownWorkerPanes(['%42'], { graceMs: 1 });
7797
+ assert.equal(summary.kill.attempted, 0);
7798
+ assert.deepEqual(summary.proofUnavailable.map((proof) => proof.paneId), ['%42']);
7799
+ assert.doesNotMatch(await readFile(logPath, 'utf8'), /kill-pane/);
7800
+ });
7801
+ });
7802
+ it('does not kill a stale pane ID whose live PID differs from persisted identity', async () => {
7803
+ await withMockTmuxFixture('omx-teardown-pid-changed-', (logPath) => `#!/bin/sh
7804
+ set -eu
7805
+ printf '%s\n' "$*" >> "${logPath}"
7806
+ if [ "$1" = "list-panes" ]; then printf '%%42\t0\t4343\n'; fi
7807
+ `, async ({ logPath }) => {
7808
+ const summary = await teardownWorkerPanes(['%42'], {
7809
+ graceMs: 1,
7810
+ expectedPanePids: { '%42': 4242 },
7811
+ });
7812
+ assert.deepEqual(summary.proofUnavailable.map((proof) => proof.reason), ['pane_pid_changed']);
7813
+ assert.doesNotMatch(await readFile(logPath, 'utf8'), /kill-pane/);
7814
+ });
7815
+ });
7816
+ it('does not kill a replacement pane when owner authorization recycles its ID', async () => {
7817
+ await withMockTmuxFixture('omx-teardown-owner-read-pid-reuse-', (logPath) => `#!/bin/sh
7818
+ set -eu
7819
+ printf '%s\\n' "$*" >> "${logPath}"
7820
+ case "$1" in
7821
+ list-panes)
7822
+ if [ -f "${logPath}.owner-read" ]; then
7823
+ printf '%%42\\t0\\t4343\\n'
7824
+ else
7825
+ printf '%%42\\t0\\t4242\\n'
7826
+ fi
7827
+ ;;
7828
+ show-options)
7829
+ : > "${logPath}.owner-read"
7830
+ echo 'team:owned'
7831
+ ;;
7832
+ esac
7833
+ `, async ({ logPath }) => {
7834
+ const summary = await teardownWorkerPanes(['%42'], {
7835
+ graceMs: 1,
7836
+ expectedPanePids: { '%42': 4242 },
7837
+ authorizePaneKill: () => {
7838
+ spawnSync('tmux', ['show-options', '-p', '-t', '%42', '@omx_team_pane_owner_id']);
7839
+ return true;
7840
+ },
7841
+ });
7842
+ assert.deepEqual(summary.proofUnavailable.map((proof) => proof.reason), ['pane_pid_changed']);
7843
+ assert.doesNotMatch(await readFile(logPath, 'utf8'), /kill-pane/);
7844
+ });
7845
+ });
7846
+ it('preserves an empty pane_start_command field but rejects a missing trailing field before startup mutation', async () => {
7847
+ await withMockTmuxFixture('omx-startup-topology-trailing-field-', (logPath) => `#!/bin/sh
7848
+ set -eu
7849
+ printf '%s\\n' "$*" >> "${logPath}"
7850
+ if [ "$1" = "list-panes" ]; then
7851
+ case "$*" in
7852
+ *"pane_current_command"*) printf '%%11\\tzsh\\t\\n' ;;
7853
+ *) printf '%%11\\n' ;;
7854
+ esac
7855
+ fi
7856
+ `, async ({ logPath }) => {
7857
+ assert.deepEqual(listPaneIds('leader:0'), ['%11']);
7858
+ const log = await readFile(logPath, 'utf8');
7859
+ assert.doesNotMatch(log, /set-option|split-window|resize-pane|select-layout|set-window-option|select-pane|send-keys/);
7860
+ });
7861
+ await withMockTmuxFixture('omx-startup-topology-missing-field-', (logPath) => `#!/bin/sh
7862
+ set -eu
7863
+ printf '%s\\n' "$*" >> "${logPath}"
7864
+ if [ "$1" = "list-panes" ]; then printf '%%11\\tzsh\\n'; fi
7865
+ `, async ({ logPath }) => {
7866
+ assert.deepEqual(listPaneIds('leader:0'), []);
7867
+ const log = await readFile(logPath, 'utf8');
7868
+ assert.doesNotMatch(log, /set-option|split-window|resize-pane|select-layout|set-window-option|select-pane|send-keys/);
7869
+ });
7870
+ });
7871
+ });
5607
7872
  //# sourceMappingURL=tmux-session.test.js.map