oh-my-codex 0.20.2 → 0.20.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (805) hide show
  1. package/Cargo.lock +36 -18
  2. package/Cargo.toml +1 -1
  3. package/README.md +28 -2
  4. package/crates/omx-runtime/Cargo.toml +4 -0
  5. package/crates/omx-runtime/src/main.rs +415 -2
  6. package/crates/omx-runtime/tests/execution.rs +483 -0
  7. package/crates/omx-runtime-core/src/dispatch.rs +96 -10
  8. package/crates/omx-runtime-core/src/engine.rs +839 -29
  9. package/crates/omx-runtime-core/src/lib.rs +8 -0
  10. package/dist/adapt/__tests__/herdr-target.test.d.ts +2 -0
  11. package/dist/adapt/__tests__/herdr-target.test.d.ts.map +1 -0
  12. package/dist/adapt/__tests__/herdr-target.test.js +72 -0
  13. package/dist/adapt/__tests__/herdr-target.test.js.map +1 -0
  14. package/dist/adapt/contracts.d.ts +1 -1
  15. package/dist/adapt/contracts.d.ts.map +1 -1
  16. package/dist/adapt/contracts.js +1 -1
  17. package/dist/adapt/contracts.js.map +1 -1
  18. package/dist/adapt/herdr/__tests__/authority.test.d.ts +2 -0
  19. package/dist/adapt/herdr/__tests__/authority.test.d.ts.map +1 -0
  20. package/dist/adapt/herdr/__tests__/authority.test.js +49 -0
  21. package/dist/adapt/herdr/__tests__/authority.test.js.map +1 -0
  22. package/dist/adapt/herdr/__tests__/bridge.test.d.ts +2 -0
  23. package/dist/adapt/herdr/__tests__/bridge.test.d.ts.map +1 -0
  24. package/dist/adapt/herdr/__tests__/bridge.test.js +176 -0
  25. package/dist/adapt/herdr/__tests__/bridge.test.js.map +1 -0
  26. package/dist/adapt/herdr/__tests__/sanitize.test.d.ts +2 -0
  27. package/dist/adapt/herdr/__tests__/sanitize.test.d.ts.map +1 -0
  28. package/dist/adapt/herdr/__tests__/sanitize.test.js +45 -0
  29. package/dist/adapt/herdr/__tests__/sanitize.test.js.map +1 -0
  30. package/dist/adapt/herdr/__tests__/semantic.test.d.ts +2 -0
  31. package/dist/adapt/herdr/__tests__/semantic.test.d.ts.map +1 -0
  32. package/dist/adapt/herdr/__tests__/semantic.test.js +130 -0
  33. package/dist/adapt/herdr/__tests__/semantic.test.js.map +1 -0
  34. package/dist/adapt/herdr/__tests__/seq-store.test.d.ts +2 -0
  35. package/dist/adapt/herdr/__tests__/seq-store.test.d.ts.map +1 -0
  36. package/dist/adapt/herdr/__tests__/seq-store.test.js +54 -0
  37. package/dist/adapt/herdr/__tests__/seq-store.test.js.map +1 -0
  38. package/dist/adapt/herdr/__tests__/socket-live.test.d.ts +2 -0
  39. package/dist/adapt/herdr/__tests__/socket-live.test.d.ts.map +1 -0
  40. package/dist/adapt/herdr/__tests__/socket-live.test.js +104 -0
  41. package/dist/adapt/herdr/__tests__/socket-live.test.js.map +1 -0
  42. package/dist/adapt/herdr/__tests__/team-map.test.d.ts +2 -0
  43. package/dist/adapt/herdr/__tests__/team-map.test.d.ts.map +1 -0
  44. package/dist/adapt/herdr/__tests__/team-map.test.js +46 -0
  45. package/dist/adapt/herdr/__tests__/team-map.test.js.map +1 -0
  46. package/dist/adapt/herdr/__tests__/transport.test.d.ts +2 -0
  47. package/dist/adapt/herdr/__tests__/transport.test.d.ts.map +1 -0
  48. package/dist/adapt/herdr/__tests__/transport.test.js +160 -0
  49. package/dist/adapt/herdr/__tests__/transport.test.js.map +1 -0
  50. package/dist/adapt/herdr/__tests__/wiring.test.d.ts +2 -0
  51. package/dist/adapt/herdr/__tests__/wiring.test.d.ts.map +1 -0
  52. package/dist/adapt/herdr/__tests__/wiring.test.js +118 -0
  53. package/dist/adapt/herdr/__tests__/wiring.test.js.map +1 -0
  54. package/dist/adapt/herdr/authority.d.ts +34 -0
  55. package/dist/adapt/herdr/authority.d.ts.map +1 -0
  56. package/dist/adapt/herdr/authority.js +71 -0
  57. package/dist/adapt/herdr/authority.js.map +1 -0
  58. package/dist/adapt/herdr/bridge.d.ts +84 -0
  59. package/dist/adapt/herdr/bridge.d.ts.map +1 -0
  60. package/dist/adapt/herdr/bridge.js +222 -0
  61. package/dist/adapt/herdr/bridge.js.map +1 -0
  62. package/dist/adapt/herdr/sanitize.d.ts +20 -0
  63. package/dist/adapt/herdr/sanitize.d.ts.map +1 -0
  64. package/dist/adapt/herdr/sanitize.js +70 -0
  65. package/dist/adapt/herdr/sanitize.js.map +1 -0
  66. package/dist/adapt/herdr/semantic.d.ts +67 -0
  67. package/dist/adapt/herdr/semantic.d.ts.map +1 -0
  68. package/dist/adapt/herdr/semantic.js +170 -0
  69. package/dist/adapt/herdr/semantic.js.map +1 -0
  70. package/dist/adapt/herdr/seq-store.d.ts +32 -0
  71. package/dist/adapt/herdr/seq-store.d.ts.map +1 -0
  72. package/dist/adapt/herdr/seq-store.js +126 -0
  73. package/dist/adapt/herdr/seq-store.js.map +1 -0
  74. package/dist/adapt/herdr/team-map.d.ts +30 -0
  75. package/dist/adapt/herdr/team-map.d.ts.map +1 -0
  76. package/dist/adapt/herdr/team-map.js +37 -0
  77. package/dist/adapt/herdr/team-map.js.map +1 -0
  78. package/dist/adapt/herdr/transport.d.ts +115 -0
  79. package/dist/adapt/herdr/transport.d.ts.map +1 -0
  80. package/dist/adapt/herdr/transport.js +260 -0
  81. package/dist/adapt/herdr/transport.js.map +1 -0
  82. package/dist/adapt/herdr/wiring.d.ts +24 -0
  83. package/dist/adapt/herdr/wiring.d.ts.map +1 -0
  84. package/dist/adapt/herdr/wiring.js +62 -0
  85. package/dist/adapt/herdr/wiring.js.map +1 -0
  86. package/dist/adapt/herdr.d.ts +19 -0
  87. package/dist/adapt/herdr.d.ts.map +1 -0
  88. package/dist/adapt/herdr.js +115 -0
  89. package/dist/adapt/herdr.js.map +1 -0
  90. package/dist/adapt/index.d.ts +4 -0
  91. package/dist/adapt/index.d.ts.map +1 -1
  92. package/dist/adapt/index.js +48 -0
  93. package/dist/adapt/index.js.map +1 -1
  94. package/dist/adapt/registry.d.ts.map +1 -1
  95. package/dist/adapt/registry.js +11 -0
  96. package/dist/adapt/registry.js.map +1 -1
  97. package/dist/agents/__tests__/definitions.test.js +81 -0
  98. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  99. package/dist/agents/__tests__/native-config.test.js +25 -0
  100. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  101. package/dist/agents/native-config.d.ts +2 -1
  102. package/dist/agents/native-config.d.ts.map +1 -1
  103. package/dist/agents/native-config.js.map +1 -1
  104. package/dist/auth/__tests__/hotswap.test.js +60 -1
  105. package/dist/auth/__tests__/hotswap.test.js.map +1 -1
  106. package/dist/auth/__tests__/storage.test.js +33 -1
  107. package/dist/auth/__tests__/storage.test.js.map +1 -1
  108. package/dist/auth/hotswap.d.ts +22 -2
  109. package/dist/auth/hotswap.d.ts.map +1 -1
  110. package/dist/auth/hotswap.js +107 -22
  111. package/dist/auth/hotswap.js.map +1 -1
  112. package/dist/auth/storage.js +1 -1
  113. package/dist/auth/storage.js.map +1 -1
  114. package/dist/autopilot/__tests__/ralplan-gate.test.js +86 -1333
  115. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  116. package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
  117. package/dist/autopilot/ralplan-gate.js +7 -6
  118. package/dist/autopilot/ralplan-gate.js.map +1 -1
  119. package/dist/cli/__tests__/agents-init.test.js +4 -23
  120. package/dist/cli/__tests__/agents-init.test.js.map +1 -1
  121. package/dist/cli/__tests__/api.test.js +61 -24
  122. package/dist/cli/__tests__/api.test.js.map +1 -1
  123. package/dist/cli/__tests__/auth.test.js +63 -7
  124. package/dist/cli/__tests__/auth.test.js.map +1 -1
  125. package/dist/cli/__tests__/codex-feature-probe.test.js +66 -1
  126. package/dist/cli/__tests__/codex-feature-probe.test.js.map +1 -1
  127. package/dist/cli/__tests__/codex-plugin-layout.test.js +150 -2
  128. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  129. package/dist/cli/__tests__/detached-leader-teardown.test.d.ts +2 -0
  130. package/dist/cli/__tests__/detached-leader-teardown.test.d.ts.map +1 -0
  131. package/dist/cli/__tests__/detached-leader-teardown.test.js +473 -0
  132. package/dist/cli/__tests__/detached-leader-teardown.test.js.map +1 -0
  133. package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts +2 -0
  134. package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts.map +1 -0
  135. package/dist/cli/__tests__/doctor-process-identity-readiness.test.js +103 -0
  136. package/dist/cli/__tests__/doctor-process-identity-readiness.test.js.map +1 -0
  137. package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts +2 -0
  138. package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts.map +1 -0
  139. package/dist/cli/__tests__/doctor-state-root-binding.test.js +139 -0
  140. package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -0
  141. package/dist/cli/__tests__/doctor-warning-copy.test.js +135 -1
  142. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  143. package/dist/cli/__tests__/exec.test.js +211 -0
  144. package/dist/cli/__tests__/exec.test.js.map +1 -1
  145. package/dist/cli/__tests__/index.test.js +878 -463
  146. package/dist/cli/__tests__/index.test.js.map +1 -1
  147. package/dist/cli/__tests__/launch-fallback.test.js +1290 -127
  148. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  149. package/dist/cli/__tests__/mcp-parity.test.js +8 -4
  150. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
  151. package/dist/cli/__tests__/native-assets.test.js +398 -16
  152. package/dist/cli/__tests__/native-assets.test.js.map +1 -1
  153. package/dist/cli/__tests__/native-hook-claim-journal.test.js +132 -6
  154. package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
  155. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +3 -3
  156. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  157. package/dist/cli/__tests__/ralph.test.js +3 -3
  158. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  159. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +49 -164
  160. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
  161. package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts +2 -0
  162. package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts.map +1 -0
  163. package/dist/cli/__tests__/ralplan-child-binding-3212.test.js +56 -0
  164. package/dist/cli/__tests__/ralplan-child-binding-3212.test.js.map +1 -0
  165. package/dist/cli/__tests__/ralplan.test.js +107 -109
  166. package/dist/cli/__tests__/ralplan.test.js.map +1 -1
  167. package/dist/cli/__tests__/resume.test.js +11 -10
  168. package/dist/cli/__tests__/resume.test.js.map +1 -1
  169. package/dist/cli/__tests__/session-scoped-runtime.test.js +1118 -43
  170. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  171. package/dist/cli/__tests__/session-search-help.test.js +25 -1
  172. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  173. package/dist/cli/__tests__/setup-agents-overwrite.test.js +8 -55
  174. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  175. package/dist/cli/__tests__/setup-install-mode.test.js +224 -7
  176. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  177. package/dist/cli/__tests__/sparkshell-cli.test.js +93 -41
  178. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  179. package/dist/cli/__tests__/team.test.js +150 -8
  180. package/dist/cli/__tests__/team.test.js.map +1 -1
  181. package/dist/cli/__tests__/ultragoal.test.js +117 -2
  182. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  183. package/dist/cli/__tests__/uninstall.test.js +95 -4
  184. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  185. package/dist/cli/__tests__/update.test.js +669 -177
  186. package/dist/cli/__tests__/update.test.js.map +1 -1
  187. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +33 -2
  188. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  189. package/dist/cli/api.d.ts.map +1 -1
  190. package/dist/cli/api.js +9 -10
  191. package/dist/cli/api.js.map +1 -1
  192. package/dist/cli/codex-feature-probe.d.ts +16 -0
  193. package/dist/cli/codex-feature-probe.d.ts.map +1 -1
  194. package/dist/cli/codex-feature-probe.js +44 -0
  195. package/dist/cli/codex-feature-probe.js.map +1 -1
  196. package/dist/cli/codex-home.d.ts.map +1 -1
  197. package/dist/cli/codex-home.js +7 -4
  198. package/dist/cli/codex-home.js.map +1 -1
  199. package/dist/cli/doctor.d.ts +20 -0
  200. package/dist/cli/doctor.d.ts.map +1 -1
  201. package/dist/cli/doctor.js +290 -3
  202. package/dist/cli/doctor.js.map +1 -1
  203. package/dist/cli/index.d.ts +234 -12
  204. package/dist/cli/index.d.ts.map +1 -1
  205. package/dist/cli/index.js +2409 -746
  206. package/dist/cli/index.js.map +1 -1
  207. package/dist/cli/mcp-parity.js +2 -2
  208. package/dist/cli/mcp-parity.js.map +1 -1
  209. package/dist/cli/native-assets.d.ts +25 -0
  210. package/dist/cli/native-assets.d.ts.map +1 -1
  211. package/dist/cli/native-assets.js +448 -51
  212. package/dist/cli/native-assets.js.map +1 -1
  213. package/dist/cli/native-hook-claim-journal.d.ts +20 -5
  214. package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
  215. package/dist/cli/native-hook-claim-journal.js +67 -32
  216. package/dist/cli/native-hook-claim-journal.js.map +1 -1
  217. package/dist/cli/package-manager-ownership.d.ts +63 -0
  218. package/dist/cli/package-manager-ownership.d.ts.map +1 -0
  219. package/dist/cli/package-manager-ownership.js +323 -0
  220. package/dist/cli/package-manager-ownership.js.map +1 -0
  221. package/dist/cli/ralph.js +2 -2
  222. package/dist/cli/ralph.js.map +1 -1
  223. package/dist/cli/ralplan.d.ts +5 -7
  224. package/dist/cli/ralplan.d.ts.map +1 -1
  225. package/dist/cli/ralplan.js +83 -134
  226. package/dist/cli/ralplan.js.map +1 -1
  227. package/dist/cli/session-search.d.ts +5 -0
  228. package/dist/cli/session-search.d.ts.map +1 -1
  229. package/dist/cli/session-search.js +87 -0
  230. package/dist/cli/session-search.js.map +1 -1
  231. package/dist/cli/setup-preferences.d.ts +15 -0
  232. package/dist/cli/setup-preferences.d.ts.map +1 -1
  233. package/dist/cli/setup-preferences.js +27 -1
  234. package/dist/cli/setup-preferences.js.map +1 -1
  235. package/dist/cli/setup.d.ts +8 -0
  236. package/dist/cli/setup.d.ts.map +1 -1
  237. package/dist/cli/setup.js +119 -47
  238. package/dist/cli/setup.js.map +1 -1
  239. package/dist/cli/sparkshell.d.ts.map +1 -1
  240. package/dist/cli/sparkshell.js +52 -9
  241. package/dist/cli/sparkshell.js.map +1 -1
  242. package/dist/cli/ultragoal.d.ts.map +1 -1
  243. package/dist/cli/ultragoal.js +2 -2
  244. package/dist/cli/ultragoal.js.map +1 -1
  245. package/dist/cli/uninstall.d.ts +9 -0
  246. package/dist/cli/uninstall.d.ts.map +1 -1
  247. package/dist/cli/uninstall.js +71 -30
  248. package/dist/cli/uninstall.js.map +1 -1
  249. package/dist/cli/update-worker.d.ts +2 -0
  250. package/dist/cli/update-worker.d.ts.map +1 -0
  251. package/dist/cli/update-worker.js +142 -0
  252. package/dist/cli/update-worker.js.map +1 -0
  253. package/dist/cli/update.d.ts +14 -22
  254. package/dist/cli/update.d.ts.map +1 -1
  255. package/dist/cli/update.js +192 -241
  256. package/dist/cli/update.js.map +1 -1
  257. package/dist/compat/__tests__/rust-runtime-compat.test.js +1 -1
  258. package/dist/config/__tests__/generator-idempotent.test.js +503 -30
  259. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  260. package/dist/config/__tests__/generator-notify.test.js +5 -3
  261. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  262. package/dist/config/__tests__/models.test.js +67 -3
  263. package/dist/config/__tests__/models.test.js.map +1 -1
  264. package/dist/config/generator.d.ts +27 -21
  265. package/dist/config/generator.d.ts.map +1 -1
  266. package/dist/config/generator.js +610 -152
  267. package/dist/config/generator.js.map +1 -1
  268. package/dist/config/models.d.ts +12 -2
  269. package/dist/config/models.d.ts.map +1 -1
  270. package/dist/config/models.js +15 -1
  271. package/dist/config/models.js.map +1 -1
  272. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +25 -0
  273. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
  274. package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -1
  275. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
  276. package/dist/goal-workflows/codex-goal-snapshot.js +2 -0
  277. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
  278. package/dist/hooks/__tests__/agents-overlay.test.js +5 -3
  279. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  280. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +27 -4
  281. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  282. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts +2 -0
  283. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts.map +1 -0
  284. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js +172 -0
  285. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js.map +1 -0
  286. package/dist/hooks/__tests__/keyword-detector.test.js +176 -37
  287. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  288. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +402 -545
  289. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  290. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +39 -4
  291. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  292. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +161 -26
  293. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  294. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +27 -0
  295. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  296. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +10 -118
  297. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
  298. package/dist/hooks/__tests__/notify-hook-regression-205.test.js +7 -0
  299. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
  300. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +533 -14
  301. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  302. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +159 -24
  303. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  304. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +482 -32
  305. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  306. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js +2 -1
  307. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js.map +1 -1
  308. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +68 -1
  309. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  310. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +7 -1
  311. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
  312. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +44 -4
  313. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  314. package/dist/hooks/__tests__/prompt-guidance-contract.test.js +9 -0
  315. package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
  316. package/dist/hooks/__tests__/session.test.js +3130 -365
  317. package/dist/hooks/__tests__/session.test.js.map +1 -1
  318. package/dist/hooks/agents-overlay.js +1 -1
  319. package/dist/hooks/agents-overlay.js.map +1 -1
  320. package/dist/hooks/extensibility/__tests__/dispatcher.test.js +108 -2
  321. package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
  322. package/dist/hooks/extensibility/__tests__/sdk.test.js +151 -1
  323. package/dist/hooks/extensibility/__tests__/sdk.test.js.map +1 -1
  324. package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
  325. package/dist/hooks/extensibility/dispatcher.js +20 -3
  326. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  327. package/dist/hooks/extensibility/plugin-runner.js +1 -0
  328. package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
  329. package/dist/hooks/extensibility/runtime.d.ts.map +1 -1
  330. package/dist/hooks/extensibility/runtime.js.map +1 -1
  331. package/dist/hooks/extensibility/sdk/paths.d.ts +1 -1
  332. package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -1
  333. package/dist/hooks/extensibility/sdk/paths.js +2 -2
  334. package/dist/hooks/extensibility/sdk/paths.js.map +1 -1
  335. package/dist/hooks/extensibility/sdk/runtime-state.d.ts +1 -1
  336. package/dist/hooks/extensibility/sdk/runtime-state.d.ts.map +1 -1
  337. package/dist/hooks/extensibility/sdk/runtime-state.js +55 -15
  338. package/dist/hooks/extensibility/sdk/runtime-state.js.map +1 -1
  339. package/dist/hooks/extensibility/sdk.d.ts +1 -0
  340. package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
  341. package/dist/hooks/extensibility/sdk.js +1 -1
  342. package/dist/hooks/extensibility/sdk.js.map +1 -1
  343. package/dist/hooks/extensibility/types.d.ts +7 -0
  344. package/dist/hooks/extensibility/types.d.ts.map +1 -1
  345. package/dist/hooks/keyword-detector.d.ts +10 -3
  346. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  347. package/dist/hooks/keyword-detector.js +116 -20
  348. package/dist/hooks/keyword-detector.js.map +1 -1
  349. package/dist/hooks/session.d.ts +204 -14
  350. package/dist/hooks/session.d.ts.map +1 -1
  351. package/dist/hooks/session.js +1500 -223
  352. package/dist/hooks/session.js.map +1 -1
  353. package/dist/hud/__tests__/hud-tmux-injection.test.js +6 -6
  354. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  355. package/dist/hud/__tests__/index.test.js +26 -16
  356. package/dist/hud/__tests__/index.test.js.map +1 -1
  357. package/dist/hud/__tests__/reconcile.test.js +83 -78
  358. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  359. package/dist/hud/__tests__/state.test.js +36 -31
  360. package/dist/hud/__tests__/state.test.js.map +1 -1
  361. package/dist/hud/__tests__/tmux.test.js +155 -198
  362. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  363. package/dist/hud/tmux.d.ts +42 -4
  364. package/dist/hud/tmux.d.ts.map +1 -1
  365. package/dist/hud/tmux.js +1017 -139
  366. package/dist/hud/tmux.js.map +1 -1
  367. package/dist/leader/__tests__/contract.test.js +124 -37
  368. package/dist/leader/__tests__/contract.test.js.map +1 -1
  369. package/dist/leader/contract.d.ts +17 -6
  370. package/dist/leader/contract.d.ts.map +1 -1
  371. package/dist/leader/contract.js +102 -26
  372. package/dist/leader/contract.js.map +1 -1
  373. package/dist/mcp/__tests__/code-intel-server.test.js +31 -3
  374. package/dist/mcp/__tests__/code-intel-server.test.js.map +1 -1
  375. package/dist/mcp/__tests__/hermes-bridge.test.js +17 -0
  376. package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
  377. package/dist/mcp/__tests__/server-lifecycle.test.js +4 -4
  378. package/dist/mcp/__tests__/state-paths.test.js +735 -4
  379. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  380. package/dist/mcp/__tests__/state-server.test.js +1 -1
  381. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  382. package/dist/mcp/__tests__/trace-server.test.js +2 -2
  383. package/dist/mcp/__tests__/trace-server.test.js.map +1 -1
  384. package/dist/mcp/__tests__/wiki-server.test.js +18 -0
  385. package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
  386. package/dist/mcp/code-intel-server.d.ts.map +1 -1
  387. package/dist/mcp/code-intel-server.js +13 -3
  388. package/dist/mcp/code-intel-server.js.map +1 -1
  389. package/dist/mcp/hermes-bridge.d.ts.map +1 -1
  390. package/dist/mcp/hermes-bridge.js +2 -2
  391. package/dist/mcp/hermes-bridge.js.map +1 -1
  392. package/dist/mcp/state-paths.d.ts +78 -4
  393. package/dist/mcp/state-paths.d.ts.map +1 -1
  394. package/dist/mcp/state-paths.js +543 -40
  395. package/dist/mcp/state-paths.js.map +1 -1
  396. package/dist/mcp/state-server.d.ts +4 -4
  397. package/dist/mcp/state-server.d.ts.map +1 -1
  398. package/dist/mcp/wiki-server.d.ts +11 -0
  399. package/dist/mcp/wiki-server.d.ts.map +1 -1
  400. package/dist/mcp/wiki-server.js +9 -2
  401. package/dist/mcp/wiki-server.js.map +1 -1
  402. package/dist/modes/__tests__/base-autopilot-gates.test.js +36 -7
  403. package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
  404. package/dist/modes/__tests__/base-multi-state-compat.test.js +1 -1
  405. package/dist/modes/__tests__/base-multi-state-compat.test.js.map +1 -1
  406. package/dist/modes/__tests__/base-session-scope.test.js +17 -8
  407. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  408. package/dist/modes/__tests__/base-tmux-pane.test.js +4 -4
  409. package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
  410. package/dist/modes/base.d.ts +5 -5
  411. package/dist/modes/base.d.ts.map +1 -1
  412. package/dist/modes/base.js +76 -21
  413. package/dist/modes/base.js.map +1 -1
  414. package/dist/native-assets/archive.d.ts +17 -0
  415. package/dist/native-assets/archive.d.ts.map +1 -0
  416. package/dist/native-assets/archive.js +238 -0
  417. package/dist/native-assets/archive.js.map +1 -0
  418. package/dist/native-assets/policy.d.ts +42 -0
  419. package/dist/native-assets/policy.d.ts.map +1 -0
  420. package/dist/native-assets/policy.js +155 -0
  421. package/dist/native-assets/policy.js.map +1 -0
  422. package/dist/notifications/temp-contract.d.ts.map +1 -1
  423. package/dist/notifications/temp-contract.js +4 -0
  424. package/dist/notifications/temp-contract.js.map +1 -1
  425. package/dist/pipeline/__tests__/orchestrator.test.js +164 -42
  426. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  427. package/dist/pipeline/__tests__/stages.test.js +72 -155
  428. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  429. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  430. package/dist/pipeline/orchestrator.js +124 -18
  431. package/dist/pipeline/orchestrator.js.map +1 -1
  432. package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
  433. package/dist/pipeline/stages/ralplan.js +36 -8
  434. package/dist/pipeline/stages/ralplan.js.map +1 -1
  435. package/dist/ralplan/__tests__/consensus-gate.test.js +73 -1316
  436. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
  437. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts +2 -0
  438. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts.map +1 -0
  439. package/dist/ralplan/__tests__/documented-leader-preflight.test.js +335 -0
  440. package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -0
  441. package/dist/ralplan/__tests__/runtime.test.js +85 -101
  442. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  443. package/dist/ralplan/consensus-gate.d.ts +9 -1
  444. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  445. package/dist/ralplan/consensus-gate.js +82 -338
  446. package/dist/ralplan/consensus-gate.js.map +1 -1
  447. package/dist/ralplan/documented-leader-preflight.d.ts +41 -0
  448. package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -0
  449. package/dist/ralplan/documented-leader-preflight.js +360 -0
  450. package/dist/ralplan/documented-leader-preflight.js.map +1 -0
  451. package/dist/ralplan/runtime.d.ts +2 -1
  452. package/dist/ralplan/runtime.d.ts.map +1 -1
  453. package/dist/ralplan/runtime.js +67 -168
  454. package/dist/ralplan/runtime.js.map +1 -1
  455. package/dist/runtime/__tests__/bridge.test.js +42 -1
  456. package/dist/runtime/__tests__/bridge.test.js.map +1 -1
  457. package/dist/runtime/bridge.d.ts +12 -0
  458. package/dist/runtime/bridge.d.ts.map +1 -1
  459. package/dist/runtime/bridge.js +52 -1
  460. package/dist/runtime/bridge.js.map +1 -1
  461. package/dist/runtime/run-state.d.ts +5 -1
  462. package/dist/runtime/run-state.d.ts.map +1 -1
  463. package/dist/runtime/run-state.js +15 -8
  464. package/dist/runtime/run-state.js.map +1 -1
  465. package/dist/scripts/__tests__/codex-native-hook.test.js +16323 -8978
  466. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  467. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts +2 -0
  468. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts.map +1 -0
  469. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js +225 -0
  470. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js.map +1 -0
  471. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts +2 -0
  472. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts.map +1 -0
  473. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js +188 -0
  474. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js.map +1 -0
  475. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts +2 -0
  476. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts.map +1 -0
  477. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js +259 -0
  478. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js.map +1 -0
  479. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts +2 -0
  480. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts.map +1 -0
  481. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js +491 -0
  482. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js.map +1 -0
  483. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts +2 -0
  484. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts.map +1 -0
  485. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js +708 -0
  486. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js.map +1 -0
  487. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts +2 -0
  488. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts.map +1 -0
  489. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js +208 -0
  490. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js.map +1 -0
  491. package/dist/scripts/__tests__/notify-state-io.test.js +26 -0
  492. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  493. package/dist/scripts/__tests__/notify-tmux-injection.test.js +23 -1
  494. package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -1
  495. package/dist/scripts/__tests__/postinstall.test.js +19 -1
  496. package/dist/scripts/__tests__/postinstall.test.js.map +1 -1
  497. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts +2 -0
  498. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts.map +1 -0
  499. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +108 -0
  500. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -0
  501. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +63 -173
  502. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
  503. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +74 -159
  504. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
  505. package/dist/scripts/__tests__/smoke-packed-install.test.js +404 -13
  506. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  507. package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts +2 -0
  508. package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts.map +1 -0
  509. package/dist/scripts/__tests__/verify-native-release-assets.test.js +235 -0
  510. package/dist/scripts/__tests__/verify-native-release-assets.test.js.map +1 -0
  511. package/dist/scripts/codex-native-hook.d.ts +53 -1
  512. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  513. package/dist/scripts/codex-native-hook.js +14448 -1480
  514. package/dist/scripts/codex-native-hook.js.map +1 -1
  515. package/dist/scripts/codex-native-pre-post.d.ts +9 -0
  516. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  517. package/dist/scripts/codex-native-pre-post.js +35 -12
  518. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  519. package/dist/scripts/generate-native-release-manifest.js +80 -51
  520. package/dist/scripts/generate-native-release-manifest.js.map +1 -1
  521. package/dist/scripts/notify-fallback-watcher.js +78 -115
  522. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  523. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  524. package/dist/scripts/notify-hook/auto-nudge.js +74 -3
  525. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  526. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  527. package/dist/scripts/notify-hook/state-io.js +5 -0
  528. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  529. package/dist/scripts/notify-hook/team-dispatch.d.ts +9 -0
  530. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  531. package/dist/scripts/notify-hook/team-dispatch.js +234 -79
  532. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  533. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  534. package/dist/scripts/notify-hook/team-leader-nudge.js +71 -16
  535. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  536. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +12 -4
  537. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  538. package/dist/scripts/notify-hook/team-tmux-guard.js +365 -44
  539. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  540. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  541. package/dist/scripts/notify-hook/team-worker-stop.js +42 -21
  542. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  543. package/dist/scripts/notify-hook/team-worker.d.ts +3 -0
  544. package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
  545. package/dist/scripts/notify-hook/team-worker.js +82 -33
  546. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  547. package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -0
  548. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  549. package/dist/scripts/notify-hook/tmux-injection.js +32 -3
  550. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  551. package/dist/scripts/notify-hook.js +79 -82
  552. package/dist/scripts/notify-hook.js.map +1 -1
  553. package/dist/scripts/postinstall-advisory.d.ts +14 -0
  554. package/dist/scripts/postinstall-advisory.d.ts.map +1 -0
  555. package/dist/scripts/postinstall-advisory.js +49 -0
  556. package/dist/scripts/postinstall-advisory.js.map +1 -0
  557. package/dist/scripts/postinstall.d.ts +1 -1
  558. package/dist/scripts/postinstall.d.ts.map +1 -1
  559. package/dist/scripts/postinstall.js +1 -1
  560. package/dist/scripts/postinstall.js.map +1 -1
  561. package/dist/scripts/smoke-packed-install.d.ts +80 -4
  562. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  563. package/dist/scripts/smoke-packed-install.js +3162 -26
  564. package/dist/scripts/smoke-packed-install.js.map +1 -1
  565. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  566. package/dist/scripts/sync-plugin-mirror.js +16 -1
  567. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  568. package/dist/scripts/verify-native-release-assets.js +37 -23
  569. package/dist/scripts/verify-native-release-assets.js.map +1 -1
  570. package/dist/state/__tests__/mode-state-context.test.js +61 -0
  571. package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
  572. package/dist/state/__tests__/operations.test.js +462 -850
  573. package/dist/state/__tests__/operations.test.js.map +1 -1
  574. package/dist/state/__tests__/skill-active.test.js +400 -2
  575. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  576. package/dist/state/mode-state-context.d.ts +8 -0
  577. package/dist/state/mode-state-context.d.ts.map +1 -1
  578. package/dist/state/mode-state-context.js +73 -0
  579. package/dist/state/mode-state-context.js.map +1 -1
  580. package/dist/state/operations.d.ts +3 -0
  581. package/dist/state/operations.d.ts.map +1 -1
  582. package/dist/state/operations.js +144 -40
  583. package/dist/state/operations.js.map +1 -1
  584. package/dist/state/skill-active.d.ts +18 -1
  585. package/dist/state/skill-active.d.ts.map +1 -1
  586. package/dist/state/skill-active.js +420 -23
  587. package/dist/state/skill-active.js.map +1 -1
  588. package/dist/state/workflow-transition-reconcile.d.ts +3 -0
  589. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  590. package/dist/state/workflow-transition-reconcile.js +10 -4
  591. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  592. package/dist/state/workflow-transition.d.ts.map +1 -1
  593. package/dist/state/workflow-transition.js +3 -1
  594. package/dist/state/workflow-transition.js.map +1 -1
  595. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +53 -247
  596. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
  597. package/dist/subagents/__tests__/tracker.test.js +837 -1048
  598. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  599. package/dist/subagents/tracker.d.ts +58 -135
  600. package/dist/subagents/tracker.d.ts.map +1 -1
  601. package/dist/subagents/tracker.js +811 -632
  602. package/dist/subagents/tracker.js.map +1 -1
  603. package/dist/team/__tests__/api-interop.test.js +91 -1
  604. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  605. package/dist/team/__tests__/delivery-e2e-smoke.test.js +58 -17
  606. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  607. package/dist/team/__tests__/idle-nudge.test.js +45 -15
  608. package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
  609. package/dist/team/__tests__/mcp-comm.test.js +197 -30
  610. package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
  611. package/dist/team/__tests__/model-contract.test.js +110 -3
  612. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  613. package/dist/team/__tests__/notice-ledger.test.d.ts +2 -0
  614. package/dist/team/__tests__/notice-ledger.test.d.ts.map +1 -0
  615. package/dist/team/__tests__/notice-ledger.test.js +131 -0
  616. package/dist/team/__tests__/notice-ledger.test.js.map +1 -0
  617. package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts +2 -0
  618. package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts.map +1 -0
  619. package/dist/team/__tests__/pane-teardown-residue-e2e.test.js +410 -0
  620. package/dist/team/__tests__/pane-teardown-residue-e2e.test.js.map +1 -0
  621. package/dist/team/__tests__/phase-controller.test.js +20 -0
  622. package/dist/team/__tests__/phase-controller.test.js.map +1 -1
  623. package/dist/team/__tests__/runtime-cli.test.js +9 -0
  624. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  625. package/dist/team/__tests__/runtime.test.js +3963 -428
  626. package/dist/team/__tests__/runtime.test.js.map +1 -1
  627. package/dist/team/__tests__/scaling.test.js +2405 -203
  628. package/dist/team/__tests__/scaling.test.js.map +1 -1
  629. package/dist/team/__tests__/state-root.test.js +117 -2
  630. package/dist/team/__tests__/state-root.test.js.map +1 -1
  631. package/dist/team/__tests__/state.test.js +804 -36
  632. package/dist/team/__tests__/state.test.js.map +1 -1
  633. package/dist/team/__tests__/team-ops-contract.test.js +6 -0
  634. package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
  635. package/dist/team/__tests__/tmux-session.test.js +4099 -1039
  636. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  637. package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts +2 -0
  638. package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts.map +1 -0
  639. package/dist/team/__tests__/tmux-source-authority-realtmux.test.js +149 -0
  640. package/dist/team/__tests__/tmux-source-authority-realtmux.test.js.map +1 -0
  641. package/dist/team/__tests__/tmux-test-fixture.d.ts +69 -0
  642. package/dist/team/__tests__/tmux-test-fixture.d.ts.map +1 -1
  643. package/dist/team/__tests__/tmux-test-fixture.js +242 -15
  644. package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
  645. package/dist/team/__tests__/tmux-test-fixture.test.js +177 -1
  646. package/dist/team/__tests__/tmux-test-fixture.test.js.map +1 -1
  647. package/dist/team/__tests__/worker-runtime-identity.test.js +53 -44
  648. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  649. package/dist/team/__tests__/worktree.test.js +6 -0
  650. package/dist/team/__tests__/worktree.test.js.map +1 -1
  651. package/dist/team/api-interop.d.ts.map +1 -1
  652. package/dist/team/api-interop.js +15 -9
  653. package/dist/team/api-interop.js.map +1 -1
  654. package/dist/team/exact-pane.d.ts +23 -0
  655. package/dist/team/exact-pane.d.ts.map +1 -0
  656. package/dist/team/exact-pane.js +133 -0
  657. package/dist/team/exact-pane.js.map +1 -0
  658. package/dist/team/idle-nudge.d.ts +12 -1
  659. package/dist/team/idle-nudge.d.ts.map +1 -1
  660. package/dist/team/idle-nudge.js +41 -14
  661. package/dist/team/idle-nudge.js.map +1 -1
  662. package/dist/team/model-contract.d.ts +2 -5
  663. package/dist/team/model-contract.d.ts.map +1 -1
  664. package/dist/team/model-contract.js +59 -17
  665. package/dist/team/model-contract.js.map +1 -1
  666. package/dist/team/notice-ledger.d.ts +54 -0
  667. package/dist/team/notice-ledger.d.ts.map +1 -0
  668. package/dist/team/notice-ledger.js +259 -0
  669. package/dist/team/notice-ledger.js.map +1 -0
  670. package/dist/team/phase-controller.d.ts.map +1 -1
  671. package/dist/team/phase-controller.js +2 -0
  672. package/dist/team/phase-controller.js.map +1 -1
  673. package/dist/team/runtime.d.ts +4 -1
  674. package/dist/team/runtime.d.ts.map +1 -1
  675. package/dist/team/runtime.js +1527 -291
  676. package/dist/team/runtime.js.map +1 -1
  677. package/dist/team/scaling.d.ts +13 -1
  678. package/dist/team/scaling.d.ts.map +1 -1
  679. package/dist/team/scaling.js +1503 -387
  680. package/dist/team/scaling.js.map +1 -1
  681. package/dist/team/state/dispatch.d.ts.map +1 -1
  682. package/dist/team/state/dispatch.js +26 -8
  683. package/dist/team/state/dispatch.js.map +1 -1
  684. package/dist/team/state/locks.d.ts.map +1 -1
  685. package/dist/team/state/locks.js +61 -13
  686. package/dist/team/state/locks.js.map +1 -1
  687. package/dist/team/state/monitor.d.ts +10 -0
  688. package/dist/team/state/monitor.d.ts.map +1 -1
  689. package/dist/team/state/monitor.js +13 -0
  690. package/dist/team/state/monitor.js.map +1 -1
  691. package/dist/team/state/tasks.d.ts.map +1 -1
  692. package/dist/team/state/tasks.js +4 -3
  693. package/dist/team/state/tasks.js.map +1 -1
  694. package/dist/team/state/types.d.ts +15 -0
  695. package/dist/team/state/types.d.ts.map +1 -1
  696. package/dist/team/state/types.js.map +1 -1
  697. package/dist/team/state-root.d.ts +12 -0
  698. package/dist/team/state-root.d.ts.map +1 -1
  699. package/dist/team/state-root.js +177 -0
  700. package/dist/team/state-root.js.map +1 -1
  701. package/dist/team/state.d.ts +85 -1
  702. package/dist/team/state.d.ts.map +1 -1
  703. package/dist/team/state.js +672 -129
  704. package/dist/team/state.js.map +1 -1
  705. package/dist/team/team-ops.d.ts +5 -0
  706. package/dist/team/team-ops.d.ts.map +1 -1
  707. package/dist/team/team-ops.js +4 -0
  708. package/dist/team/team-ops.js.map +1 -1
  709. package/dist/team/tmux-session.d.ts +163 -27
  710. package/dist/team/tmux-session.d.ts.map +1 -1
  711. package/dist/team/tmux-session.js +1682 -376
  712. package/dist/team/tmux-session.js.map +1 -1
  713. package/dist/team/worktree.d.ts.map +1 -1
  714. package/dist/team/worktree.js +4 -0
  715. package/dist/team/worktree.js.map +1 -1
  716. package/dist/ultragoal/__tests__/artifacts.test.js +950 -2
  717. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  718. package/dist/ultragoal/__tests__/docs-contract.test.js +1 -0
  719. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  720. package/dist/ultragoal/artifacts.d.ts +35 -1
  721. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  722. package/dist/ultragoal/artifacts.js +232 -46
  723. package/dist/ultragoal/artifacts.js.map +1 -1
  724. package/dist/utils/__tests__/file-durability.test.d.ts +2 -0
  725. package/dist/utils/__tests__/file-durability.test.d.ts.map +1 -0
  726. package/dist/utils/__tests__/file-durability.test.js +114 -0
  727. package/dist/utils/__tests__/file-durability.test.js.map +1 -0
  728. package/dist/utils/__tests__/paths.test.js +21 -1
  729. package/dist/utils/__tests__/paths.test.js.map +1 -1
  730. package/dist/utils/file-durability.d.ts +25 -0
  731. package/dist/utils/file-durability.d.ts.map +1 -0
  732. package/dist/utils/file-durability.js +67 -0
  733. package/dist/utils/file-durability.js.map +1 -0
  734. package/dist/utils/paths.d.ts.map +1 -1
  735. package/dist/utils/paths.js +14 -6
  736. package/dist/utils/paths.js.map +1 -1
  737. package/dist/verification/__tests__/ci-rust-gates.test.js +49 -4
  738. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  739. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +3 -0
  740. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
  741. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +60 -0
  742. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  743. package/dist/verification/__tests__/native-release-manifest.test.js +157 -4
  744. package/dist/verification/__tests__/native-release-manifest.test.js.map +1 -1
  745. package/dist/wiki/lint.d.ts +4 -1
  746. package/dist/wiki/lint.d.ts.map +1 -1
  747. package/dist/wiki/lint.js +3 -2
  748. package/dist/wiki/lint.js.map +1 -1
  749. package/package.json +13 -5
  750. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  751. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +105 -17
  752. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +13 -5
  753. package/plugins/oh-my-codex/skills/cancel/SKILL.md +30 -85
  754. package/plugins/oh-my-codex/skills/ralph/SKILL.md +6 -4
  755. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +16 -17
  756. package/plugins/oh-my-codex/skills/team/SKILL.md +13 -9
  757. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +3 -2
  758. package/skills/autopilot/SKILL.md +13 -5
  759. package/skills/cancel/SKILL.md +30 -85
  760. package/skills/ralph/SKILL.md +6 -4
  761. package/skills/ralplan/SKILL.md +16 -17
  762. package/skills/team/SKILL.md +13 -9
  763. package/skills/ultragoal/SKILL.md +3 -2
  764. package/src/scripts/__tests__/codex-native-hook.test.ts +8831 -951
  765. package/src/scripts/__tests__/issue-3293-callsite-parity.test.ts +187 -0
  766. package/src/scripts/__tests__/issue-3293-cancel-transaction.test.ts +189 -0
  767. package/src/scripts/__tests__/issue-3293-hook-owned-cancel.test.ts +187 -0
  768. package/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts +591 -0
  769. package/src/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.ts +701 -0
  770. package/src/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.ts +211 -0
  771. package/src/scripts/__tests__/notify-state-io.test.ts +29 -0
  772. package/src/scripts/__tests__/notify-tmux-injection.test.ts +26 -1
  773. package/src/scripts/__tests__/postinstall.test.ts +23 -1
  774. package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +129 -0
  775. package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +67 -185
  776. package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +79 -178
  777. package/src/scripts/__tests__/smoke-packed-install.test.ts +525 -16
  778. package/src/scripts/__tests__/verify-native-release-assets.test.ts +240 -0
  779. package/src/scripts/codex-native-hook.ts +14238 -1429
  780. package/src/scripts/codex-native-pre-post.ts +43 -12
  781. package/src/scripts/generate-native-release-manifest.ts +83 -79
  782. package/src/scripts/notify-fallback-watcher.ts +91 -122
  783. package/src/scripts/notify-hook/auto-nudge.ts +71 -3
  784. package/src/scripts/notify-hook/state-io.ts +11 -0
  785. package/src/scripts/notify-hook/team-dispatch.ts +242 -75
  786. package/src/scripts/notify-hook/team-leader-nudge.ts +72 -22
  787. package/src/scripts/notify-hook/team-tmux-guard.ts +366 -39
  788. package/src/scripts/notify-hook/team-worker-stop.ts +43 -20
  789. package/src/scripts/notify-hook/team-worker.ts +82 -31
  790. package/src/scripts/notify-hook/tmux-injection.ts +33 -3
  791. package/src/scripts/notify-hook.ts +84 -82
  792. package/src/scripts/postinstall-advisory.ts +67 -0
  793. package/src/scripts/postinstall.ts +1 -1
  794. package/src/scripts/smoke-packed-install.ts +3608 -39
  795. package/src/scripts/sync-plugin-mirror.ts +17 -1
  796. package/src/scripts/verify-native-release-assets.ts +39 -31
  797. package/templates/AGENTS.md +1 -0
  798. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +0 -2
  799. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +0 -1
  800. package/dist/subagents/__tests__/adapted-role-binding.test.js +0 -988
  801. package/dist/subagents/__tests__/adapted-role-binding.test.js.map +0 -1
  802. package/dist/subagents/adapted-role-binding.d.ts +0 -17
  803. package/dist/subagents/adapted-role-binding.d.ts.map +0 -1
  804. package/dist/subagents/adapted-role-binding.js +0 -75
  805. package/dist/subagents/adapted-role-binding.js.map +0 -1
@@ -1,8 +1,9 @@
1
1
  import { spawnSync, execFile } from 'child_process';
2
2
  import { promisify } from 'util';
3
- import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs';
3
+ import { closeSync, chmodSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'fs';
4
4
  import { homedir } from 'os';
5
- import { dirname, isAbsolute, join, resolve } from 'path';
5
+ import { randomBytes } from 'crypto';
6
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'path';
6
7
  import { CODEX_BYPASS_FLAG, CLAUDE_SKIP_PERMISSIONS_FLAG, MADMAX_FLAG, CONFIG_FLAG, LONG_CONFIG_FLAG, MODEL_FLAG, } from '../cli/constants.js';
7
8
  import { getAgent } from '../agents/definitions.js';
8
9
  import { buildCapturePaneArgv as sharedBuildCapturePaneArgv, buildVisibleCapturePaneArgv as sharedBuildVisibleCapturePaneArgv, normalizeTmuxCapture as sharedNormalizeTmuxCapture, paneHasActiveTask as sharedPaneHasActiveTask, paneIsBootstrapping as sharedPaneIsBootstrapping, paneShowsCodexViewport as sharedPaneShowsCodexViewport, paneLooksReady as sharedPaneLooksReady, } from '../scripts/tmux-hook-engine.js';
@@ -11,6 +12,8 @@ import { classifyTeamWorkerLaunchPolicy, extractModelProviderOverrideValue, norm
11
12
  import { sleep, sleepSync } from '../utils/sleep.js';
12
13
  import { buildPlatformCommandSpec, classifySpawnError, resolveCommandPathForPlatform, spawnPlatformCommandSync, } from '../utils/platform-command.js';
13
14
  import { resolveOmxCliEntryPath } from '../utils/paths.js';
15
+ import { readExactPaneProof, readExactPaneProofsSync, readExactPaneProofSync } from './exact-pane.js';
16
+ import { resolveCanonicalTeamStateRoot } from './state-root.js';
14
17
  const execFileAsync = promisify(execFile);
15
18
  import { HUD_RESIZE_RECONCILE_DELAY_SECONDS, HUD_TMUX_TEAM_HEIGHT_LINES } from '../hud/constants.js';
16
19
  import { OMX_TMUX_HUD_OWNER_ENV } from '../hud/reconcile.js';
@@ -18,6 +21,51 @@ import { findHudWatchPaneIds, hudPaneMatchesOwner, OMX_TMUX_HUD_LEADER_PANE_ENV
18
21
  const OMX_INSTANCE_OPTION = '@omx_instance_id';
19
22
  const OMX_PANE_INSTANCE_OPTION = '@omx_pane_instance_id';
20
23
  const OMX_TEAM_PANE_OWNER_OPTION = '@omx_team_pane_owner_id';
24
+ /** `kill(pid, 0)` establishes absence only on ESRCH. */
25
+ function probeProcessLiveness(pid) {
26
+ try {
27
+ process.kill(pid, 0);
28
+ return 'alive';
29
+ }
30
+ catch (error) {
31
+ return error.code === 'ESRCH' ? 'gone' : 'unknown';
32
+ }
33
+ }
34
+ /** Carries live tmux resources when creation cannot safely roll them back. */
35
+ export class CreateTeamSessionPartialError extends Error {
36
+ partialSession;
37
+ proofUnavailable;
38
+ originalError;
39
+ cleanupErrors;
40
+ constructor(partialSession, proofUnavailable, originalError,
41
+ /** Cleanup commands that failed after resources were created and must be retried. */
42
+ cleanupErrors = []) {
43
+ super('create_team_session_cleanup_incomplete');
44
+ this.partialSession = partialSession;
45
+ this.proofUnavailable = proofUnavailable;
46
+ this.originalError = originalError;
47
+ this.cleanupErrors = cleanupErrors;
48
+ this.name = 'CreateTeamSessionPartialError';
49
+ }
50
+ }
51
+ class ExactPaneProofUnavailableError extends Error {
52
+ proof;
53
+ constructor(proof) {
54
+ super(`exact_pane_proof_unavailable:${proof.paneId}:${proof.reason}`);
55
+ this.proof = proof;
56
+ this.name = 'ExactPaneProofUnavailableError';
57
+ }
58
+ }
59
+ class AmbiguousSplitWindowOutputError extends Error {
60
+ newlyObservedPaneIds;
61
+ reconciliationError;
62
+ constructor(newlyObservedPaneIds, reconciliationError = null) {
63
+ super('tmux_split_window_output_ambiguous');
64
+ this.newlyObservedPaneIds = newlyObservedPaneIds;
65
+ this.reconciliationError = reconciliationError;
66
+ this.name = 'AmbiguousSplitWindowOutputError';
67
+ }
68
+ }
21
69
  const INJECTION_MARKER = '[OMX_TMUX_INJECT]';
22
70
  const MODEL_INSTRUCTIONS_FILE_KEY = 'model_instructions_file';
23
71
  const OMX_BYPASS_DEFAULT_SYSTEM_PROMPT_ENV = 'OMX_BYPASS_DEFAULT_SYSTEM_PROMPT';
@@ -64,7 +112,7 @@ const TMUX_PANE_STABILITY_POLL_MS = 60;
64
112
  const TMUX_PANE_STABILITY_POLLS_REQUIRED = 2;
65
113
  const TMUX_PANE_STABILITY_TIMEOUT_MS = 750;
66
114
  const OMX_TEAM_STATE_ROOT_ENV = 'OMX_TEAM_STATE_ROOT';
67
- function runTmux(args) {
115
+ function runTmux(args, preserveStdout = false) {
68
116
  const { result } = spawnPlatformCommandSync('tmux', args, { encoding: 'utf-8' });
69
117
  if (result.error) {
70
118
  return { ok: false, stderr: result.error.message };
@@ -72,7 +120,325 @@ function runTmux(args) {
72
120
  if (result.status !== 0) {
73
121
  return { ok: false, stderr: (result.stderr || '').trim() || `tmux exited ${result.status}` };
74
122
  }
75
- return { ok: true, stdout: (result.stdout || '').trim() };
123
+ const stdout = result.stdout || '';
124
+ return { ok: true, stdout: preserveStdout ? stdout : stdout.trim() };
125
+ }
126
+ export function parseSplitWindowPaneId(stdout, expectedReceipt) {
127
+ const match = expectedReceipt === undefined
128
+ ? /^(%\d+)(?:\r?\n)?$/.exec(stdout)
129
+ : /^(%\d+)\t([^\r\n\t]+)(?:\r?\n)?$/.exec(stdout);
130
+ if (!match || (expectedReceipt !== undefined && match[2] !== expectedReceipt))
131
+ return null;
132
+ return match[1];
133
+ }
134
+ /** Preserve structured tmux fields, including an empty final field. */
135
+ /** Preserve structured tmux fields, including an empty final field. */
136
+ function runTmuxStructured(args) {
137
+ const { result } = spawnPlatformCommandSync('tmux', args, { encoding: 'utf-8' });
138
+ if (result.error)
139
+ return { ok: false, stderr: result.error.message };
140
+ if (result.status !== 0) {
141
+ return { ok: false, stderr: (result.stderr || '').trim() || `tmux exited ${result.status}` };
142
+ }
143
+ return { ok: true, stdout: (result.stdout || '').replace(/\r?\n$/, '') };
144
+ }
145
+ function captureSourcePaneAuthority(paneId, expectedTeamPaneOwnerId) {
146
+ const target = paneId.trim();
147
+ if (!/^%[0-9]+$/.test(target))
148
+ throw new Error(`invalid tmux source pane: ${paneId}`);
149
+ const result = runTmuxStructured([
150
+ 'display-message', '-p', '-t', target,
151
+ '#{session_name}\t#{session_id}\t#{session_created}\t#{window_index}\t#{window_id}\t#{pane_id}\t#{pane_pid}',
152
+ ]);
153
+ if (!result.ok)
154
+ throw new Error(`failed to capture tmux source authority: ${result.stderr}`);
155
+ const fields = result.stdout.split('\t');
156
+ if (fields.length !== 7)
157
+ throw new Error('malformed tmux source authority');
158
+ const [sessionName, sessionId, sessionCreated, windowIndex, windowId, capturedPaneId, panePid] = fields;
159
+ if (!sessionName || !/^\$[0-9]+$/.test(sessionId) || !/^[0-9]+$/.test(sessionCreated)
160
+ || !/^[0-9]+$/.test(windowIndex) || !/^@[0-9]+$/.test(windowId)
161
+ || capturedPaneId !== target || !/^[1-9][0-9]*$/.test(panePid)) {
162
+ throw new Error('malformed tmux source authority');
163
+ }
164
+ const parsedPid = Number(panePid);
165
+ if (!Number.isSafeInteger(parsedPid) || parsedPid <= 0)
166
+ throw new Error('malformed tmux source authority');
167
+ const proof = readExactPaneProofSync(target);
168
+ if (proof.status === 'unavailable')
169
+ throw new ExactPaneProofUnavailableError(proof);
170
+ if (proof.status !== 'live' || proof.pid !== parsedPid)
171
+ throw new Error(`tmux pane identity changed: ${target}`);
172
+ const ownerResult = runTmuxStructured(['show-option', '-qv', '-p', '-t', target, OMX_TEAM_PANE_OWNER_OPTION]);
173
+ if (!ownerResult.ok)
174
+ throw new Error(`failed to capture tmux pane owner: ${ownerResult.stderr}`);
175
+ const teamPaneOwnerId = ownerResult.stdout.trim() || null;
176
+ if (teamPaneOwnerId && !/^[A-Za-z0-9._:-]+$/.test(teamPaneOwnerId)) {
177
+ throw new Error('malformed tmux pane owner');
178
+ }
179
+ if (expectedTeamPaneOwnerId !== undefined && teamPaneOwnerId !== expectedTeamPaneOwnerId) {
180
+ throw new Error(`tmux pane team owner changed: ${target}`);
181
+ }
182
+ return {
183
+ paneId: target,
184
+ panePid: parsedPid,
185
+ sessionName,
186
+ sessionId,
187
+ sessionCreated,
188
+ windowId,
189
+ windowIndex,
190
+ teamPaneOwnerId,
191
+ };
192
+ }
193
+ function sourceAuthorityPredicate(source) {
194
+ return [
195
+ '#{==:#{pane_dead},0}',
196
+ `#{==:#{pane_id},${source.paneId}}`,
197
+ `#{==:#{pane_pid},${source.panePid}}`,
198
+ `#{==:#{session_id},${source.sessionId}}`,
199
+ `#{==:#{session_created},${source.sessionCreated}}`,
200
+ `#{==:#{window_id},${source.windowId}}`,
201
+ ...(source.teamPaneOwnerId ? [`#{==:#{${OMX_TEAM_PANE_OWNER_OPTION}},${source.teamPaneOwnerId}}`] : []),
202
+ ].reduce((combined, condition) => `#{&&:${combined},${condition}}`);
203
+ }
204
+ function sourceTransactionReceipt() {
205
+ return `omx_source_${process.pid}_${Date.now()}_${randomBytes(16).toString('hex')}`;
206
+ }
207
+ function bindSplitReceiptToPaneCommand(command, receipt) {
208
+ return `${command} # ${receipt}`;
209
+ }
210
+ /** Queue an effect in the source pane's tmux server only when its exact pane/session/window incarnation still matches. */
211
+ export function runSourceAuthorizedTmux(source, effect, receipt = sourceTransactionReceipt()) {
212
+ const result = runTmux([
213
+ 'if-shell', '-F', '-t', source.paneId, sourceAuthorityPredicate(source),
214
+ `${effect} ; display-message -p ${shellQuoteSingle(receipt)}`,
215
+ "display-message -p ''",
216
+ ]);
217
+ if (!result.ok)
218
+ throw new Error(`tmux source authority transaction failed: ${result.stderr}`);
219
+ if (result.stdout !== receipt)
220
+ throw new Error('tmux source authority changed before effect');
221
+ return receipt;
222
+ }
223
+ function runSourceAuthorizedSplit(source, buildEffect) {
224
+ // Reconcile against the immutable window ID, not the mutable session:index
225
+ // target: a concurrent renumber between the guarded split and the after-read
226
+ // must never turn an unrelated window's pane into kill authority.
227
+ const windowTarget = source.windowId;
228
+ const beforeTopology = listPanesResult(windowTarget);
229
+ if (beforeTopology.error) {
230
+ throw new Error(`failed to read tmux pane topology before split: ${beforeTopology.error}`);
231
+ }
232
+ const beforePaneIds = new Set(beforeTopology.panes.map((pane) => pane.paneId));
233
+ const receipt = sourceTransactionReceipt();
234
+ const effect = buildEffect(receipt);
235
+ const result = runTmux([
236
+ 'if-shell', '-F', '-t', source.paneId, sourceAuthorityPredicate(source),
237
+ effect.replace("-F '#{pane_id}'", `-F '#{pane_id}\\t${receipt}'`),
238
+ "display-message -p ''",
239
+ ], true);
240
+ if (!result.ok)
241
+ throw new Error(`tmux source authority transaction failed: ${result.stderr}`);
242
+ const paneId = parseSplitWindowPaneId(result.stdout, receipt);
243
+ if (!paneId) {
244
+ const afterTopology = listPanesResult(windowTarget);
245
+ const newlyObservedPaneIds = afterTopology.error
246
+ ? []
247
+ : afterTopology.panes
248
+ .filter((pane) => !beforePaneIds.has(pane.paneId) && pane.startCommand.includes(receipt))
249
+ .map((pane) => pane.paneId);
250
+ throw new AmbiguousSplitWindowOutputError(newlyObservedPaneIds, afterTopology.error);
251
+ }
252
+ return paneId;
253
+ }
254
+ function restoredHudCleanupDebtPath(cwd, stateRoot) {
255
+ const canonicalStateRoot = resolveCanonicalTeamStateRoot(resolve(cwd));
256
+ if (typeof stateRoot !== 'string' || !stateRoot.trim()) {
257
+ return join(canonicalStateRoot, '.restored-hud-cleanup-debt.json');
258
+ }
259
+ const canonicalTeamsRoot = join(canonicalStateRoot, 'team');
260
+ const root = resolve(stateRoot);
261
+ const relativeRoot = relative(canonicalTeamsRoot, root);
262
+ if (!isAbsolute(stateRoot.trim())
263
+ || !relativeRoot
264
+ || relativeRoot === '..'
265
+ || relativeRoot.startsWith(`..${sep}`)
266
+ || isAbsolute(relativeRoot)
267
+ || relativeRoot.split(sep).length !== 1) {
268
+ throw new Error('restored_hud_cleanup_debt_state_root_invalid');
269
+ }
270
+ for (let candidate = root;; candidate = dirname(candidate)) {
271
+ try {
272
+ const metadata = lstatSync(candidate);
273
+ if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
274
+ throw new Error('restored_hud_cleanup_debt_state_root_invalid');
275
+ }
276
+ }
277
+ catch (error) {
278
+ if (error.code !== 'ENOENT')
279
+ throw error;
280
+ }
281
+ if (candidate === canonicalTeamsRoot)
282
+ break;
283
+ }
284
+ return join(root, '.restored-hud-cleanup-debt.json');
285
+ }
286
+ function syncRestoredHudDebtParentSync(path) {
287
+ let descriptor;
288
+ try {
289
+ descriptor = openSync(dirname(path), 'r');
290
+ fsyncSync(descriptor);
291
+ }
292
+ catch (error) {
293
+ const code = error.code;
294
+ if (!(code === 'EPERM' && process.platform === 'win32'))
295
+ throw error;
296
+ }
297
+ finally {
298
+ if (descriptor !== undefined)
299
+ closeSync(descriptor);
300
+ }
301
+ }
302
+ function persistRestoredHudCleanupDebtSync(cwd, debt, stateRoot) {
303
+ const debtPath = restoredHudCleanupDebtPath(cwd, stateRoot);
304
+ mkdirSync(dirname(debtPath), { recursive: true });
305
+ // Recheck after mkdir: an untrusted Team root must still be canonical and
306
+ // non-symlinked immediately before the durable file mutation.
307
+ restoredHudCleanupDebtPath(cwd, stateRoot);
308
+ const temporaryPath = `${debtPath}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
309
+ writeFileSync(temporaryPath, `${JSON.stringify(debt)}\n`, { mode: 0o600 });
310
+ const temporaryDescriptor = openSync(temporaryPath, 'r');
311
+ try {
312
+ fsyncSync(temporaryDescriptor);
313
+ }
314
+ finally {
315
+ closeSync(temporaryDescriptor);
316
+ }
317
+ renameSync(temporaryPath, debtPath);
318
+ syncRestoredHudDebtParentSync(debtPath);
319
+ }
320
+ function parseRestoredHudCleanupDebtSync(cwd, stateRoot) {
321
+ const debtPath = restoredHudCleanupDebtPath(cwd, stateRoot);
322
+ if (!existsSync(debtPath))
323
+ return null;
324
+ let debt;
325
+ try {
326
+ debt = JSON.parse(readFileSync(debtPath, 'utf8'));
327
+ }
328
+ catch {
329
+ throw new Error('restored_hud_cleanup_debt_malformed');
330
+ }
331
+ if (debt.schema_version !== 1 || debt.operation !== 'restored_hud_cleanup'
332
+ || !hasExplicitWorkerPaneId(debt.pane_id)
333
+ || !hasExplicitWorkerPaneId(debt.leader_pane_id)
334
+ || !Number.isSafeInteger(debt.leader_pane_pid) || debt.leader_pane_pid <= 0
335
+ || !(typeof debt.leader_pane_owner_id === 'string' || debt.leader_pane_owner_id === null)
336
+ || debt.hud_owner_leader_pane_id !== debt.leader_pane_id
337
+ || (debt.pane_pid !== null && (!Number.isSafeInteger(debt.pane_pid) || debt.pane_pid <= 0))) {
338
+ throw new Error('restored_hud_cleanup_debt_malformed');
339
+ }
340
+ return { path: debtPath, debt };
341
+ }
342
+ function removeRestoredHudCleanupDebtSync(path) {
343
+ unlinkSync(path);
344
+ syncRestoredHudDebtParentSync(path);
345
+ }
346
+ /**
347
+ * Replays a prior restored-HUD obligation. A live pane is killable only when
348
+ * its recorded PID, HUD command identity, and tagged leader identity all
349
+ * still match; PID-less and ambiguous records remain durable debt.
350
+ */
351
+ export function reconcileRestoredHudCleanupDebtSync(cwd, stateRoot) {
352
+ const record = parseRestoredHudCleanupDebtSync(cwd, stateRoot);
353
+ if (!record)
354
+ return;
355
+ const { path, debt } = record;
356
+ const proof = readExactPaneProofSync(debt.pane_id);
357
+ if (proof.status === 'gone') {
358
+ removeRestoredHudCleanupDebtSync(path);
359
+ return;
360
+ }
361
+ if (proof.status !== 'live' || debt.pane_pid === null || proof.pid !== debt.pane_pid || !debt.leader_pane_owner_id) {
362
+ throw new Error(`restored_hud_cleanup_debt_unresolved:${debt.pane_id}`);
363
+ }
364
+ const leader = requireLiveTeamOwnedPaneSync(debt.leader_pane_id, debt.leader_pane_pid, debt.leader_pane_owner_id);
365
+ const topology = listPanesResult(leader);
366
+ const hudMatches = !topology.error && topology.panes.filter((pane) => pane.paneId === proof.paneId
367
+ && hudPaneMatchesOwner(pane, { leaderPaneId: debt.hud_owner_leader_pane_id })).length === 1;
368
+ if (!hudMatches)
369
+ throw new Error(`restored_hud_cleanup_debt_unresolved:${debt.pane_id}`);
370
+ killExactPaneSync(proof.paneId, debt.pane_pid, () => {
371
+ requireLiveTeamOwnedPaneSync(debt.leader_pane_id, debt.leader_pane_pid, debt.leader_pane_owner_id);
372
+ });
373
+ removeRestoredHudCleanupDebtSync(path);
374
+ }
375
+ /** Remove restored-HUD debt only after the canonical config transaction commits the same frozen identity. */
376
+ export function finalizeRestoredHudCleanupDebtSync(cwd, paneId, panePid, stateRoot, _requireFreshHudIdentity) {
377
+ const record = parseRestoredHudCleanupDebtSync(cwd, stateRoot);
378
+ if (!record)
379
+ return;
380
+ const { debt } = record;
381
+ if (debt.pane_id !== paneId || debt.pane_pid !== panePid || !debt.leader_pane_owner_id) {
382
+ throw new Error(`restored_hud_cleanup_debt_unresolved:${debt.pane_id}`);
383
+ }
384
+ const leader = requireLiveTeamOwnedPaneSync(debt.leader_pane_id, debt.leader_pane_pid, debt.leader_pane_owner_id);
385
+ const topology = listPanesResult(leader);
386
+ const hudMatches = !topology.error && topology.panes.filter((pane) => pane.paneId === debt.pane_id
387
+ && hudPaneMatchesOwner(pane, { leaderPaneId: debt.hud_owner_leader_pane_id })).length === 1;
388
+ if (!hudMatches)
389
+ throw new Error(`restored_hud_cleanup_debt_unresolved:${debt.pane_id}`);
390
+ const finalProof = readExactPaneProofSync(debt.pane_id);
391
+ if (finalProof.status !== 'live' || finalProof.pid !== debt.pane_pid) {
392
+ throw new Error(`restored_hud_cleanup_debt_unresolved:${debt.pane_id}`);
393
+ }
394
+ removeRestoredHudCleanupDebtSync(record.path);
395
+ }
396
+ /**
397
+ * Kill only a pane that a fresh global snapshot proves live. Callers treat
398
+ * gone/dead rows as already cleaned and unavailable snapshots as fail-closed.
399
+ */
400
+ function requireLiveExactPaneSync(paneId, expectedPid) {
401
+ const proof = readExactPaneProofSync(paneId);
402
+ if (proof.status === 'unavailable')
403
+ throw new ExactPaneProofUnavailableError(proof);
404
+ if (proof.status === 'gone')
405
+ throw new Error(`tmux pane is not proven live: ${paneId}`);
406
+ if (expectedPid !== undefined && proof.pid !== expectedPid) {
407
+ throw new Error(`tmux pane identity changed: ${paneId}`);
408
+ }
409
+ return proof.paneId;
410
+ }
411
+ function killExactPaneSync(paneId, expectedPid, assertAuthorization) {
412
+ const proof = readExactPaneProofSync(paneId);
413
+ if (proof.status === 'unavailable')
414
+ throw new ExactPaneProofUnavailableError(proof);
415
+ if (proof.status === 'gone')
416
+ return;
417
+ if (expectedPid !== undefined && proof.pid !== expectedPid) {
418
+ throw new Error(`tmux pane identity changed: ${paneId}`);
419
+ }
420
+ assertAuthorization?.();
421
+ // Authorization can read tmux. Re-prove immediately after it so a recycled
422
+ // pane ID cannot be targeted by the subsequent kill.
423
+ const finalProof = readExactPaneProofSync(proof.paneId);
424
+ if (finalProof.status === 'unavailable')
425
+ throw new ExactPaneProofUnavailableError(finalProof);
426
+ if (finalProof.status === 'gone')
427
+ return;
428
+ if (finalProof.pid !== proof.pid || (expectedPid !== undefined && finalProof.pid !== expectedPid)) {
429
+ throw new Error(`tmux pane identity changed: ${paneId}`);
430
+ }
431
+ const result = runTmux(['kill-pane', '-t', finalProof.paneId]);
432
+ if (!result.ok)
433
+ throw new Error(`failed to kill tmux pane ${finalProof.paneId}: ${result.stderr}`);
434
+ const afterKill = readExactPaneProofSync(finalProof.paneId);
435
+ if (afterKill.status === 'unavailable')
436
+ throw new ExactPaneProofUnavailableError(afterKill);
437
+ if (afterKill.status !== 'gone') {
438
+ if (afterKill.pid !== finalProof.pid)
439
+ throw new Error(`tmux pane identity changed: ${paneId}`);
440
+ throw new Error(`tmux pane remains live after kill: ${paneId}`);
441
+ }
76
442
  }
77
443
  function appendNoUnderlineStyleFlags(style) {
78
444
  const normalized = style
@@ -86,7 +452,8 @@ function appendNoUnderlineStyleFlags(style) {
86
452
  }
87
453
  return combined.join(',');
88
454
  }
89
- function sanitizeTmuxStyleOption(sessionTarget, optionName) {
455
+ function sanitizeTmuxStyleOption(sessionTarget, optionName, beforeEffect) {
456
+ beforeEffect?.();
90
457
  const shown = runTmux(['show-options', '-gv', '-t', sessionTarget, optionName]);
91
458
  if (!shown.ok)
92
459
  return false;
@@ -96,36 +463,28 @@ function sanitizeTmuxStyleOption(sessionTarget, optionName) {
96
463
  const sanitized = appendNoUnderlineStyleFlags(current);
97
464
  if (sanitized === current)
98
465
  return true;
466
+ beforeEffect?.();
99
467
  const result = runTmux(['set-option', '-t', sessionTarget, optionName, sanitized]);
100
468
  return result.ok;
101
469
  }
102
- function tagPaneInstance(paneTarget, instanceId) {
103
- const target = paneTarget.trim();
104
- const sanitized = instanceId.trim();
105
- if (!target || !sanitized)
106
- return;
107
- const result = runTmux(['set-option', '-p', '-t', target, OMX_PANE_INSTANCE_OPTION, sanitized]);
108
- if (!result.ok) {
109
- throw new Error(`failed to tag tmux pane ${target}: ${result.stderr}`);
110
- }
111
- }
112
- export function tagPaneTeamOwner(paneTarget, teamOwnerId) {
470
+ export function tagPaneTeamOwner(paneTarget, teamOwnerId, expectedPanePid) {
113
471
  const target = paneTarget.trim();
114
472
  const sanitized = teamOwnerId.trim();
115
473
  if (!target || !sanitized)
116
474
  return;
117
- const result = runTmux(['set-option', '-p', '-t', target, OMX_TEAM_PANE_OWNER_OPTION, sanitized]);
475
+ const provenTarget = requireLiveExactPaneSync(target, expectedPanePid);
476
+ const result = runTmux(['set-option', '-p', '-t', provenTarget, OMX_TEAM_PANE_OWNER_OPTION, sanitized]);
118
477
  if (!result.ok) {
119
- throw new Error(`failed to tag tmux pane ${target}: ${result.stderr}`);
478
+ throw new Error(`failed to tag tmux pane ${provenTarget}: ${result.stderr}`);
120
479
  }
121
480
  }
122
- export function mitigateCopyModeUnderlineArtifacts(sessionTarget) {
481
+ export function mitigateCopyModeUnderlineArtifacts(sessionTarget, beforeEffect) {
123
482
  const normalizedTarget = sessionTarget.trim();
124
483
  if (normalizedTarget === '')
125
484
  return false;
126
485
  let applied = false;
127
486
  for (const optionName of TMUX_COPY_MODE_STYLE_OPTIONS) {
128
- if (sanitizeTmuxStyleOption(normalizedTarget, optionName)) {
487
+ if (sanitizeTmuxStyleOption(normalizedTarget, optionName, beforeEffect)) {
129
488
  applied = true;
130
489
  }
131
490
  }
@@ -181,18 +540,104 @@ function baseSessionName(target) {
181
540
  return target.split(':')[0] || target;
182
541
  }
183
542
  function listPanes(target) {
184
- const result = runTmux(['list-panes', '-t', target, '-F', '#{pane_id}\t#{pane_current_command}\t#{pane_start_command}']);
543
+ return listPanesResult(target).panes;
544
+ }
545
+ function listPanesResult(target) {
546
+ const result = runTmuxStructured(['list-panes', '-t', target, '-F', '#{pane_id}\t#{pane_current_command}\t#{pane_start_command}']);
185
547
  if (!result.ok)
186
- return [];
187
- return result.stdout
188
- .split('\n')
189
- .map((line) => line.trim())
190
- .filter((line) => line.length > 0)
191
- .map((line) => {
192
- const [paneId = '', currentCommand = '', startCommand = ''] = line.split('\t');
193
- return { paneId, currentCommand, startCommand };
194
- })
195
- .filter((pane) => pane.paneId.startsWith('%'));
548
+ return { panes: [], error: result.stderr };
549
+ const panes = [];
550
+ for (const rawLine of result.stdout.split('\n')) {
551
+ const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine;
552
+ if (line.length === 0)
553
+ continue;
554
+ const firstSeparator = line.indexOf('\t');
555
+ const secondSeparator = firstSeparator >= 0 ? line.indexOf('\t', firstSeparator + 1) : -1;
556
+ const paneId = firstSeparator >= 0 ? line.slice(0, firstSeparator) : line;
557
+ if (firstSeparator < 0 || secondSeparator < 0 || !/^%[0-9]+$/.test(paneId)) {
558
+ return { panes: [], error: 'malformed pane topology' };
559
+ }
560
+ if (panes.some((pane) => pane.paneId === paneId)) {
561
+ return { panes: [], error: 'malformed pane topology' };
562
+ }
563
+ panes.push({
564
+ paneId,
565
+ currentCommand: line.slice(firstSeparator + 1, secondSeparator),
566
+ startCommand: line.slice(secondSeparator + 1),
567
+ });
568
+ }
569
+ return { panes, error: null };
570
+ }
571
+ /**
572
+ * Window-wide layout effects may only touch the exact startup pane set. A
573
+ * target-scoped topology read rejects foreign/new panes, and targeted global
574
+ * snapshots pin every surviving pane identity. The final scoped read happens
575
+ * immediately before the mutation so panes added during owner reads cannot
576
+ * inherit that authority.
577
+ */
578
+ /**
579
+ * Re-prove an explicitly Team-owned pane immediately before an effect. The
580
+ * double PID proof keeps ownership and process identity continuous across an
581
+ * untrusted tmux option read.
582
+ */
583
+ function requireLiveTeamOwnedPaneSync(paneId, expectedPanePid, expectedTeamOwnerId) {
584
+ const target = requireLiveExactPaneSync(paneId, expectedPanePid);
585
+ const owner = readPaneTeamOwnerTagResult(target);
586
+ if (owner.status !== 'value' || owner.value !== expectedTeamOwnerId) {
587
+ const detail = owner.status === 'error' ? owner.error : 'missing';
588
+ throw new Error(`tmux pane team owner changed: ${target}: ${detail}`);
589
+ }
590
+ return requireLiveExactPaneSync(target, expectedPanePid);
591
+ }
592
+ function frozenWindowTopologyError(teamTarget, expectedPaneIds, actualPaneIds) {
593
+ const expected = [...expectedPaneIds].sort();
594
+ const actual = [...actualPaneIds].sort();
595
+ const unexpected = actual.filter((paneId) => !expectedPaneIds.has(paneId));
596
+ const missing = expected.filter((paneId) => !actualPaneIds.has(paneId));
597
+ return new Error(`tmux window topology changed before layout mutation: target=${teamTarget}; expected=[${expected.join(',')}]; actual=[${actual.join(',')}]; unexpected=[${unexpected.join(',')}]; missing=[${missing.join(',')}]. Team requires an isolated tmux window with no host-owned foreign panes`);
598
+ }
599
+ function requireFrozenWindowTopologySync(teamTarget, expectedPanePids, expectedPaneOwners) {
600
+ const topology = listPanesResult(teamTarget);
601
+ if (topology.error)
602
+ throw new Error(`failed to read tmux pane topology: ${topology.error}`);
603
+ const actualPaneIds = new Set(topology.panes.map((pane) => pane.paneId));
604
+ if (actualPaneIds.size !== expectedPanePids.size
605
+ || [...expectedPanePids.keys()].some((paneId) => !actualPaneIds.has(paneId))) {
606
+ throw frozenWindowTopologyError(teamTarget, new Set(expectedPanePids.keys()), actualPaneIds);
607
+ }
608
+ for (const [paneId, expectedPanePid] of expectedPanePids) {
609
+ const expectedOwner = expectedPaneOwners?.get(paneId);
610
+ if (expectedOwner)
611
+ requireLiveTeamOwnedPaneSync(paneId, expectedPanePid, expectedOwner);
612
+ else {
613
+ const proof = readExactPaneProofSync(paneId);
614
+ if (proof.status === 'unavailable')
615
+ throw new ExactPaneProofUnavailableError(proof);
616
+ if (proof.status === 'gone')
617
+ throw new Error(`tmux pane is not proven live: ${proof.paneId}`);
618
+ if (proof.pid !== expectedPanePid)
619
+ throw new Error(`tmux pane identity changed: ${proof.paneId}`);
620
+ }
621
+ }
622
+ const finalProofs = readExactPaneProofsSync([...expectedPanePids.keys()]);
623
+ for (const [index, [paneId, expectedPanePid]] of [...expectedPanePids.entries()].entries()) {
624
+ const proof = finalProofs[index];
625
+ if (proof.status === 'unavailable')
626
+ throw new ExactPaneProofUnavailableError(proof);
627
+ if (proof.status === 'gone')
628
+ throw new Error(`tmux pane is not proven live: ${proof.paneId}`);
629
+ if (proof.paneId !== paneId || proof.pid !== expectedPanePid) {
630
+ throw new Error(`tmux pane identity changed: ${paneId}`);
631
+ }
632
+ }
633
+ const finalTopology = listPanesResult(teamTarget);
634
+ if (finalTopology.error)
635
+ throw new Error(`failed to read tmux pane topology: ${finalTopology.error}`);
636
+ const finalPaneIds = new Set(finalTopology.panes.map((pane) => pane.paneId));
637
+ if (finalPaneIds.size !== expectedPanePids.size
638
+ || [...expectedPanePids.keys()].some((paneId) => !finalPaneIds.has(paneId))) {
639
+ throw frozenWindowTopologyError(teamTarget, new Set(expectedPanePids.keys()), finalPaneIds);
640
+ }
196
641
  }
197
642
  export function listPaneIds(target) {
198
643
  return listPanes(target).map((pane) => pane.paneId);
@@ -237,12 +682,6 @@ export function chooseTeamLeaderPaneId(panes, preferredPaneId) {
237
682
  return nonHud.paneId;
238
683
  return preferredPaneId;
239
684
  }
240
- function findHudPaneIds(target, leaderPaneId) {
241
- return findHudWatchPaneIds(listPanes(target), leaderPaneId, { leaderPaneId });
242
- }
243
- function findOwnedHudPaneIds(target, leaderPaneId) {
244
- return findHudWatchPaneIds(listPanes(target), leaderPaneId, { leaderPaneId });
245
- }
246
685
  function readPaneCurrentPath(paneId) {
247
686
  if (!paneId.startsWith('%'))
248
687
  return null;
@@ -281,7 +720,8 @@ function shouldAttemptRestoreCwdCandidate(candidate) {
281
720
  }
282
721
  return pathIsUsableDirectory(candidate.rawPath);
283
722
  }
284
- function resolveStandaloneHudRestoreCwdCandidates(leaderPaneId, fallbackCwd, explicitCwd) {
723
+ function resolveStandaloneHudRestoreCwdCandidates(leaderPaneId, fallbackCwd, explicitCwd, beforeReadLiveLeaderCwd) {
724
+ beforeReadLiveLeaderCwd?.();
285
725
  const liveLeaderCwd = readPaneCurrentPath(leaderPaneId);
286
726
  return uniqueRestoreCwdCandidates([
287
727
  { source: 'explicit', rawPath: explicitCwd },
@@ -318,105 +758,214 @@ async function runTmuxAsync(args) {
318
758
  return { ok: false, stderr: (err.stderr || err.message || '').trim() || 'tmux command failed' };
319
759
  }
320
760
  }
321
- async function sendKeyAsync(target, key) {
761
+ function hasExplicitWorkerPaneId(workerPaneId) {
762
+ return typeof workerPaneId === 'string' && workerPaneId.trim().length > 0;
763
+ }
764
+ function readExactWorkerPaneLivenessProofSync(paneId) {
765
+ if (!/^%[0-9]+$/.test(paneId))
766
+ return { status: 'unavailable', paneId, reason: 'invalid_pane_id' };
767
+ const result = runTmuxStructured([
768
+ 'display-message', '-p', '-t', paneId,
769
+ `#{pane_id}\t#{pane_dead}\t#{pane_pid}\t#{${OMX_TEAM_PANE_OWNER_OPTION}}`,
770
+ ]);
771
+ if (!result.ok) {
772
+ if (/can't find pane|no such pane|unknown pane/i.test(result.stderr)) {
773
+ return { status: 'gone', paneId, reason: 'absent' };
774
+ }
775
+ return { status: 'unavailable', paneId, reason: `query_failed:${result.stderr || 'unknown'}` };
776
+ }
777
+ const lines = result.stdout.replace(/\r\n/g, '\n').split('\n').filter((line) => line.length > 0);
778
+ if (lines.length !== 1)
779
+ return { status: 'unavailable', paneId, reason: 'malformed_exact_pane_result' };
780
+ const fields = lines[0].split('\t');
781
+ if (fields.length !== 4)
782
+ return { status: 'unavailable', paneId, reason: 'malformed_exact_pane_result' };
783
+ const [actualPaneId, paneDead, rawPid, ownerId] = fields;
784
+ if (actualPaneId !== paneId) {
785
+ return { status: 'unavailable', paneId, reason: `pane_id_changed:expected=${paneId}:actual=${actualPaneId}` };
786
+ }
787
+ if (paneDead !== '0' && paneDead !== '1')
788
+ return { status: 'unavailable', paneId, reason: 'malformed_exact_pane_result' };
789
+ if (!/^[0-9]+$/.test(rawPid))
790
+ return { status: 'unavailable', paneId, reason: 'malformed_exact_pane_result' };
791
+ const pid = Number(rawPid);
792
+ if (!Number.isSafeInteger(pid) || pid <= 0)
793
+ return { status: 'unavailable', paneId, reason: 'malformed_exact_pane_result' };
794
+ if (paneDead === '1')
795
+ return { status: 'gone', paneId, reason: 'dead' };
796
+ return { status: 'live', paneId, pid, ownerId };
797
+ }
798
+ function requireExactWorkerPaneLivenessIdentity(paneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
799
+ if (hudPaneId?.trim() && paneId === hudPaneId.trim()) {
800
+ throw new Error(`tmux worker pane is HUD target: ${paneId}`);
801
+ }
802
+ const proof = readExactWorkerPaneLivenessProofSync(paneId);
803
+ if (proof.status === 'unavailable' && proof.reason.startsWith('pane_id_changed:')) {
804
+ throw new Error(`tmux worker pane identity changed: ${paneId}: ${proof.reason}`);
805
+ }
806
+ if (proof.status !== 'live')
807
+ return proof;
808
+ if (typeof expectedPanePid === 'number' && proof.pid !== expectedPanePid) {
809
+ throw new Error(`tmux worker pane PID changed: ${paneId}: expected ${expectedPanePid}, got ${proof.pid}`);
810
+ }
811
+ const expectedOwner = typeof expectedTeamOwnerId === 'string' ? expectedTeamOwnerId.trim() : '';
812
+ if (expectedOwner && proof.ownerId !== expectedOwner) {
813
+ throw new Error(`tmux worker pane incarnation changed: ${paneId}: expected ${expectedOwner}, got ${proof.ownerId || 'missing'}`);
814
+ }
815
+ return proof;
816
+ }
817
+ function resolveWorkerPaneTargetSync(sessionName, workerIndex, workerPaneId) {
818
+ if (!hasExplicitWorkerPaneId(workerPaneId))
819
+ return paneTarget(sessionName, workerIndex);
820
+ const proof = readExactPaneProofSync(workerPaneId);
821
+ return proof.status === 'live' ? proof.paneId : null;
822
+ }
823
+ async function resolveWorkerPaneTargetAsync(sessionName, workerIndex, workerPaneId) {
824
+ if (!hasExplicitWorkerPaneId(workerPaneId))
825
+ return paneTarget(sessionName, workerIndex);
826
+ const proof = await readExactPaneProof(workerPaneId);
827
+ return proof.status === 'live' ? proof.paneId : null;
828
+ }
829
+ function createPinnedWorkerPaneTargetResolverSync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
830
+ if (!hasExplicitWorkerPaneId(workerPaneId)) {
831
+ return () => resolveWorkerPaneTargetSync(sessionName, workerIndex, workerPaneId);
832
+ }
833
+ const pinnedPid = expectedPanePid;
834
+ const expectedOwner = typeof expectedTeamOwnerId === 'string' ? expectedTeamOwnerId.trim() : '';
835
+ const canonicalHudPaneId = typeof hudPaneId === 'string' ? hudPaneId.trim() : '';
836
+ return () => {
837
+ if (typeof pinnedPid !== 'number' || !Number.isSafeInteger(pinnedPid) || pinnedPid <= 0)
838
+ return null;
839
+ if (!expectedOwner)
840
+ return null;
841
+ if (canonicalHudPaneId && workerPaneId === canonicalHudPaneId) {
842
+ throw new Error(`tmux worker pane is HUD target: ${workerPaneId}`);
843
+ }
844
+ const proof = readExactPaneProofSync(workerPaneId);
845
+ if (proof.status !== 'live')
846
+ return null;
847
+ if (proof.pid !== pinnedPid) {
848
+ throw new Error(`tmux pane identity changed: ${workerPaneId}`);
849
+ }
850
+ const owner = readPaneTeamOwnerTagResult(proof.paneId);
851
+ if (owner.status !== 'value' || owner.value !== expectedOwner)
852
+ return null;
853
+ const finalProof = readExactPaneProofSync(workerPaneId);
854
+ if (finalProof.status !== 'live')
855
+ return null;
856
+ if (finalProof.pid !== pinnedPid) {
857
+ throw new Error(`tmux pane identity changed: ${workerPaneId}`);
858
+ }
859
+ return finalProof.paneId;
860
+ };
861
+ }
862
+ function createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
863
+ if (!hasExplicitWorkerPaneId(workerPaneId)) {
864
+ return () => resolveWorkerPaneTargetAsync(sessionName, workerIndex, workerPaneId);
865
+ }
866
+ const pinnedPid = expectedPanePid;
867
+ const expectedOwner = typeof expectedTeamOwnerId === 'string' ? expectedTeamOwnerId.trim() : '';
868
+ const canonicalHudPaneId = typeof hudPaneId === 'string' ? hudPaneId.trim() : '';
869
+ let lastResolvedPid;
870
+ const resolveTarget = async () => {
871
+ if (typeof pinnedPid !== 'number' || !Number.isSafeInteger(pinnedPid) || pinnedPid <= 0)
872
+ return null;
873
+ if (!expectedOwner)
874
+ return null;
875
+ if (canonicalHudPaneId && workerPaneId === canonicalHudPaneId) {
876
+ throw new Error(`tmux worker pane is HUD target: ${workerPaneId}`);
877
+ }
878
+ const proof = await readExactPaneProof(workerPaneId);
879
+ if (proof.status !== 'live')
880
+ return null;
881
+ if (proof.pid !== pinnedPid) {
882
+ throw new Error(`tmux pane identity changed: ${workerPaneId}`);
883
+ }
884
+ const owner = readPaneTeamOwnerTagResult(proof.paneId);
885
+ if (owner.status !== 'value' || owner.value !== expectedOwner) {
886
+ const detail = owner.status === 'error' ? owner.error : 'missing';
887
+ throw new Error(`tmux pane team owner changed: ${workerPaneId}: ${detail}`);
888
+ }
889
+ // The option read is untrusted and can race pane ID reuse. Its immediately
890
+ // adjacent PID proof authorizes the following capture/control/input effect.
891
+ const finalProof = await readExactPaneProof(workerPaneId);
892
+ if (finalProof.status !== 'live')
893
+ return null;
894
+ if (finalProof.pid !== pinnedPid) {
895
+ throw new Error(`tmux pane identity changed: ${workerPaneId}`);
896
+ }
897
+ lastResolvedPid = finalProof.pid;
898
+ return finalProof.paneId;
899
+ };
900
+ resolveTarget.lastResolvedPid = () => lastResolvedPid;
901
+ return resolveTarget;
902
+ }
903
+ async function requireAsyncPaneTarget(resolveTarget) {
904
+ const target = await resolveTarget();
905
+ if (!target)
906
+ throw new Error('tmux pane is not proven live');
907
+ return target;
908
+ }
909
+ async function sendKeyAsync(resolveTarget, key) {
910
+ const target = await requireAsyncPaneTarget(resolveTarget);
322
911
  const result = await runTmuxAsync(['send-keys', '-t', target, key]);
323
912
  if (!result.ok) {
324
913
  throw new Error(`sendKeyAsync: failed to send ${key}: ${result.stderr}`);
325
914
  }
326
915
  }
327
- async function capturePaneAsync(target) {
916
+ async function capturePaneAsync(resolveTarget) {
917
+ const target = await resolveTarget();
918
+ if (!target)
919
+ return '';
328
920
  const result = await runTmuxAsync(sharedBuildCapturePaneArgv(target, 80));
329
921
  if (!result.ok)
330
922
  return '';
331
923
  return result.stdout;
332
924
  }
333
- async function captureVisiblePaneAsync(target) {
925
+ /** Capture an explicit Team worker pane only after PID-owner-PID authorization. */
926
+ export async function captureWorkerPane(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
927
+ const resolveTarget = createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
928
+ return capturePaneAsync(resolveTarget);
929
+ }
930
+ async function captureVisiblePaneAsync(resolveTarget) {
931
+ const target = await resolveTarget();
932
+ if (!target)
933
+ return '';
334
934
  const result = await runTmuxAsync(sharedBuildVisibleCapturePaneArgv(target));
335
935
  if (!result.ok)
336
936
  return '';
337
937
  return result.stdout;
338
938
  }
339
- async function isWorkerAliveAsync(sessionName, workerIndex, workerPaneId) {
340
- if (workerPaneId?.startsWith('%')) {
341
- const paneStatus = await readPaneLivenessByIdAsync(workerPaneId);
342
- if (paneStatus !== null)
343
- return paneStatus;
939
+ async function isWorkerAliveAsync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId, resolveTarget) {
940
+ if (hasExplicitWorkerPaneId(workerPaneId)) {
941
+ const resolver = resolveTarget ?? createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
942
+ const target = await resolver();
943
+ const pid = resolver.lastResolvedPid?.() ?? expectedPanePid;
944
+ if (!target || typeof pid !== 'number' || !Number.isSafeInteger(pid) || pid <= 0)
945
+ return 'unknown';
946
+ return probeProcessLiveness(pid);
344
947
  }
345
948
  const result = await runTmuxAsync([
346
949
  'list-panes',
347
- '-t', paneTarget(sessionName, workerIndex, workerPaneId),
950
+ '-t', paneTarget(sessionName, workerIndex),
348
951
  '-F',
349
952
  '#{pane_dead} #{pane_pid}',
350
953
  ]);
351
954
  if (!result.ok)
352
- return false;
955
+ return 'unknown';
353
956
  const line = result.stdout.split('\n')[0]?.trim();
354
957
  if (!line)
355
- return false;
958
+ return 'unknown';
356
959
  const parts = line.split(/\s+/);
357
960
  if (parts.length < 2)
358
- return false;
961
+ return 'unknown';
359
962
  const paneDead = parts[0];
360
963
  const pid = Number.parseInt(parts[1], 10);
361
964
  if (paneDead === '1')
362
- return false;
965
+ return 'gone';
363
966
  if (!Number.isFinite(pid))
364
- return false;
365
- try {
366
- process.kill(pid, 0);
367
- return true;
368
- }
369
- catch {
370
- return false;
371
- }
372
- }
373
- function parsePaneLivenessLine(line, allowMissingPid) {
374
- const parts = line.trim().split(/\s+/);
375
- if (parts.length < 1 || parts[0] === '')
376
- return null;
377
- const paneDead = parts[0];
378
- const pid = Number.parseInt(parts[1] ?? '', 10);
379
- if (paneDead === '1')
380
- return false;
381
- if (!Number.isFinite(pid))
382
- return allowMissingPid ? true : false;
383
- try {
384
- process.kill(pid, 0);
385
- return true;
386
- }
387
- catch {
388
- return allowMissingPid;
389
- }
390
- }
391
- function readPaneLivenessById(paneId) {
392
- const result = runTmux(['list-panes', '-a', '-F', '#{pane_id} #{pane_dead} #{pane_pid}']);
393
- if (!result.ok)
394
- return null;
395
- for (const line of result.stdout.split('\n')) {
396
- const trimmed = line.trim();
397
- if (!trimmed)
398
- continue;
399
- const [id = '', ...rest] = trimmed.split(/\s+/);
400
- if (id !== paneId)
401
- continue;
402
- return parsePaneLivenessLine(rest.join(' '), true);
403
- }
404
- return null;
405
- }
406
- async function readPaneLivenessByIdAsync(paneId) {
407
- const result = await runTmuxAsync(['list-panes', '-a', '-F', '#{pane_id} #{pane_dead} #{pane_pid}']);
408
- if (!result.ok)
409
- return null;
410
- for (const line of result.stdout.split('\n')) {
411
- const trimmed = line.trim();
412
- if (!trimmed)
413
- continue;
414
- const [id = '', ...rest] = trimmed.split(/\s+/);
415
- if (id !== paneId)
416
- continue;
417
- return parsePaneLivenessLine(rest.join(' '), true);
418
- }
419
- return null;
967
+ return 'unknown';
968
+ return probeProcessLiveness(pid);
420
969
  }
421
970
  function shellQuoteSingle(value) {
422
971
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -510,6 +1059,20 @@ function buildNestedTmuxShellCommand(command) {
510
1059
  function buildBestEffortShellCommand(command) {
511
1060
  return `${command} >/dev/null 2>&1 || true`;
512
1061
  }
1062
+ function buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES, expectedPanePid, expectedPaneOwnerId, effectCommand = buildNestedTmuxShellCommand(buildHudResizeCommand(buildHudPaneTarget(hudPaneId), heightLines))) {
1063
+ const target = buildHudPaneTarget(hudPaneId);
1064
+ const snapshot = buildNestedTmuxShellCommand("list-panes -a -F '#{pane_id}\t#{pane_dead}\t#{pane_pid}'");
1065
+ const expectedPid = typeof expectedPanePid === 'number' && Number.isSafeInteger(expectedPanePid) && expectedPanePid > 0
1066
+ ? ` && $3 == "${expectedPanePid}"`
1067
+ : '';
1068
+ const proof = `awk -F '\\t' -v pane='${target}' 'NF != 3 || $1 !~ /^%[0-9]+$/ || ($2 != "0" && $2 != "1") || (seen[$1]++) || ($1 == pane && ($3 !~ /^[1-9][0-9]*$/ || length($3) > 16 || (length($3) == 16 && ("x" $3) > "x9007199254740991"))) || ($1 != pane && $3 != "" && ($3 !~ /^[1-9][0-9]*$/ || length($3) > 16 || (length($3) == 16 && ("x" $3) > "x9007199254740991"))) { bad = 1 } $1 == pane && $2 == "0"${expectedPid} { live = 1 } END { exit bad || !live }'`;
1069
+ const expectedOwner = expectedPaneOwnerId?.trim();
1070
+ if (!expectedOwner) {
1071
+ return `if snapshot=$(${snapshot}); then printf '%s\\n' "$snapshot" | ${proof} && ${effectCommand}; fi`;
1072
+ }
1073
+ const owner = buildNestedTmuxShellCommand(`show-option -qv -p -t ${target} ${OMX_TEAM_PANE_OWNER_OPTION}`);
1074
+ return `if snapshot=$(${snapshot}); then printf '%s\\n' "$snapshot" | ${proof} && owner=$(${owner}) && [ "$owner" = ${shellQuoteSingle(expectedOwner)} ] && final_snapshot=$(${snapshot}) && printf '%s\\n' "$final_snapshot" | ${proof} && ${effectCommand}; fi`;
1075
+ }
513
1076
  /** Upper bound for tmux hook indices (signed 32-bit max). */
514
1077
  const TMUX_HOOK_INDEX_MAX = 2147483647;
515
1078
  function buildResizeHookSlot(hookName) {
@@ -526,8 +1089,8 @@ function buildClientAttachedHookSlot(hookName) {
526
1089
  }
527
1090
  return `client-attached[${Math.abs(hash) % TMUX_HOOK_INDEX_MAX}]`;
528
1091
  }
529
- export function buildRegisterResizeHookArgs(hookTarget, hookName, hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES) {
530
- const resizeCommand = buildBestEffortShellCommand(buildNestedTmuxShellCommand(buildHudResizeCommand(hudPaneId, heightLines)));
1092
+ export function buildRegisterResizeHookArgs(hookTarget, hookName, hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES, expectedPanePid, expectedPaneOwnerId) {
1093
+ const resizeCommand = buildBestEffortShellCommand(buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines, expectedPanePid, expectedPaneOwnerId));
531
1094
  const hookCommand = shellQuoteSingle(`${resizeCommand}; sleep ${HUD_RESIZE_RECONCILE_DELAY_SECONDS}; ${resizeCommand}`);
532
1095
  return ['set-hook', '-t', hookTarget, buildResizeHookSlot(hookName), `run-shell -b ${hookCommand}`];
533
1096
  }
@@ -543,9 +1106,11 @@ export function buildClientAttachedReconcileHookName(teamName, sessionName, wind
543
1106
  normalizeHudPaneToken(hudPaneId),
544
1107
  ].join('_');
545
1108
  }
546
- export function buildRegisterClientAttachedReconcileArgs(hookTarget, hookName, hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES) {
1109
+ export function buildRegisterClientAttachedReconcileArgs(hookTarget, hookName, hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES, expectedPanePid, expectedPaneOwnerId) {
547
1110
  const hookSlot = buildClientAttachedHookSlot(hookName);
548
- const oneShotCommand = shellQuoteSingle(`${buildBestEffortShellCommand(buildNestedTmuxShellCommand(buildHudResizeCommand(hudPaneId, heightLines)))}; ${buildNestedTmuxShellCommand(`set-hook -u -t ${hookTarget} ${hookSlot}`)}`);
1111
+ const resizeCommand = buildBestEffortShellCommand(buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines, expectedPanePid, expectedPaneOwnerId));
1112
+ const unregisterCommand = buildBestEffortShellCommand(buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines, expectedPanePid, expectedPaneOwnerId, buildNestedTmuxShellCommand(`set-hook -u -t ${hookTarget} ${hookSlot}`)));
1113
+ const oneShotCommand = shellQuoteSingle(`${resizeCommand}; ${unregisterCommand}`);
549
1114
  return ['set-hook', '-t', hookTarget, hookSlot, `run-shell -b ${oneShotCommand}`];
550
1115
  }
551
1116
  export function buildUnregisterClientAttachedReconcileArgs(hookTarget, hookName) {
@@ -555,18 +1120,18 @@ export function unregisterResizeHook(hookTarget, hookName) {
555
1120
  const result = runTmux(buildUnregisterResizeHookArgs(hookTarget, hookName));
556
1121
  return result.ok;
557
1122
  }
558
- export function buildScheduleDelayedHudResizeArgs(hudPaneId, delaySeconds = HUD_RESIZE_RECONCILE_DELAY_SECONDS, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES) {
1123
+ export function buildScheduleDelayedHudResizeArgs(hudPaneId, delaySeconds = HUD_RESIZE_RECONCILE_DELAY_SECONDS, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES, expectedPanePid, expectedPaneOwnerId) {
559
1124
  const delay = Number.isFinite(delaySeconds) && delaySeconds > 0 ? delaySeconds : HUD_RESIZE_RECONCILE_DELAY_SECONDS;
560
- return ['run-shell', '-b', `sleep ${delay}; ${buildBestEffortShellCommand(buildNestedTmuxShellCommand(buildHudResizeCommand(hudPaneId, heightLines)))}`];
1125
+ return ['run-shell', '-b', `sleep ${delay}; ${buildBestEffortShellCommand(buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines, expectedPanePid, expectedPaneOwnerId))}`];
561
1126
  }
562
- export function buildReconcileHudResizeArgs(hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES) {
563
- return ['run-shell', buildBestEffortShellCommand(buildNestedTmuxShellCommand(buildHudResizeCommand(hudPaneId, heightLines)))];
1127
+ export function buildReconcileHudResizeArgs(hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES, expectedPanePid, expectedPaneOwnerId) {
1128
+ return ['run-shell', buildBestEffortShellCommand(buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines, expectedPanePid, expectedPaneOwnerId))];
564
1129
  }
565
- function redrawLeaderPaneAfterTeamLayout(leaderPaneId) {
566
- const target = leaderPaneId.trim();
567
- if (!target.startsWith('%'))
568
- return;
569
- runTmux(['send-keys', '-t', target, 'C-l']);
1130
+ function redrawLeaderPaneAfterTeamLayout(source, expectedTeamOwnerId) {
1131
+ const provenTarget = requireLiveTeamOwnedPaneSync(source.paneId, source.panePid, expectedTeamOwnerId);
1132
+ if (provenTarget !== source.paneId)
1133
+ throw new Error(`tmux pane identity changed: ${source.paneId}`);
1134
+ runSourceAuthorizedTmux(source, `send-keys -t ${source.paneId} C-l`);
570
1135
  }
571
1136
  const ZSH_CANDIDATE_PATHS = ['/bin/zsh', '/usr/bin/zsh', '/usr/local/bin/zsh', '/opt/local/bin/zsh', '/opt/homebrew/bin/zsh'];
572
1137
  const BASH_CANDIDATE_PATHS = ['/bin/bash', '/usr/bin/bash'];
@@ -923,6 +1488,23 @@ function appendTeamWorkerMcpDisableOverrides(args, env) {
923
1488
  args.splice(endOfOptionsIndex < 0 ? args.length : endOfOptionsIndex, 0, CONFIG_FLAG, `${key}=false`);
924
1489
  }
925
1490
  }
1491
+ const CODEX_BYPASS_MDM_INCOMPATIBLE_REASON = 'codex_bypass_mdm_incompatible';
1492
+ const CODEX_BYPASS_MDM_REJECTED_MARKER = 'MDM_POLICY_REJECTED: approval_policy=never forbids --dangerously-bypass-approvals-and-sandbox';
1493
+ export function classifyCodexBypassMdmIncompatibility(captured, workerCli, finalArgs) {
1494
+ if (workerCli !== 'codex' || !finalArgs || !parseTeamWorkerLaunchArgs([...finalArgs], 'final tmux worker arguments', { directPolicyMode: 'ignore' }).wantsBypass)
1495
+ return null;
1496
+ const normalizedLines = captured.replace(/\r\n?/g, '\n').split('\n');
1497
+ return normalizedLines.includes(CODEX_BYPASS_MDM_REJECTED_MARKER)
1498
+ ? CODEX_BYPASS_MDM_INCOMPATIBLE_REASON
1499
+ : null;
1500
+ }
1501
+ function buildAttachedTmuxWorkerStartupPlan(teamName, workerIndex, launchArgs, cwd, extraEnv, workerCliOverride, initialPrompt, workerRole) {
1502
+ const processSpec = buildWorkerStartupProcessLaunchSpec(teamName, workerIndex, launchArgs, cwd, extraEnv, workerCliOverride, initialPrompt, workerRole);
1503
+ const startupArgs = [...processSpec.args];
1504
+ if (processSpec.workerCli === 'codex')
1505
+ appendTeamWorkerMcpDisableOverrides(startupArgs, { ...process.env, ...extraEnv });
1506
+ return Object.freeze({ processSpec, startupArgs: Object.freeze(startupArgs) });
1507
+ }
926
1508
  function insertArgsBeforeEndOfOptions(args, insertedArgs) {
927
1509
  const endOfOptionsIndex = args.indexOf('--');
928
1510
  if (endOfOptionsIndex < 0)
@@ -1046,24 +1628,17 @@ function buildWorkerStartupScriptContent(processSpec, startupEnv, startupArgs, c
1046
1628
  '',
1047
1629
  ].filter((line) => line !== '').join('\n');
1048
1630
  }
1049
- export function writeWorkerStartupScriptCommand(teamName, workerIndex, launchArgs = [], cwd = process.cwd(), extraEnv = {}, workerCliOverride, initialPrompt, workerRole) {
1631
+ export function writeWorkerStartupScriptCommand(teamName, workerIndex, launchArgs = [], cwd = process.cwd(), extraEnv = {}, workerCliOverride, initialPrompt, workerRole, attachedPlan) {
1050
1632
  if (process.platform === 'win32' && !isMsysOrGitBash())
1051
1633
  return null;
1052
1634
  const stateRoot = extraEnv[OMX_TEAM_STATE_ROOT_ENV]?.trim();
1053
1635
  if (!stateRoot)
1054
1636
  return null;
1055
- const processSpec = buildWorkerStartupProcessLaunchSpec(teamName, workerIndex, launchArgs, cwd, extraEnv, workerCliOverride, initialPrompt, workerRole);
1056
- const startupEnv = {
1057
- ...readTmuxWorkerAmbientEnv(process.env),
1058
- ...processSpec.env,
1059
- };
1060
- const startupArgs = [...processSpec.args];
1061
- if (processSpec.workerCli === 'codex') {
1062
- appendTeamWorkerMcpDisableOverrides(startupArgs, { ...process.env, ...extraEnv });
1063
- }
1637
+ const startupPlan = attachedPlan ?? buildAttachedTmuxWorkerStartupPlan(teamName, workerIndex, launchArgs, cwd, extraEnv, workerCliOverride, initialPrompt, workerRole);
1638
+ const startupEnv = { ...readTmuxWorkerAmbientEnv(process.env), ...startupPlan.processSpec.env };
1064
1639
  const scriptPath = join(stateRoot, 'team', teamName, 'runtime', `worker-${workerIndex}-startup.sh`);
1065
1640
  mkdirSync(dirname(scriptPath), { recursive: true });
1066
- writeFileSync(scriptPath, buildWorkerStartupScriptContent(processSpec, startupEnv, startupArgs, cwd, extraEnv), 'utf-8');
1641
+ writeFileSync(scriptPath, buildWorkerStartupScriptContent(startupPlan.processSpec, startupEnv, [...startupPlan.startupArgs], cwd, extraEnv), 'utf-8');
1067
1642
  chmodSync(scriptPath, 0o700);
1068
1643
  return `exec /bin/sh ${shellQuoteSingle(translatePathForMsys(scriptPath))}`;
1069
1644
  }
@@ -1217,7 +1792,20 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
1217
1792
  const safeTeamName = sanitizeTeamName(teamName);
1218
1793
  let registeredResizeHook = null;
1219
1794
  let registeredClientAttachedHook = null;
1220
- const rollbackPaneIds = [];
1795
+ const rollbackPanes = new Map();
1796
+ // An owner-tag command may have succeeded even if tmux reports an ambiguous
1797
+ // failure; rollback can only kill panes whose owner is re-authorized.
1798
+ const rollbackTaggedPaneOwnerIds = new Map();
1799
+ let partialTeamTarget = null;
1800
+ let partialLeaderPaneId = null;
1801
+ let partialLeaderPanePid;
1802
+ let partialTeamPaneOwnerId = '';
1803
+ const partialWorkerPaneIds = [];
1804
+ const partialWorkerPaneIdsByIndex = Array.from({ length: workerCount }, () => null);
1805
+ const partialWorkerPanePidsByIndex = Array.from({ length: workerCount }, () => null);
1806
+ const partialStartupCleanupPanePids = new Map();
1807
+ let partialHudPaneId = null;
1808
+ let partialHudPanePid = null;
1221
1809
  try {
1222
1810
  const tmuxPaneTarget = process.env.TMUX_PANE;
1223
1811
  const displayArgs = tmuxPaneTarget
@@ -1234,82 +1822,185 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
1234
1822
  throw new Error(`failed to parse current tmux target: ${context.stdout}`);
1235
1823
  }
1236
1824
  const teamTarget = `${sessionName}:${windowIndex}`;
1825
+ partialTeamTarget = teamTarget;
1237
1826
  const ownerSessionId = (options.ownerSessionId ?? process.env.OMX_SESSION_ID ?? '').trim();
1238
1827
  const teamPaneOwnerId = (options.teamPaneOwnerId ?? `team:${safeTeamName}`).trim();
1239
- if (ownerSessionId) {
1240
- const tagResult = runTmux(['set-option', '-t', sessionName, OMX_INSTANCE_OPTION, ownerSessionId]);
1241
- if (!tagResult.ok) {
1242
- throw new Error(`failed to tag tmux session ${sessionName}: ${tagResult.stderr}`);
1243
- }
1828
+ partialTeamPaneOwnerId = teamPaneOwnerId;
1829
+ const paneListResult = listPanesResult(teamTarget);
1830
+ if (paneListResult.error)
1831
+ throw new Error(`failed to read tmux pane topology: ${paneListResult.error}`);
1832
+ const leaderPaneId = chooseTeamLeaderPaneId(paneListResult.panes, detectedLeaderPaneId);
1833
+ const leaderProof = readExactPaneProofSync(leaderPaneId);
1834
+ if (leaderProof.status === 'unavailable')
1835
+ throw new ExactPaneProofUnavailableError(leaderProof);
1836
+ if (leaderProof.status === 'gone')
1837
+ throw new Error(`tmux pane is not proven live: ${leaderPaneId}`);
1838
+ let leaderSource = captureSourcePaneAuthority(leaderPaneId);
1839
+ if (leaderSource.sessionName !== sessionName || leaderSource.windowIndex !== windowIndex || leaderSource.panePid !== leaderProof.pid) {
1840
+ throw new Error('tmux source authority changed during team startup');
1244
1841
  }
1245
- const panes = listPanes(teamTarget);
1246
- const leaderPaneId = chooseTeamLeaderPaneId(panes, detectedLeaderPaneId);
1247
- tagPaneInstance(leaderPaneId, ownerSessionId);
1248
- tagPaneTeamOwner(leaderPaneId, teamPaneOwnerId);
1249
- const initialHudPaneIds = findHudPaneIds(teamTarget, leaderPaneId);
1842
+ const leaderPanePid = leaderSource.panePid;
1843
+ partialLeaderPaneId = leaderPaneId;
1844
+ partialLeaderPanePid = leaderPanePid;
1845
+ const initialHudPaneIds = findHudWatchPaneIds(paneListResult.panes, leaderPaneId, { leaderPaneId });
1846
+ const initialWindowPanePids = new Map([[leaderPaneId, leaderPanePid]]);
1250
1847
  const omxEntry = resolveOmxCliEntryPath();
1251
1848
  const canRecreateTeamHud = Boolean(omxEntry && omxEntry.trim() !== '');
1849
+ // Freeze every HUD owned by this leader even when startup cannot recreate
1850
+ // it; the untouched pane remains part of the authorized topology.
1851
+ for (const hudPaneId of initialHudPaneIds) {
1852
+ const proof = readExactPaneProofSync(hudPaneId);
1853
+ if (proof.status === 'unavailable')
1854
+ throw new ExactPaneProofUnavailableError(proof);
1855
+ if (proof.status === 'gone')
1856
+ throw new Error(`tmux pane is not proven live: ${hudPaneId}`);
1857
+ initialWindowPanePids.set(hudPaneId, proof.pid);
1858
+ }
1252
1859
  // Team mode prioritizes leader + worker visibility. Remove HUD panes only
1253
1860
  // when we can recreate the team HUD. Otherwise keep the existing HUD alive
1254
1861
  // instead of making it disappear on team startup failures or broken installs.
1862
+ requireFrozenWindowTopologySync(teamTarget, initialWindowPanePids);
1863
+ if (ownerSessionId) {
1864
+ runSourceAuthorizedTmux(leaderSource, `set-option -t ${leaderSource.sessionId} ${OMX_INSTANCE_OPTION} ${shellQuoteSingle(ownerSessionId)}`);
1865
+ }
1866
+ if (ownerSessionId) {
1867
+ runSourceAuthorizedTmux(leaderSource, `set-option -p -t ${leaderPaneId} ${OMX_PANE_INSTANCE_OPTION} ${shellQuoteSingle(ownerSessionId)}`);
1868
+ }
1869
+ runSourceAuthorizedTmux(leaderSource, `set-option -p -t ${leaderPaneId} ${OMX_TEAM_PANE_OWNER_OPTION} ${shellQuoteSingle(teamPaneOwnerId)}`);
1870
+ // All subsequent Team mutations must bind the frozen leader owner in the
1871
+ // same tmux-server transaction as their effect. The initial tag itself is
1872
+ // deliberately the sole bootstrap mutation before an owner exists.
1873
+ leaderSource = captureSourcePaneAuthority(leaderPaneId, teamPaneOwnerId);
1874
+ const exactProveAndTagReconciledPane = (paneId) => {
1875
+ const proof = readExactPaneProofSync(paneId);
1876
+ if (proof.status === 'unavailable')
1877
+ throw new ExactPaneProofUnavailableError(proof);
1878
+ if (proof.status === 'gone')
1879
+ return null;
1880
+ const paneSource = captureSourcePaneAuthority(paneId);
1881
+ if (paneSource.panePid !== proof.pid)
1882
+ throw new Error(`tmux pane identity changed: ${paneId}`);
1883
+ if (paneSource.teamPaneOwnerId && paneSource.teamPaneOwnerId !== teamPaneOwnerId) {
1884
+ throw new Error(`tmux pane team owner changed: ${paneId}`);
1885
+ }
1886
+ if (ownerSessionId) {
1887
+ runSourceAuthorizedTmux(paneSource, `set-option -p -t ${paneId} ${OMX_PANE_INSTANCE_OPTION} ${shellQuoteSingle(ownerSessionId)}`);
1888
+ }
1889
+ rollbackTaggedPaneOwnerIds.set(paneId, teamPaneOwnerId);
1890
+ runSourceAuthorizedTmux(paneSource, `set-option -p -t ${paneId} ${OMX_TEAM_PANE_OWNER_OPTION} ${shellQuoteSingle(teamPaneOwnerId)}`);
1891
+ rollbackPanes.set(paneId, proof.pid);
1892
+ return proof.pid;
1893
+ };
1255
1894
  if (canRecreateTeamHud) {
1256
- for (const hudPaneId of initialHudPaneIds) {
1257
- runTmux(['kill-pane', '-t', hudPaneId]);
1895
+ for (const [hudPaneId, hudPanePid] of initialWindowPanePids) {
1896
+ if (hudPaneId !== leaderPaneId)
1897
+ killExactPaneSync(hudPaneId, hudPanePid);
1258
1898
  }
1259
1899
  }
1260
1900
  const workerPaneIds = [];
1901
+ const workerStartupArgs = Array.from({ length: workerCount }, () => Object.freeze([]));
1902
+ const workerPanePidsByIndex = Array.from({ length: workerCount }, () => null);
1261
1903
  let rightStackRootPaneId = null;
1904
+ let rightStackRootPanePid = null;
1905
+ const frozenWindowPanePids = new Map([[leaderPaneId, leaderPanePid]]);
1906
+ const frozenWindowPaneOwners = new Map([[leaderPaneId, teamPaneOwnerId]]);
1262
1907
  for (let i = 1; i <= workerCount; i++) {
1263
1908
  const startup = workerStartups[i - 1] || {};
1264
1909
  const workerCwd = startup.cwd || cwd;
1265
1910
  const tmuxWorkerCwd = translatePathForMsys(workerCwd);
1266
1911
  const workerEnv = startup.env || {};
1267
1912
  const launchArgsForWorker = [...(workerLaunchPolicyPlan[i - 1] ?? workerLaunchArgs)];
1913
+ const attachedStartupPlan = buildAttachedTmuxWorkerStartupPlan(safeTeamName, i, launchArgsForWorker, workerCwd, workerEnv, workerCliPlan[i - 1], startup.initialPrompt, startup.workerRole);
1914
+ workerStartupArgs[i - 1] = attachedStartupPlan.startupArgs;
1268
1915
  trustWorkerMiseConfigIfAvailable(workerCwd);
1269
- const cmd = writeWorkerStartupScriptCommand(safeTeamName, i, launchArgsForWorker, workerCwd, workerEnv, workerCliPlan[i - 1], startup.initialPrompt, startup.workerRole) ?? buildWorkerStartupCommand(safeTeamName, i, launchArgsForWorker, workerCwd, workerEnv, workerCliPlan[i - 1], startup.initialPrompt, startup.workerRole);
1916
+ const cmd = writeWorkerStartupScriptCommand(safeTeamName, i, launchArgsForWorker, workerCwd, workerEnv, workerCliPlan[i - 1], startup.initialPrompt, startup.workerRole, attachedStartupPlan) ?? buildWorkerStartupCommand(safeTeamName, i, launchArgsForWorker, workerCwd, workerEnv, workerCliPlan[i - 1], startup.initialPrompt, startup.workerRole);
1270
1917
  // First split creates the right side from leader. Remaining splits stack on the right.
1271
1918
  const splitDirection = i === 1 ? '-h' : '-v';
1272
- const splitTarget = i === 1 ? leaderPaneId : (rightStackRootPaneId ?? leaderPaneId);
1273
- const split = runTmux([
1274
- 'split-window',
1275
- splitDirection,
1276
- '-t',
1277
- splitTarget,
1278
- '-d',
1279
- '-P',
1280
- '-F',
1281
- '#{pane_id}',
1282
- '-c',
1283
- tmuxWorkerCwd,
1284
- cmd,
1285
- ]);
1286
- if (!split.ok) {
1287
- throw new Error(`failed to create worker pane ${i}: ${split.stderr}`);
1919
+ const splitTarget = requireLiveTeamOwnedPaneSync(i === 1 ? leaderPaneId : (rightStackRootPaneId ?? leaderPaneId), i === 1 ? leaderPanePid : (rightStackRootPanePid ?? leaderPanePid), teamPaneOwnerId);
1920
+ const splitSource = captureSourcePaneAuthority(splitTarget, teamPaneOwnerId);
1921
+ let paneId;
1922
+ try {
1923
+ paneId = runSourceAuthorizedSplit(splitSource, (receipt) => `split-window ${splitDirection} -t ${splitTarget} -d -P -F '#{pane_id}' -c ${shellQuoteSingle(tmuxWorkerCwd)} ${shellQuoteSingle(bindSplitReceiptToPaneCommand(cmd, receipt))}`);
1288
1924
  }
1289
- const paneId = split.stdout.split('\n')[0]?.trim();
1290
- if (!paneId || !paneId.startsWith('%')) {
1291
- throw new Error(`failed to capture worker pane id for worker ${i}`);
1925
+ catch (error) {
1926
+ if (error instanceof AmbiguousSplitWindowOutputError) {
1927
+ const isUnassignedAmbiguity = error.newlyObservedPaneIds.length > 1;
1928
+ for (const candidatePaneId of error.newlyObservedPaneIds) {
1929
+ rollbackPanes.set(candidatePaneId, null);
1930
+ partialWorkerPaneIds.push(candidatePaneId);
1931
+ if (isUnassignedAmbiguity)
1932
+ partialStartupCleanupPanePids.set(candidatePaneId, null);
1933
+ }
1934
+ if (error.newlyObservedPaneIds.length === 1) {
1935
+ partialWorkerPaneIdsByIndex[i - 1] = error.newlyObservedPaneIds[0];
1936
+ }
1937
+ for (const candidatePaneId of error.newlyObservedPaneIds) {
1938
+ const reconciledPid = exactProveAndTagReconciledPane(candidatePaneId);
1939
+ if (reconciledPid === null) {
1940
+ rollbackPanes.delete(candidatePaneId);
1941
+ partialWorkerPaneIds.splice(partialWorkerPaneIds.lastIndexOf(candidatePaneId), 1);
1942
+ partialStartupCleanupPanePids.delete(candidatePaneId);
1943
+ if (partialWorkerPaneIdsByIndex[i - 1] === candidatePaneId) {
1944
+ partialWorkerPaneIdsByIndex[i - 1] = null;
1945
+ }
1946
+ }
1947
+ else if (isUnassignedAmbiguity) {
1948
+ partialStartupCleanupPanePids.set(candidatePaneId, reconciledPid);
1949
+ }
1950
+ else {
1951
+ partialWorkerPanePidsByIndex[i - 1] = reconciledPid;
1952
+ }
1953
+ }
1954
+ }
1955
+ throw error;
1292
1956
  }
1293
- rollbackPaneIds.push(paneId);
1957
+ // The pane exists once split-window returns its concrete ID. Persist it in
1958
+ // rollback/partial state before its first proof: a malformed or unavailable
1959
+ // topology must retain cleanup debt, but cannot authorize an effect.
1960
+ rollbackPanes.set(paneId, null);
1961
+ partialWorkerPaneIds.push(paneId);
1962
+ partialWorkerPaneIdsByIndex[i - 1] = paneId;
1963
+ const paneProof = readExactPaneProofSync(paneId);
1964
+ if (paneProof.status === 'unavailable')
1965
+ throw new ExactPaneProofUnavailableError(paneProof);
1966
+ if (paneProof.status === 'gone')
1967
+ throw new Error(`tmux pane is not proven live: ${paneId}`);
1968
+ const panePid = paneProof.pid;
1969
+ rollbackPanes.set(paneId, panePid);
1970
+ partialWorkerPanePidsByIndex[i - 1] = panePid;
1971
+ workerPanePidsByIndex[i - 1] = panePid;
1972
+ frozenWindowPanePids.set(paneId, panePid);
1294
1973
  if (isNativeWindows() && !waitForPaneToRemainPresent(teamTarget, paneId)) {
1295
1974
  throw new Error(`worker pane ${i} did not remain present after tmux split-window returned ${paneId}`);
1296
1975
  }
1297
- tagPaneInstance(paneId, ownerSessionId);
1298
- tagPaneTeamOwner(paneId, teamPaneOwnerId);
1976
+ const paneSource = captureSourcePaneAuthority(paneId);
1977
+ if (paneSource.panePid !== panePid)
1978
+ throw new Error(`tmux pane identity changed: ${paneId}`);
1979
+ if (ownerSessionId) {
1980
+ runSourceAuthorizedTmux(paneSource, `set-option -p -t ${paneId} ${OMX_PANE_INSTANCE_OPTION} ${shellQuoteSingle(ownerSessionId)}`);
1981
+ }
1982
+ rollbackTaggedPaneOwnerIds.set(paneId, teamPaneOwnerId);
1983
+ runSourceAuthorizedTmux(paneSource, `set-option -p -t ${paneId} ${OMX_TEAM_PANE_OWNER_OPTION} ${shellQuoteSingle(teamPaneOwnerId)}`);
1984
+ frozenWindowPaneOwners.set(paneId, teamPaneOwnerId);
1299
1985
  workerPaneIds.push(paneId);
1300
- if (i === 1)
1986
+ if (i === 1) {
1301
1987
  rightStackRootPaneId = paneId;
1988
+ rightStackRootPanePid = panePid;
1989
+ }
1302
1990
  }
1303
1991
  // Keep leader as full left/main pane; workers stay stacked on the right.
1304
- runTmux(['select-layout', '-t', teamTarget, 'main-vertical']);
1992
+ requireFrozenWindowTopologySync(teamTarget, frozenWindowPanePids, frozenWindowPaneOwners);
1993
+ runSourceAuthorizedTmux(leaderSource, `select-layout -t ${leaderSource.windowId} main-vertical`);
1305
1994
  // Force leader pane to use half the window width.
1306
1995
  const windowWidthResult = runTmux(['display-message', '-p', '-t', teamTarget, '#{window_width}']);
1307
1996
  if (windowWidthResult.ok) {
1308
1997
  const width = Number.parseInt(windowWidthResult.stdout.split('\n')[0]?.trim() || '', 10);
1309
1998
  if (Number.isFinite(width) && width >= 40) {
1310
1999
  const half = String(Math.floor(width / 2));
1311
- runTmux(['set-window-option', '-t', teamTarget, 'main-pane-width', half]);
1312
- runTmux(['select-layout', '-t', teamTarget, 'main-vertical']);
2000
+ requireFrozenWindowTopologySync(teamTarget, frozenWindowPanePids, frozenWindowPaneOwners);
2001
+ runSourceAuthorizedTmux(leaderSource, `set-window-option -t ${leaderSource.windowId} main-pane-width ${half}`);
2002
+ requireFrozenWindowTopologySync(teamTarget, frozenWindowPanePids, frozenWindowPaneOwners);
2003
+ runSourceAuthorizedTmux(leaderSource, `select-layout -t ${leaderSource.windowId} main-vertical`);
1313
2004
  }
1314
2005
  }
1315
2006
  // Re-create a single team HUD as a full-width bottom strip spanning both
@@ -1322,82 +2013,159 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
1322
2013
  if (canRecreateTeamHud && omxEntry) {
1323
2014
  const hudCmd = `exec env ${formatHudEnvAssignments(process.env, { sessionId: ownerSessionId, leaderPaneId })} node ${shellQuoteSingle(translatePathForMsys(omxEntry))} hud --watch`;
1324
2015
  const hudCwd = translatePathForMsys(cwd);
1325
- const hudResult = runTmux([
1326
- 'split-window', '-v', '-f', '-l', String(HUD_TMUX_TEAM_HEIGHT_LINES), '-t', teamTarget, '-d', '-P', '-F', '#{pane_id}', '-c', hudCwd, hudCmd,
1327
- ]);
1328
- if (hudResult.ok) {
1329
- const id = hudResult.stdout.split('\n')[0]?.trim() ?? '';
1330
- if (id.startsWith('%')) {
1331
- rollbackPaneIds.push(id);
1332
- if (isNativeWindows() && !waitForPaneToRemainPresent(teamTarget, id)) {
1333
- throw new Error(`HUD pane did not remain present after tmux split-window returned ${id}`);
2016
+ requireFrozenWindowTopologySync(teamTarget, frozenWindowPanePids, frozenWindowPaneOwners);
2017
+ const hudSplitTarget = leaderPaneId;
2018
+ const hudSource = captureSourcePaneAuthority(hudSplitTarget, teamPaneOwnerId);
2019
+ let id;
2020
+ try {
2021
+ id = runSourceAuthorizedSplit(hudSource, (receipt) => `split-window -v -f -l ${HUD_TMUX_TEAM_HEIGHT_LINES} -t ${hudSplitTarget} -d -P -F '#{pane_id}' -c ${shellQuoteSingle(hudCwd)} ${shellQuoteSingle(bindSplitReceiptToPaneCommand(hudCmd, receipt))}`);
2022
+ }
2023
+ catch (error) {
2024
+ if (error instanceof AmbiguousSplitWindowOutputError) {
2025
+ const isUnassignedAmbiguity = error.newlyObservedPaneIds.length > 1;
2026
+ for (const candidatePaneId of error.newlyObservedPaneIds) {
2027
+ rollbackPanes.set(candidatePaneId, null);
2028
+ if (isUnassignedAmbiguity)
2029
+ partialStartupCleanupPanePids.set(candidatePaneId, null);
1334
2030
  }
1335
- tagPaneInstance(id, ownerSessionId);
1336
- tagPaneTeamOwner(id, teamPaneOwnerId);
1337
- hudPaneId = id;
1338
- if (isNativeWindows()) {
1339
- // Native Windows tmux support may flow through psmux; issuing a
1340
- // direct control-plane resize avoids nested run-shell PATH drift.
1341
- const reconcile = runTmux(buildHudResizeArgs(hudPaneId));
1342
- if (!reconcile.ok) {
1343
- throw new Error(`failed to reconcile HUD resize: ${reconcile.stderr}`);
1344
- }
2031
+ if (error.newlyObservedPaneIds.length === 1) {
2032
+ partialHudPaneId = error.newlyObservedPaneIds[0];
2033
+ partialHudPanePid = null;
1345
2034
  }
1346
- else {
1347
- const hookTarget = buildResizeHookTarget(sessionName, windowIndex);
1348
- const hookName = buildResizeHookName(safeTeamName, sessionName, windowIndex, hudPaneId);
1349
- const registerHook = runTmux(buildRegisterResizeHookArgs(hookTarget, hookName, hudPaneId));
1350
- const clientAttachedHookName = buildClientAttachedReconcileHookName(safeTeamName, sessionName, windowIndex, hudPaneId);
1351
- if (registerHook.ok) {
1352
- resizeHookTarget = hookTarget;
1353
- resizeHookName = hookName;
1354
- registeredResizeHook = { name: resizeHookName, target: resizeHookTarget };
2035
+ for (const candidatePaneId of error.newlyObservedPaneIds) {
2036
+ const reconciledPid = exactProveAndTagReconciledPane(candidatePaneId);
2037
+ if (reconciledPid === null) {
2038
+ rollbackPanes.delete(candidatePaneId);
2039
+ partialStartupCleanupPanePids.delete(candidatePaneId);
2040
+ if (partialHudPaneId === candidatePaneId)
2041
+ partialHudPaneId = null;
1355
2042
  }
1356
- else {
1357
- // tmux versions/builds that reject indexed client-resized hooks should not
1358
- // abort madmax/team startup after panes were successfully created. Keep the
1359
- // fallback narrow: skip only the long-lived resize hook metadata, then
1360
- // still try the one-shot client-attached reconcile plus the explicit
1361
- // delayed/direct resize checks below so real tmux/run-shell failures
1362
- // still surface.
1363
- console.warn(`[omx] tmux resize hook unavailable for ${hookTarget} (${hookName}): ${registerHook.stderr}; `
1364
- + 'continuing with best-effort HUD resize fallback.');
1365
- }
1366
- const registerClientAttachedHook = runTmux(buildRegisterClientAttachedReconcileArgs(hookTarget, clientAttachedHookName, hudPaneId));
1367
- if (registerClientAttachedHook.ok) {
1368
- registeredClientAttachedHook = { name: clientAttachedHookName, target: hookTarget };
2043
+ else if (isUnassignedAmbiguity) {
2044
+ partialStartupCleanupPanePids.set(candidatePaneId, reconciledPid);
1369
2045
  }
1370
2046
  else {
1371
- console.warn(`[omx] tmux client-attached resize fallback unavailable for ${hookTarget} `
1372
- + `(${clientAttachedHookName}): ${registerClientAttachedHook.stderr}; continuing with delayed HUD resize fallback.`);
1373
- }
1374
- const delayed = runTmux(buildScheduleDelayedHudResizeArgs(hudPaneId));
1375
- if (!delayed.ok) {
1376
- console.warn(`[omx] tmux delayed HUD resize unavailable for ${hudPaneId}: ${delayed.stderr}; continuing.`);
1377
- }
1378
- const reconcile = runTmux(buildReconcileHudResizeArgs(hudPaneId));
1379
- if (!reconcile.ok) {
1380
- console.warn(`[omx] tmux HUD resize reconcile unavailable for ${hudPaneId}: ${reconcile.stderr}; continuing.`);
2047
+ partialHudPanePid = reconciledPid;
1381
2048
  }
1382
2049
  }
1383
2050
  }
2051
+ throw error;
2052
+ }
2053
+ {
2054
+ // split-window has created a concrete pane even though no proof has
2055
+ // authorized an effect yet. Retain PID-less cleanup debt first.
2056
+ rollbackPanes.set(id, null);
2057
+ partialHudPaneId = id;
2058
+ partialHudPanePid = null;
2059
+ const hudProof = readExactPaneProofSync(id);
2060
+ if (hudProof.status === 'unavailable')
2061
+ throw new ExactPaneProofUnavailableError(hudProof);
2062
+ if (hudProof.status === 'gone')
2063
+ throw new Error(`tmux pane is not proven live: ${id}`);
2064
+ const hudPanePid = hudProof.pid;
2065
+ frozenWindowPanePids.set(id, hudPanePid);
2066
+ rollbackPanes.set(id, hudPanePid);
2067
+ partialHudPanePid = hudPanePid;
2068
+ if (isNativeWindows() && !waitForPaneToRemainPresent(teamTarget, id)) {
2069
+ throw new Error(`HUD pane did not remain present after tmux split-window returned ${id}`);
2070
+ }
2071
+ const hudSource = captureSourcePaneAuthority(id);
2072
+ if (hudSource.panePid !== hudPanePid)
2073
+ throw new Error(`tmux pane identity changed: ${id}`);
2074
+ if (ownerSessionId) {
2075
+ runSourceAuthorizedTmux(hudSource, `set-option -p -t ${id} ${OMX_PANE_INSTANCE_OPTION} ${shellQuoteSingle(ownerSessionId)}`);
2076
+ }
2077
+ rollbackTaggedPaneOwnerIds.set(id, teamPaneOwnerId);
2078
+ runSourceAuthorizedTmux(hudSource, `set-option -p -t ${id} ${OMX_TEAM_PANE_OWNER_OPTION} ${shellQuoteSingle(teamPaneOwnerId)}`);
2079
+ frozenWindowPaneOwners.set(id, teamPaneOwnerId);
2080
+ hudPaneId = id;
2081
+ if (isNativeWindows()) {
2082
+ const provenHudPaneId = requireLiveTeamOwnedPaneSync(hudPaneId, hudPanePid, teamPaneOwnerId);
2083
+ const reconcile = runTmux(buildHudResizeArgs(provenHudPaneId));
2084
+ if (!reconcile.ok) {
2085
+ throw new Error(`failed to reconcile HUD resize: ${reconcile.stderr}`);
2086
+ }
2087
+ }
2088
+ else {
2089
+ const hookTarget = buildResizeHookTarget(sessionName, windowIndex);
2090
+ const hookName = buildResizeHookName(safeTeamName, sessionName, windowIndex, hudPaneId);
2091
+ requireLiveTeamOwnedPaneSync(hudPaneId, hudPanePid, teamPaneOwnerId);
2092
+ const registerHook = runTmux(buildRegisterResizeHookArgs(hookTarget, hookName, hudPaneId, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid, teamPaneOwnerId));
2093
+ const clientAttachedHookName = buildClientAttachedReconcileHookName(safeTeamName, sessionName, windowIndex, hudPaneId);
2094
+ if (registerHook.ok) {
2095
+ resizeHookTarget = hookTarget;
2096
+ resizeHookName = hookName;
2097
+ registeredResizeHook = {
2098
+ name: resizeHookName,
2099
+ target: resizeHookTarget,
2100
+ leaderPaneId,
2101
+ leaderPanePid,
2102
+ hudPaneId,
2103
+ hudPanePid,
2104
+ teamPaneOwnerId,
2105
+ };
2106
+ }
2107
+ else {
2108
+ console.warn(`[omx] tmux resize hook unavailable for ${hookTarget} (${hookName}): ${registerHook.stderr}; `
2109
+ + 'continuing with best-effort HUD resize fallback.');
2110
+ }
2111
+ requireLiveTeamOwnedPaneSync(hudPaneId, hudPanePid, teamPaneOwnerId);
2112
+ const registerClientAttachedHook = runTmux(buildRegisterClientAttachedReconcileArgs(hookTarget, clientAttachedHookName, hudPaneId, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid, teamPaneOwnerId));
2113
+ if (registerClientAttachedHook.ok) {
2114
+ registeredClientAttachedHook = {
2115
+ name: clientAttachedHookName,
2116
+ target: hookTarget,
2117
+ leaderPaneId,
2118
+ leaderPanePid,
2119
+ hudPaneId,
2120
+ hudPanePid,
2121
+ teamPaneOwnerId,
2122
+ };
2123
+ }
2124
+ else {
2125
+ console.warn(`[omx] tmux client-attached resize fallback unavailable for ${hookTarget} `
2126
+ + `(${clientAttachedHookName}): ${registerClientAttachedHook.stderr}; continuing with delayed HUD resize fallback.`);
2127
+ }
2128
+ requireLiveTeamOwnedPaneSync(hudPaneId, hudPanePid, teamPaneOwnerId);
2129
+ const delayed = runTmux(buildScheduleDelayedHudResizeArgs(hudPaneId, HUD_RESIZE_RECONCILE_DELAY_SECONDS, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid, teamPaneOwnerId));
2130
+ if (!delayed.ok) {
2131
+ console.warn(`[omx] tmux delayed HUD resize unavailable for ${hudPaneId}: ${delayed.stderr}; continuing.`);
2132
+ }
2133
+ requireLiveTeamOwnedPaneSync(hudPaneId, hudPanePid, teamPaneOwnerId);
2134
+ const reconcile = runTmux(buildReconcileHudResizeArgs(hudPaneId, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid, teamPaneOwnerId));
2135
+ if (!reconcile.ok) {
2136
+ console.warn(`[omx] tmux HUD resize reconcile unavailable for ${hudPaneId}: ${reconcile.stderr}; continuing.`);
2137
+ }
2138
+ }
1384
2139
  }
1385
2140
  }
1386
- runTmux(['select-pane', '-t', leaderPaneId]);
1387
- redrawLeaderPaneAfterTeamLayout(leaderPaneId);
2141
+ const focusedLeader = requireLiveTeamOwnedPaneSync(leaderPaneId, leaderPanePid, teamPaneOwnerId);
2142
+ if (focusedLeader !== leaderSource.paneId)
2143
+ throw new Error(`tmux pane identity changed: ${leaderPaneId}`);
2144
+ runSourceAuthorizedTmux(leaderSource, `select-pane -t ${leaderPaneId}`);
2145
+ redrawLeaderPaneAfterTeamLayout(leaderSource, teamPaneOwnerId);
1388
2146
  sleepSeconds(0.5);
1389
2147
  // Enable mouse scrolling so agent output panes can be scrolled with the
1390
2148
  // mouse wheel without conflicting with keyboard up/down arrow-key input
1391
2149
  // history navigation in the Codex CLI input field. (issue #103)
1392
2150
  // Opt-out: set OMX_TEAM_MOUSE=0 in the environment.
1393
2151
  if (process.env.OMX_TEAM_MOUSE !== '0') {
1394
- enableMouseScrolling(sessionName);
2152
+ enableMouseScrolling(sessionName, () => { requireLiveTeamOwnedPaneSync(leaderPaneId, leaderPanePid, teamPaneOwnerId); });
1395
2153
  }
1396
2154
  return {
1397
2155
  name: teamTarget,
2156
+ tmuxSessionId: leaderSource.sessionId,
2157
+ tmuxSessionCreated: leaderSource.sessionCreated,
1398
2158
  workerCount,
1399
2159
  cwd,
1400
2160
  workerPaneIds,
2161
+ workerPaneIdsByIndex: [...workerPaneIds],
2162
+ workerPanePidsByIndex,
2163
+ leaderPanePid,
2164
+ workerStartupArgs: Object.freeze(workerStartupArgs),
2165
+ leaderSessionId: leaderSource.sessionId,
2166
+ leaderSessionCreated: leaderSource.sessionCreated,
2167
+ leaderWindowId: leaderSource.windowId,
2168
+ hudPanePid: partialHudPanePid,
1401
2169
  leaderPaneId,
1402
2170
  hudPaneId,
1403
2171
  resizeHookName,
@@ -1406,14 +2174,90 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
1406
2174
  };
1407
2175
  }
1408
2176
  catch (error) {
1409
- if (registeredClientAttachedHook) {
1410
- runTmux(buildUnregisterClientAttachedReconcileArgs(registeredClientAttachedHook.target, registeredClientAttachedHook.name));
2177
+ const cleanupErrors = [];
2178
+ if (error instanceof AmbiguousSplitWindowOutputError && error.reconciliationError) {
2179
+ cleanupErrors.push('failed to reconcile tmux pane topology after ambiguous split output');
1411
2180
  }
1412
- if (registeredResizeHook) {
1413
- runTmux(buildUnregisterResizeHookArgs(registeredResizeHook.target, registeredResizeHook.name));
2181
+ const unregisterAuthorizedHook = (hook, unregister, label) => {
2182
+ try {
2183
+ requireLiveTeamOwnedPaneSync(hook.leaderPaneId, hook.leaderPanePid, hook.teamPaneOwnerId);
2184
+ requireLiveTeamOwnedPaneSync(hook.hudPaneId, hook.hudPanePid, hook.teamPaneOwnerId);
2185
+ const unregistered = runTmux(unregister(hook.target, hook.name));
2186
+ if (unregistered.ok)
2187
+ return true;
2188
+ cleanupErrors.push(`failed to unregister tmux ${label} hook ${hook.name}: ${unregistered.stderr}`);
2189
+ }
2190
+ catch (cleanupError) {
2191
+ cleanupErrors.push(`unable to authorize tmux ${label} hook cleanup ${hook.name}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
2192
+ }
2193
+ return false;
2194
+ };
2195
+ if (registeredClientAttachedHook && unregisterAuthorizedHook(registeredClientAttachedHook, buildUnregisterClientAttachedReconcileArgs, 'client-attached')) {
2196
+ registeredClientAttachedHook = null;
2197
+ }
2198
+ if (registeredResizeHook && unregisterAuthorizedHook(registeredResizeHook, buildUnregisterResizeHookArgs, 'resize')) {
2199
+ registeredResizeHook = null;
2200
+ }
2201
+ const proofUnavailable = [];
2202
+ if (error instanceof ExactPaneProofUnavailableError)
2203
+ proofUnavailable.push(error.proof);
2204
+ const unresolvedPaneIds = new Set(rollbackPanes.keys());
2205
+ for (const [paneId, panePid] of rollbackPanes) {
2206
+ // split-window IDs without a positive exact PID proof are cleanup debt,
2207
+ // never authorization to affect a potentially recycled pane ID.
2208
+ if (panePid === null)
2209
+ continue;
2210
+ try {
2211
+ const expectedOwnerId = rollbackTaggedPaneOwnerIds.get(paneId);
2212
+ killExactPaneSync(paneId, panePid, expectedOwnerId
2213
+ ? () => { requireLiveTeamOwnedPaneSync(paneId, panePid, expectedOwnerId); }
2214
+ : undefined);
2215
+ unresolvedPaneIds.delete(paneId);
2216
+ }
2217
+ catch (cleanupError) {
2218
+ if (cleanupError instanceof ExactPaneProofUnavailableError) {
2219
+ proofUnavailable.push(cleanupError.proof);
2220
+ break;
2221
+ }
2222
+ else {
2223
+ cleanupErrors.push(cleanupError instanceof Error ? cleanupError.message : String(cleanupError));
2224
+ }
2225
+ }
1414
2226
  }
1415
- for (const paneId of rollbackPaneIds) {
1416
- runTmux(['kill-pane', '-t', paneId]);
2227
+ const unresolvedWorkerPaneIds = partialWorkerPaneIds.filter((paneId) => unresolvedPaneIds.has(paneId));
2228
+ const unresolvedWorkerPaneIdsByIndex = partialWorkerPaneIdsByIndex
2229
+ .map((paneId) => paneId && unresolvedPaneIds.has(paneId) ? paneId : null);
2230
+ const unresolvedWorkerPanePidsByIndex = partialWorkerPanePidsByIndex
2231
+ .map((panePid, index) => partialWorkerPaneIdsByIndex[index] && unresolvedPaneIds.has(partialWorkerPaneIdsByIndex[index]) ? panePid : null);
2232
+ const unresolvedStartupCleanupPanes = [...partialStartupCleanupPanePids.entries()]
2233
+ .filter(([paneId]) => unresolvedPaneIds.has(paneId))
2234
+ .map(([paneId, panePid]) => ({ paneId, panePid }));
2235
+ const unresolvedHudPaneId = partialHudPaneId && (unresolvedPaneIds.has(partialHudPaneId)
2236
+ || registeredResizeHook !== null
2237
+ || registeredClientAttachedHook !== null)
2238
+ ? partialHudPaneId
2239
+ : null;
2240
+ const hasRecoverablePartialArtifact = unresolvedPaneIds.size > 0
2241
+ || registeredResizeHook !== null
2242
+ || registeredClientAttachedHook !== null
2243
+ || (error instanceof AmbiguousSplitWindowOutputError && error.reconciliationError !== null);
2244
+ if (hasRecoverablePartialArtifact && partialTeamTarget && partialLeaderPaneId) {
2245
+ throw new CreateTeamSessionPartialError({
2246
+ name: partialTeamTarget,
2247
+ workerCount,
2248
+ cwd,
2249
+ workerPaneIds: unresolvedWorkerPaneIds,
2250
+ workerPaneIdsByIndex: unresolvedWorkerPaneIdsByIndex,
2251
+ workerPanePidsByIndex: unresolvedWorkerPanePidsByIndex,
2252
+ startupCleanupPanes: unresolvedStartupCleanupPanes,
2253
+ leaderPanePid: partialLeaderPanePid,
2254
+ hudPanePid: unresolvedHudPaneId ? partialHudPanePid : null,
2255
+ leaderPaneId: partialLeaderPaneId,
2256
+ hudPaneId: unresolvedHudPaneId,
2257
+ resizeHookName: registeredResizeHook?.name ?? null,
2258
+ resizeHookTarget: registeredResizeHook?.target ?? null,
2259
+ teamPaneOwnerId: partialTeamPaneOwnerId,
2260
+ }, proofUnavailable, error, cleanupErrors);
1417
2261
  }
1418
2262
  throw error;
1419
2263
  }
@@ -1422,34 +2266,75 @@ export function restoreStandaloneHudPane(leaderPaneId, cwd, options = {}) {
1422
2266
  const normalizedLeaderPaneId = normalizePaneTarget(leaderPaneId);
1423
2267
  if (!normalizedLeaderPaneId)
1424
2268
  return null;
2269
+ // The split is irreversible without a durable cleanup obligation. Validate
2270
+ // its canonical Team-root location before authorizing any pane effect.
2271
+ restoredHudCleanupDebtPath(cwd, options.stateRoot);
1425
2272
  const omxEntry = resolveOmxCliEntryPath();
1426
2273
  if (!omxEntry || omxEntry.trim() === '')
1427
2274
  return null;
1428
- const [existingHudPaneId, ...duplicateHudPaneIds] = findOwnedHudPaneIds(normalizedLeaderPaneId, normalizedLeaderPaneId);
2275
+ const leaderPanePid = (() => {
2276
+ const proof = readExactPaneProofSync(normalizedLeaderPaneId);
2277
+ if (proof.status === 'unavailable')
2278
+ throw new ExactPaneProofUnavailableError(proof);
2279
+ if (proof.status === 'gone')
2280
+ throw new Error(`tmux pane is not proven live: ${normalizedLeaderPaneId}`);
2281
+ if (options.expectedLeaderPanePid !== undefined && proof.pid !== options.expectedLeaderPanePid) {
2282
+ throw new Error(`tmux pane identity changed: ${normalizedLeaderPaneId}`);
2283
+ }
2284
+ return proof.pid;
2285
+ })();
2286
+ const requireAuthorizedLeaderPane = () => {
2287
+ options.assertLeaderPaneAuthorization?.();
2288
+ return requireLiveExactPaneSync(normalizedLeaderPaneId, options.expectedLeaderPanePid ?? leaderPanePid);
2289
+ };
2290
+ requireAuthorizedLeaderPane();
2291
+ const paneListResult = listPanesResult(normalizedLeaderPaneId);
2292
+ if (paneListResult.error)
2293
+ throw new Error(`failed to read tmux pane topology: ${paneListResult.error}`);
2294
+ const [existingHudPaneId, ...duplicateHudPaneIds] = findHudWatchPaneIds(paneListResult.panes, normalizedLeaderPaneId, { leaderPaneId: normalizedLeaderPaneId });
2295
+ const ownedHudPanePids = new Map();
2296
+ for (const paneId of [existingHudPaneId, ...duplicateHudPaneIds]) {
2297
+ if (!paneId)
2298
+ continue;
2299
+ const proof = readExactPaneProofSync(paneId);
2300
+ if (proof.status === 'unavailable')
2301
+ throw new ExactPaneProofUnavailableError(proof);
2302
+ if (proof.status === 'gone')
2303
+ throw new Error(`tmux pane is not proven live: ${paneId}`);
2304
+ ownedHudPanePids.set(paneId, proof.pid);
2305
+ }
1429
2306
  for (const paneId of duplicateHudPaneIds) {
1430
- runTmux(['kill-pane', '-t', paneId]);
2307
+ killExactPaneSync(paneId, ownedHudPanePids.get(paneId), requireAuthorizedLeaderPane);
1431
2308
  }
1432
2309
  if (existingHudPaneId) {
1433
2310
  if (isNativeWindows()) {
1434
- runTmux(buildHudResizeArgs(existingHudPaneId));
2311
+ const exactHudPaneId = requireLiveExactPaneSync(existingHudPaneId, ownedHudPanePids.get(existingHudPaneId));
2312
+ requireAuthorizedLeaderPane();
2313
+ const reconcile = runTmux(buildHudResizeArgs(exactHudPaneId));
2314
+ if (!reconcile.ok)
2315
+ throw new Error(`failed to reconcile standalone HUD resize: ${reconcile.stderr}`);
1435
2316
  }
1436
2317
  else {
1437
- runTmux(buildScheduleDelayedHudResizeArgs(existingHudPaneId));
1438
- runTmux(buildReconcileHudResizeArgs(existingHudPaneId));
2318
+ requireAuthorizedLeaderPane();
2319
+ runTmux(buildScheduleDelayedHudResizeArgs(existingHudPaneId, HUD_RESIZE_RECONCILE_DELAY_SECONDS, HUD_TMUX_TEAM_HEIGHT_LINES, ownedHudPanePids.get(existingHudPaneId)));
2320
+ requireAuthorizedLeaderPane();
2321
+ runTmux(buildReconcileHudResizeArgs(existingHudPaneId, HUD_TMUX_TEAM_HEIGHT_LINES, ownedHudPanePids.get(existingHudPaneId)));
1439
2322
  }
1440
- runTmux(['select-pane', '-t', normalizedLeaderPaneId]);
2323
+ runTmux(['select-pane', '-t', requireAuthorizedLeaderPane()]);
1441
2324
  return existingHudPaneId;
1442
2325
  }
1443
- const hudCmd = `exec env ${formatHudEnvAssignments(process.env, { sessionId: options.sessionId, leaderPaneId: normalizedLeaderPaneId })} ${shellQuoteSingle(translatePathForMsys(resolveLeaderNodePath()))} ${shellQuoteSingle(translatePathForMsys(omxEntry))} hud --watch`;
2326
+ const beforeSplitPaneIds = new Set(paneListResult.panes.map((pane) => pane.paneId));
2327
+ const splitReceipt = sourceTransactionReceipt();
2328
+ const hudCmd = bindSplitReceiptToPaneCommand(`exec env ${formatHudEnvAssignments(process.env, { sessionId: options.sessionId, leaderPaneId: normalizedLeaderPaneId })} ${shellQuoteSingle(translatePathForMsys(resolveLeaderNodePath()))} ${shellQuoteSingle(translatePathForMsys(omxEntry))} hud --watch`, splitReceipt);
1444
2329
  let hudResult = null;
1445
- for (const restoreCwd of resolveStandaloneHudRestoreCwdCandidates(normalizedLeaderPaneId, cwd, options.cwd)) {
2330
+ for (const restoreCwd of resolveStandaloneHudRestoreCwdCandidates(normalizedLeaderPaneId, cwd, options.cwd, requireAuthorizedLeaderPane)) {
1446
2331
  const candidateResult = runTmux([
1447
2332
  'split-window',
1448
2333
  '-v',
1449
2334
  '-l',
1450
2335
  String(HUD_TMUX_TEAM_HEIGHT_LINES),
1451
2336
  '-t',
1452
- normalizedLeaderPaneId,
2337
+ requireAuthorizedLeaderPane(),
1453
2338
  '-d',
1454
2339
  '-P',
1455
2340
  '-F',
@@ -1457,7 +2342,7 @@ export function restoreStandaloneHudPane(leaderPaneId, cwd, options = {}) {
1457
2342
  '-c',
1458
2343
  translatePathForMsys(restoreCwd.rawPath),
1459
2344
  hudCmd,
1460
- ]);
2345
+ ], true);
1461
2346
  if (candidateResult.ok) {
1462
2347
  hudResult = candidateResult;
1463
2348
  break;
@@ -1465,17 +2350,87 @@ export function restoreStandaloneHudPane(leaderPaneId, cwd, options = {}) {
1465
2350
  }
1466
2351
  if (!hudResult?.ok)
1467
2352
  return null;
1468
- const paneId = hudResult.stdout.split('\n')[0]?.trim() ?? '';
1469
- if (!paneId.startsWith('%'))
1470
- return null;
2353
+ const paneId = parseSplitWindowPaneId(hudResult.stdout);
2354
+ if (!paneId) {
2355
+ const afterTopology = listPanesResult(normalizedLeaderPaneId);
2356
+ if (afterTopology.error) {
2357
+ throw new Error('restored_hud_split_topology_reconciliation_failed');
2358
+ }
2359
+ const newlyObservedPaneIds = afterTopology.panes
2360
+ .filter((pane) => !beforeSplitPaneIds.has(pane.paneId) && pane.startCommand.includes(splitReceipt))
2361
+ .map((pane) => pane.paneId);
2362
+ if (newlyObservedPaneIds.length === 1) {
2363
+ const reconciledPaneId = newlyObservedPaneIds[0];
2364
+ persistRestoredHudCleanupDebtSync(cwd, {
2365
+ schema_version: 1,
2366
+ operation: 'restored_hud_cleanup',
2367
+ pane_id: reconciledPaneId,
2368
+ pane_pid: null,
2369
+ leader_pane_id: normalizedLeaderPaneId,
2370
+ leader_pane_pid: options.expectedLeaderPanePid ?? leaderPanePid,
2371
+ leader_pane_owner_id: options.expectedLeaderPaneOwnerId?.trim() || null,
2372
+ hud_owner_leader_pane_id: normalizedLeaderPaneId,
2373
+ }, options.stateRoot);
2374
+ const reconciledProof = readExactPaneProofSync(reconciledPaneId);
2375
+ if (reconciledProof.status === 'unavailable')
2376
+ throw new ExactPaneProofUnavailableError(reconciledProof);
2377
+ if (reconciledProof.status === 'gone')
2378
+ throw new Error(`tmux pane is not proven live: ${reconciledPaneId}`);
2379
+ persistRestoredHudCleanupDebtSync(cwd, {
2380
+ schema_version: 1,
2381
+ operation: 'restored_hud_cleanup',
2382
+ pane_id: reconciledPaneId,
2383
+ pane_pid: reconciledProof.pid,
2384
+ leader_pane_id: normalizedLeaderPaneId,
2385
+ leader_pane_pid: options.expectedLeaderPanePid ?? leaderPanePid,
2386
+ leader_pane_owner_id: options.expectedLeaderPaneOwnerId?.trim() || null,
2387
+ hud_owner_leader_pane_id: normalizedLeaderPaneId,
2388
+ }, options.stateRoot);
2389
+ }
2390
+ throw new Error('restored_hud_split_output_ambiguous');
2391
+ }
2392
+ persistRestoredHudCleanupDebtSync(cwd, {
2393
+ schema_version: 1,
2394
+ operation: 'restored_hud_cleanup',
2395
+ pane_id: paneId,
2396
+ pane_pid: null,
2397
+ leader_pane_id: normalizedLeaderPaneId,
2398
+ leader_pane_pid: options.expectedLeaderPanePid ?? leaderPanePid,
2399
+ leader_pane_owner_id: options.expectedLeaderPaneOwnerId?.trim() || null,
2400
+ hud_owner_leader_pane_id: normalizedLeaderPaneId,
2401
+ }, options.stateRoot);
2402
+ const hudPanePid = (() => {
2403
+ const proof = readExactPaneProofSync(paneId);
2404
+ if (proof.status === 'unavailable')
2405
+ throw new ExactPaneProofUnavailableError(proof);
2406
+ if (proof.status === 'gone')
2407
+ throw new Error(`tmux pane is not proven live: ${paneId}`);
2408
+ return proof.pid;
2409
+ })();
2410
+ persistRestoredHudCleanupDebtSync(cwd, {
2411
+ schema_version: 1,
2412
+ operation: 'restored_hud_cleanup',
2413
+ pane_id: paneId,
2414
+ pane_pid: hudPanePid,
2415
+ leader_pane_id: normalizedLeaderPaneId,
2416
+ leader_pane_pid: options.expectedLeaderPanePid ?? leaderPanePid,
2417
+ leader_pane_owner_id: options.expectedLeaderPaneOwnerId?.trim() || null,
2418
+ hud_owner_leader_pane_id: normalizedLeaderPaneId,
2419
+ }, options.stateRoot);
1471
2420
  if (isNativeWindows()) {
1472
- runTmux(buildHudResizeArgs(paneId));
2421
+ const exactHudPaneId = requireLiveExactPaneSync(paneId, hudPanePid);
2422
+ requireAuthorizedLeaderPane();
2423
+ const reconcile = runTmux(buildHudResizeArgs(exactHudPaneId));
2424
+ if (!reconcile.ok)
2425
+ throw new Error(`failed to reconcile standalone HUD resize: ${reconcile.stderr}`);
1473
2426
  }
1474
2427
  else {
1475
- runTmux(buildScheduleDelayedHudResizeArgs(paneId));
1476
- runTmux(buildReconcileHudResizeArgs(paneId));
2428
+ requireAuthorizedLeaderPane();
2429
+ runTmux(buildScheduleDelayedHudResizeArgs(paneId, HUD_RESIZE_RECONCILE_DELAY_SECONDS, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid));
2430
+ requireAuthorizedLeaderPane();
2431
+ runTmux(buildReconcileHudResizeArgs(paneId, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid));
1477
2432
  }
1478
- runTmux(['select-pane', '-t', normalizedLeaderPaneId]);
2433
+ runTmux(['select-pane', '-t', requireAuthorizedLeaderPane()]);
1479
2434
  return paneId;
1480
2435
  }
1481
2436
  /**
@@ -1487,21 +2442,21 @@ export function restoreStandaloneHudPane(leaderPaneId, cwd, options = {}) {
1487
2442
  * oh-my-tmux, or other sessions. Returns true if the session mouse option
1488
2443
  * was set successfully, false otherwise.
1489
2444
  */
1490
- export function enableMouseScrolling(sessionTarget) {
2445
+ export function enableMouseScrolling(sessionTarget, beforeEffect) {
2446
+ beforeEffect?.();
1491
2447
  const result = runTmux(['set-option', '-t', sessionTarget, 'mouse', 'on']);
1492
2448
  if (!result.ok)
1493
2449
  return false;
1494
2450
  // Enable OSC 52 so copy-selection-and-cancel propagates selected text to
1495
2451
  // the terminal's clipboard without requiring xclip or pbcopy. (closes #206)
2452
+ beforeEffect?.();
1496
2453
  runTmux(['set-option', '-t', sessionTarget, 'set-clipboard', 'on']);
1497
2454
  // Mouse selection enters tmux copy-mode. Keep the mitigation session-scoped
1498
2455
  // so OMX does not mutate users' global tmux style defaults. (issue #1448)
1499
- mitigateCopyModeUnderlineArtifacts(sessionTarget);
2456
+ mitigateCopyModeUnderlineArtifacts(sessionTarget, beforeEffect);
1500
2457
  return true;
1501
2458
  }
1502
- function paneTarget(sessionName, workerIndex, workerPaneId) {
1503
- if (workerPaneId && workerPaneId.startsWith('%'))
1504
- return workerPaneId;
2459
+ function paneTarget(sessionName, workerIndex) {
1505
2460
  if (sessionName.includes(':')) {
1506
2461
  return `${sessionName}.${workerIndex}`;
1507
2462
  }
@@ -1531,7 +2486,10 @@ function paneHasClaudeBypassPermissionsPrompt(captured) {
1531
2486
  && tail.some((line) => /Enter\s*to\s*confirm/i.test(line));
1532
2487
  return hasWarning && hasChoices;
1533
2488
  }
1534
- export function evaluateStartupDirectTriggerSafetyCapture(captured, workerCli) {
2489
+ export function evaluateStartupDirectTriggerSafetyCapture(captured, workerCli, finalArgs) {
2490
+ const incompatibleReason = classifyCodexBypassMdmIncompatibility(captured, workerCli, finalArgs);
2491
+ if (incompatibleReason)
2492
+ return { safe: false, reason: incompatibleReason };
1535
2493
  if (paneHasTrustPrompt(captured))
1536
2494
  return { safe: false, reason: 'trust_prompt' };
1537
2495
  if (paneHasClaudeBypassPermissionsPrompt(captured))
@@ -1544,27 +2502,53 @@ export function evaluateStartupDirectTriggerSafetyCapture(captured, workerCli) {
1544
2502
  return { safe: true, reason: 'codex_viewport' };
1545
2503
  return { safe: false, reason: 'not_agent_viewport' };
1546
2504
  }
1547
- export async function evaluateStartupDirectTriggerSafety(sessionName, workerIndex, workerPaneId, workerCli) {
2505
+ export async function evaluateStartupDirectTriggerSafety(sessionName, workerIndex, workerPaneId, workerCli, finalArgs) {
1548
2506
  if (!isTmuxAvailable())
1549
2507
  return { safe: false, reason: 'tmux_unavailable' };
1550
- const target = paneTarget(sessionName, workerIndex, workerPaneId);
2508
+ const target = await resolveWorkerPaneTargetAsync(sessionName, workerIndex, workerPaneId);
2509
+ if (!target)
2510
+ return { safe: false, reason: 'capture_failed' };
1551
2511
  const result = await runTmuxAsync(sharedBuildVisibleCapturePaneArgv(target));
1552
2512
  if (!result.ok)
1553
2513
  return { safe: false, reason: 'capture_failed' };
1554
- return evaluateStartupDirectTriggerSafetyCapture(result.stdout, workerCli);
2514
+ return evaluateStartupDirectTriggerSafetyCapture(result.stdout, workerCli, finalArgs);
1555
2515
  }
1556
- function acceptClaudeBypassPermissionsPrompt(target) {
1557
- runTmux(['send-keys', '-t', target, '-l', '--', '2']);
2516
+ function acceptClaudeBypassPermissionsPrompt(resolveTarget) {
2517
+ const literalTarget = resolveTarget();
2518
+ if (!literalTarget)
2519
+ return false;
2520
+ runTmux(['send-keys', '-t', literalTarget, '-l', '--', '2']);
1558
2521
  sleepFractionalSeconds(0.12);
1559
- runTmux(['send-keys', '-t', target, 'C-m']);
2522
+ const submitTarget = resolveTarget();
2523
+ if (!submitTarget)
2524
+ return false;
2525
+ runTmux(['send-keys', '-t', submitTarget, 'C-m']);
2526
+ return true;
1560
2527
  }
1561
- function dismissClaudeBypassPermissionsPromptIfPresent(target, captured) {
2528
+ function dismissClaudeBypassPermissionsPromptIfPresent(resolveTarget, captured) {
1562
2529
  if (process.env.OMX_TEAM_AUTO_ACCEPT_BYPASS === '0')
1563
2530
  return false;
1564
2531
  if (!paneHasClaudeBypassPermissionsPrompt(captured))
1565
2532
  return false;
1566
- acceptClaudeBypassPermissionsPrompt(target);
1567
- return true;
2533
+ return acceptClaudeBypassPermissionsPrompt(resolveTarget);
2534
+ }
2535
+ async function dismissClaudeBypassPermissionsPromptIfPresentAsync(resolveTarget, captured) {
2536
+ if (process.env.OMX_TEAM_AUTO_ACCEPT_BYPASS === '0')
2537
+ return false;
2538
+ if (!paneHasClaudeBypassPermissionsPrompt(captured))
2539
+ return false;
2540
+ const literalTarget = await resolveTarget();
2541
+ if (!literalTarget)
2542
+ return false;
2543
+ const literalSend = await runTmuxAsync(['send-keys', '-t', literalTarget, '-l', '--', '2']);
2544
+ if (!literalSend.ok)
2545
+ return false;
2546
+ await sleep(120);
2547
+ const submitTarget = await resolveTarget();
2548
+ if (!submitTarget)
2549
+ return false;
2550
+ const submitSend = await runTmuxAsync(['send-keys', '-t', submitTarget, 'C-m']);
2551
+ return submitSend.ok;
1568
2552
  }
1569
2553
  export const paneHasActiveTask = sharedPaneHasActiveTask;
1570
2554
  export function classifyWorkerStartupInjectSafety(captured) {
@@ -1581,8 +2565,8 @@ export function classifyWorkerStartupInjectSafety(captured) {
1581
2565
  return 'safe';
1582
2566
  }
1583
2567
  export async function checkWorkerStartupInjectSafety(sessionName, workerIndex, workerPaneId) {
1584
- const target = paneTarget(sessionName, workerIndex, workerPaneId);
1585
- const visibleCapture = await captureVisiblePaneAsync(target);
2568
+ const resolveTarget = () => resolveWorkerPaneTargetAsync(sessionName, workerIndex, workerPaneId);
2569
+ const visibleCapture = await captureVisiblePaneAsync(resolveTarget);
1586
2570
  const visibleSafety = classifyWorkerStartupInjectSafety(visibleCapture);
1587
2571
  if (visibleSafety === 'safe')
1588
2572
  return { safe: true, reason: 'safe' };
@@ -1591,7 +2575,7 @@ export async function checkWorkerStartupInjectSafety(sessionName, workerIndex, w
1591
2575
  if (!sharedPaneShowsCodexViewport(visibleCapture)) {
1592
2576
  return { safe: false, reason: visibleSafety };
1593
2577
  }
1594
- const scrollbackCapture = await capturePaneAsync(target);
2578
+ const scrollbackCapture = await capturePaneAsync(resolveTarget);
1595
2579
  const scrollbackSafety = classifyWorkerStartupInjectSafety(scrollbackCapture);
1596
2580
  return scrollbackSafety === 'safe'
1597
2581
  ? { safe: true, reason: 'safe' }
@@ -1669,8 +2653,9 @@ export function shouldAttemptAdaptiveRetry(strategy, paneBusyAtStart, allowAdapt
1669
2653
  return false;
1670
2654
  return true;
1671
2655
  }
1672
- function sendLiteralTextOrThrow(target, text) {
1673
- const send = runTmux(['send-keys', '-t', target, '-l', '--', text]);
2656
+ async function sendLiteralTextOrThrow(resolveTarget, text) {
2657
+ const target = await requireAsyncPaneTarget(resolveTarget);
2658
+ const send = await runTmuxAsync(['send-keys', '-t', target, '-l', '--', text]);
1674
2659
  if (!send.ok) {
1675
2660
  throw new Error(`sendToWorker: failed to send text: ${send.stderr}`);
1676
2661
  }
@@ -1682,18 +2667,18 @@ function paneHasQueuedCodexSubmission(captured) {
1682
2667
  return /messages to be submitted after next tool call/i.test(normalized)
1683
2668
  || /press esc to interrupt and send immediately/i.test(normalized);
1684
2669
  }
1685
- async function attemptSubmitRounds(target, text, rounds, queueFirstRound, submitKeyPressesPerRound) {
2670
+ async function attemptSubmitRounds(resolveTarget, text, rounds, queueFirstRound, submitKeyPressesPerRound) {
1686
2671
  const presses = Math.max(1, Math.floor(submitKeyPressesPerRound));
1687
2672
  for (let round = 0; round < rounds; round++) {
1688
2673
  await sleep(100);
1689
2674
  if (round === 0 && queueFirstRound) {
1690
- await sendKeyAsync(target, 'Tab');
2675
+ await sendKeyAsync(resolveTarget, 'Tab');
1691
2676
  await sleep(80);
1692
- await sendKeyAsync(target, 'C-m');
2677
+ await sendKeyAsync(resolveTarget, 'C-m');
1693
2678
  }
1694
2679
  else {
1695
2680
  for (let press = 0; press < presses; press++) {
1696
- await sendKeyAsync(target, 'C-m');
2681
+ await sendKeyAsync(resolveTarget, 'C-m');
1697
2682
  if (press < presses - 1) {
1698
2683
  await sleep(200);
1699
2684
  }
@@ -1701,8 +2686,8 @@ async function attemptSubmitRounds(target, text, rounds, queueFirstRound, submit
1701
2686
  }
1702
2687
  await sleep(140);
1703
2688
  const [captured, visibleCapture] = await Promise.all([
1704
- capturePaneAsync(target),
1705
- captureVisiblePaneAsync(target),
2689
+ capturePaneAsync(resolveTarget),
2690
+ captureVisiblePaneAsync(resolveTarget),
1706
2691
  ]);
1707
2692
  const normalizedCapture = normalizeWorkerTriggerForDraftMatch(captured);
1708
2693
  if (!normalizedCapture.includes(normalizeWorkerTriggerForDraftMatch(text))
@@ -1713,26 +2698,34 @@ async function attemptSubmitRounds(target, text, rounds, queueFirstRound, submit
1713
2698
  }
1714
2699
  return false;
1715
2700
  }
1716
- export function waitForWorkerReady(sessionName, workerIndex, timeoutMs = 30_000, workerPaneId) {
2701
+ export function waitForWorkerReady(sessionName, workerIndex, timeoutMs = 30_000, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
1717
2702
  const initialBackoffMs = 150;
1718
2703
  const maxBackoffMs = 8000;
1719
2704
  const startedAt = Date.now();
1720
2705
  let blockedByTrustPrompt = false;
1721
2706
  let promptDismissed = false;
2707
+ const resolveTarget = createPinnedWorkerPaneTargetResolverSync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
1722
2708
  const sendRobustEnter = () => {
1723
- const target = paneTarget(sessionName, workerIndex, workerPaneId);
1724
2709
  // Trust + follow-up splash can require two submits in Codex TUI.
1725
2710
  // Use C-m (carriage return) for raw-mode compatibility.
1726
- runTmux(['send-keys', '-t', target, 'C-m']);
2711
+ const firstTarget = resolveTarget();
2712
+ if (!firstTarget)
2713
+ return;
2714
+ runTmux(['send-keys', '-t', firstTarget, 'C-m']);
1727
2715
  sleepFractionalSeconds(0.12);
1728
- runTmux(['send-keys', '-t', target, 'C-m']);
2716
+ const secondTarget = resolveTarget();
2717
+ if (!secondTarget)
2718
+ return;
2719
+ runTmux(['send-keys', '-t', secondTarget, 'C-m']);
1729
2720
  };
1730
2721
  const check = () => {
1731
- const target = paneTarget(sessionName, workerIndex, workerPaneId);
2722
+ const target = resolveTarget();
2723
+ if (!target)
2724
+ return false;
1732
2725
  const result = runTmux(sharedBuildVisibleCapturePaneArgv(target));
1733
2726
  if (!result.ok)
1734
2727
  return false;
1735
- if (dismissClaudeBypassPermissionsPromptIfPresent(target, result.stdout)) {
2728
+ if (dismissClaudeBypassPermissionsPromptIfPresent(resolveTarget, result.stdout)) {
1736
2729
  promptDismissed = true;
1737
2730
  return false;
1738
2731
  }
@@ -1757,7 +2750,10 @@ export function waitForWorkerReady(sessionName, workerIndex, timeoutMs = 30_000,
1757
2750
  // scrollback for the prompt/helper text that may have slipped below the fold.
1758
2751
  if (!sharedPaneShowsCodexViewport(result.stdout))
1759
2752
  return false;
1760
- const scrollbackResult = runTmux(sharedBuildCapturePaneArgv(target, 80));
2753
+ const scrollbackTarget = resolveTarget();
2754
+ if (!scrollbackTarget)
2755
+ return false;
2756
+ const scrollbackResult = runTmux(sharedBuildCapturePaneArgv(scrollbackTarget, 80));
1761
2757
  if (!scrollbackResult.ok)
1762
2758
  return false;
1763
2759
  return paneLooksReady(scrollbackResult.stdout);
@@ -1785,26 +2781,41 @@ export function waitForWorkerReady(sessionName, workerIndex, timeoutMs = 30_000,
1785
2781
  // Async twin of waitForWorkerReady for team startup fan-out. Keep the readiness
1786
2782
  // semantics mirrored with the synchronous helper above, but yield between polls
1787
2783
  // so one slow worker pane cannot block later workers' startup attempts.
1788
- export async function waitForWorkerReadyAsync(sessionName, workerIndex, timeoutMs = 30_000, workerPaneId) {
2784
+ export async function waitForWorkerReadyDetailedAsync(sessionName, workerIndex, timeoutMs = 30_000, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId, incompatibility) {
1789
2785
  const initialBackoffMs = 150;
1790
2786
  const maxBackoffMs = 8000;
1791
2787
  const startedAt = Date.now();
1792
2788
  let blockedByTrustPrompt = false;
1793
2789
  let promptDismissed = false;
2790
+ let startupIncompatible = false;
2791
+ const resolveTarget = createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
1794
2792
  const sendRobustEnter = async () => {
1795
- const target = paneTarget(sessionName, workerIndex, workerPaneId);
1796
2793
  // Trust + follow-up splash can require two submits in Codex TUI.
1797
2794
  // Use C-m (carriage return) for raw-mode compatibility.
1798
- await runTmuxAsync(['send-keys', '-t', target, 'C-m']);
2795
+ const firstTarget = await resolveTarget();
2796
+ if (!firstTarget)
2797
+ return;
2798
+ await runTmuxAsync(['send-keys', '-t', firstTarget, 'C-m']);
1799
2799
  await sleep(120);
1800
- await runTmuxAsync(['send-keys', '-t', target, 'C-m']);
2800
+ const secondTarget = await resolveTarget();
2801
+ if (!secondTarget)
2802
+ return;
2803
+ await runTmuxAsync(['send-keys', '-t', secondTarget, 'C-m']);
1801
2804
  };
1802
2805
  const check = async () => {
1803
- const target = paneTarget(sessionName, workerIndex, workerPaneId);
2806
+ const target = await resolveTarget();
2807
+ if (!target)
2808
+ return false;
1804
2809
  const result = await runTmuxAsync(sharedBuildVisibleCapturePaneArgv(target));
1805
2810
  if (!result.ok)
1806
2811
  return false;
1807
- if (dismissClaudeBypassPermissionsPromptIfPresent(target, result.stdout)) {
2812
+ const incompatibleReason = classifyCodexBypassMdmIncompatibility(result.stdout, incompatibility?.workerCli, incompatibility?.finalArgs);
2813
+ if (incompatibleReason) {
2814
+ startupIncompatible = true;
2815
+ incompatibility?.onIncompatible(incompatibleReason);
2816
+ return false;
2817
+ }
2818
+ if (await dismissClaudeBypassPermissionsPromptIfPresentAsync(resolveTarget, result.stdout)) {
1808
2819
  promptDismissed = true;
1809
2820
  return false;
1810
2821
  }
@@ -1829,7 +2840,10 @@ export async function waitForWorkerReadyAsync(sessionName, workerIndex, timeoutM
1829
2840
  // scrollback for the prompt/helper text that may have slipped below the fold.
1830
2841
  if (!sharedPaneShowsCodexViewport(result.stdout))
1831
2842
  return false;
1832
- const scrollbackResult = await runTmuxAsync(sharedBuildCapturePaneArgv(target, 80));
2843
+ const scrollbackTarget = await resolveTarget();
2844
+ if (!scrollbackTarget)
2845
+ return false;
2846
+ const scrollbackResult = await runTmuxAsync(sharedBuildCapturePaneArgv(scrollbackTarget, 80));
1833
2847
  if (!scrollbackResult.ok)
1834
2848
  return false;
1835
2849
  return paneLooksReady(scrollbackResult.stdout);
@@ -1838,6 +2852,8 @@ export async function waitForWorkerReadyAsync(sessionName, workerIndex, timeoutM
1838
2852
  while (Date.now() - startedAt < timeoutMs) {
1839
2853
  if (await check())
1840
2854
  return true;
2855
+ if (startupIncompatible)
2856
+ return false;
1841
2857
  if (blockedByTrustPrompt)
1842
2858
  return false;
1843
2859
  // After dismissing a trust prompt, reset backoff so we re-check quickly
@@ -1854,26 +2870,38 @@ export async function waitForWorkerReadyAsync(sessionName, workerIndex, timeoutM
1854
2870
  }
1855
2871
  return false;
1856
2872
  }
2873
+ export async function waitForWorkerReadyAsync(sessionName, workerIndex, timeoutMs = 30_000, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
2874
+ return waitForWorkerReadyDetailedAsync(sessionName, workerIndex, timeoutMs, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
2875
+ }
1857
2876
  /**
1858
2877
  * Detect and auto-dismiss a Codex "Trust this directory?" prompt in a worker pane.
1859
2878
  * Returns true if a trust prompt was found and dismissed, false otherwise.
1860
2879
  * Opt-out: set OMX_TEAM_AUTO_TRUST=0 to disable auto-dismissal.
1861
2880
  */
1862
- export function dismissTrustPromptIfPresent(sessionName, workerIndex, workerPaneId) {
2881
+ export function dismissTrustPromptIfPresent(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
1863
2882
  if (process.env.OMX_TEAM_AUTO_TRUST === '0')
1864
2883
  return false;
1865
2884
  if (!isTmuxAvailable())
1866
2885
  return false;
1867
- const target = paneTarget(sessionName, workerIndex, workerPaneId);
1868
- const result = runTmux(sharedBuildVisibleCapturePaneArgv(target));
2886
+ const resolveTarget = createPinnedWorkerPaneTargetResolverSync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
2887
+ const captureTarget = resolveTarget();
2888
+ if (!captureTarget)
2889
+ return false;
2890
+ const result = runTmux(sharedBuildVisibleCapturePaneArgv(captureTarget));
1869
2891
  if (!result.ok)
1870
2892
  return false;
1871
2893
  if (!paneHasTrustPrompt(result.stdout))
1872
2894
  return false;
1873
2895
  // Trust prompt detected; send C-m twice to dismiss (trust + follow-up splash)
1874
- runTmux(['send-keys', '-t', target, 'C-m']);
2896
+ const firstTarget = resolveTarget();
2897
+ if (!firstTarget)
2898
+ return false;
2899
+ runTmux(['send-keys', '-t', firstTarget, 'C-m']);
1875
2900
  sleepFractionalSeconds(0.12);
1876
- runTmux(['send-keys', '-t', target, 'C-m']);
2901
+ const secondTarget = resolveTarget();
2902
+ if (!secondTarget)
2903
+ return false;
2904
+ runTmux(['send-keys', '-t', secondTarget, 'C-m']);
1877
2905
  return true;
1878
2906
  }
1879
2907
  export const normalizeTmuxCapture = sharedNormalizeTmuxCapture;
@@ -1904,49 +2932,49 @@ export function sendToWorkerStdin(stdin, text) {
1904
2932
  // Send SHORT text (<200 chars) to worker via tmux send-keys
1905
2933
  // Validates: text < 200 chars, no injection marker
1906
2934
  // Throws on violation
1907
- export async function sendToWorker(sessionName, workerIndex, text, workerPaneId, workerCli) {
2935
+ export async function sendToWorker(sessionName, workerIndex, text, workerPaneId, workerCli, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
1908
2936
  assertWorkerTriggerText(text);
1909
- const target = paneTarget(sessionName, workerIndex, workerPaneId);
2937
+ const resolveTarget = createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
1910
2938
  const strategy = resolveSendStrategyFromEnv();
1911
2939
  const resolvedWorkerCli = resolveWorkerCliForSend(workerIndex, workerCli);
1912
2940
  // Guard: if the trust prompt is still present, advance it first so our trigger text
1913
2941
  // doesn't get typed into the trust screen and ignored.
1914
- const capturedStr = await capturePaneAsync(target);
2942
+ const capturedStr = await capturePaneAsync(resolveTarget);
1915
2943
  const paneBusy = paneHasActiveTask(capturedStr);
1916
- if (dismissClaudeBypassPermissionsPromptIfPresent(target, capturedStr)) {
2944
+ if (await dismissClaudeBypassPermissionsPromptIfPresentAsync(resolveTarget, capturedStr)) {
1917
2945
  await sleep(200);
1918
2946
  }
1919
2947
  if (paneHasTrustPrompt(capturedStr)) {
1920
- await sendKeyAsync(target, 'C-m');
2948
+ await sendKeyAsync(resolveTarget, 'C-m');
1921
2949
  await sleep(120);
1922
- await sendKeyAsync(target, 'C-m');
2950
+ await sendKeyAsync(resolveTarget, 'C-m');
1923
2951
  await sleep(200);
1924
2952
  }
1925
- sendLiteralTextOrThrow(target, text);
2953
+ await sendLiteralTextOrThrow(resolveTarget, text);
1926
2954
  // Allow the input buffer to settle before sending C-m
1927
2955
  await sleep(150);
1928
2956
  const allowAutoInterruptRetry = process.env[OMX_TEAM_AUTO_INTERRUPT_RETRY_ENV] !== '0';
1929
2957
  const submitPlan = buildWorkerSubmitPlan(strategy, resolvedWorkerCli, paneBusy, allowAutoInterruptRetry);
1930
2958
  if (submitPlan.shouldInterrupt) {
1931
2959
  // Explicit interrupt mode: abort current turn first, then submit the new command.
1932
- await sendKeyAsync(target, 'C-c');
2960
+ await sendKeyAsync(resolveTarget, 'C-c');
1933
2961
  await sleep(100);
1934
2962
  }
1935
2963
  // Submit deterministically using CLI-specific plan:
1936
2964
  // - Codex: queue-first Tab+C-m when configured/busy, then double C-m rounds.
1937
2965
  // - Claude: direct C-m rounds only (never queue-first Tab).
1938
- if (await attemptSubmitRounds(target, text, submitPlan.rounds, submitPlan.queueFirstRound, submitPlan.submitKeyPressesPerRound))
2966
+ if (await attemptSubmitRounds(resolveTarget, text, submitPlan.rounds, submitPlan.queueFirstRound, submitPlan.submitKeyPressesPerRound))
1939
2967
  return;
1940
2968
  // Adaptive escalation for "likely unsent trigger text at ready prompt" cases:
1941
2969
  // clear line, re-send trigger, then re-submit with deterministic C-m rounds.
1942
- const latestCapture = await capturePaneAsync(target);
2970
+ const latestCapture = await capturePaneAsync(resolveTarget);
1943
2971
  if (shouldAttemptAdaptiveRetry(strategy, paneBusy, submitPlan.allowAdaptiveRetry, latestCapture || null, text)) {
1944
2972
  // Keep this branch non-interrupting to avoid canceling active turns on false positives.
1945
- await sendKeyAsync(target, 'C-u');
2973
+ await sendKeyAsync(resolveTarget, 'C-u');
1946
2974
  await sleep(80);
1947
- sendLiteralTextOrThrow(target, text);
2975
+ await sendLiteralTextOrThrow(resolveTarget, text);
1948
2976
  await sleep(120);
1949
- if (await attemptSubmitRounds(target, text, 4, false, submitPlan.submitKeyPressesPerRound))
2977
+ if (await attemptSubmitRounds(resolveTarget, text, 4, false, submitPlan.submitKeyPressesPerRound))
1950
2978
  return;
1951
2979
  }
1952
2980
  // Fail-open by default: Codex may keep the last submitted line visible even after executing it.
@@ -1956,15 +2984,15 @@ export async function sendToWorker(sessionName, workerIndex, text, workerPaneId,
1956
2984
  throw new Error('sendToWorker: submit_failed (trigger text still visible after retries)');
1957
2985
  }
1958
2986
  // One last best-effort double C-m nudge, then verify.
1959
- await sendKeyAsync(target, 'C-m');
2987
+ await sendKeyAsync(resolveTarget, 'C-m');
1960
2988
  await sleep(120);
1961
- await sendKeyAsync(target, 'C-m');
2989
+ await sendKeyAsync(resolveTarget, 'C-m');
1962
2990
  // Post-submit verification: wait briefly and confirm the worker consumed the
1963
2991
  // trigger (draft disappeared or active-task indicator appeared). Fixes #391.
1964
2992
  await sleep(300);
1965
2993
  const [verifyCapture, verifyVisibleCapture] = await Promise.all([
1966
- capturePaneAsync(target),
1967
- captureVisiblePaneAsync(target),
2994
+ capturePaneAsync(resolveTarget),
2995
+ captureVisiblePaneAsync(resolveTarget),
1968
2996
  ]);
1969
2997
  if (verifyCapture) {
1970
2998
  if (paneHasActiveTask(verifyCapture))
@@ -1974,14 +3002,14 @@ export async function sendToWorker(sessionName, workerIndex, text, workerPaneId,
1974
3002
  return;
1975
3003
  }
1976
3004
  // Draft still visible and no active task — one more C-m attempt.
1977
- await sendKeyAsync(target, 'C-m');
3005
+ await sendKeyAsync(resolveTarget, 'C-m');
1978
3006
  await sleep(150);
1979
- await sendKeyAsync(target, 'C-m');
1980
- const finalVisibleCapture = await captureVisiblePaneAsync(target);
3007
+ await sendKeyAsync(resolveTarget, 'C-m');
3008
+ const finalVisibleCapture = await captureVisiblePaneAsync(resolveTarget);
1981
3009
  if (paneHasQueuedCodexSubmission(finalVisibleCapture)) {
1982
3010
  throw new Error('sendToWorker: submit_queued_after_tool_call');
1983
3011
  }
1984
- const finalCapture = await capturePaneAsync(target);
3012
+ const finalCapture = await capturePaneAsync(resolveTarget);
1985
3013
  if (normalizeWorkerTriggerForDraftMatch(finalCapture).includes(normalizeWorkerTriggerForDraftMatch(text))
1986
3014
  && !paneHasActiveTask(finalCapture)
1987
3015
  && paneLooksReady(finalCapture)) {
@@ -2000,8 +3028,12 @@ export function notifyLeaderStatus(sessionName, message) {
2000
3028
  return result.ok;
2001
3029
  }
2002
3030
  // Get PID of the shell process in a worker's tmux pane
2003
- export function getWorkerPanePid(sessionName, workerIndex, workerPaneId) {
2004
- const result = runTmux(['list-panes', '-t', paneTarget(sessionName, workerIndex, workerPaneId), '-F', '#{pane_pid}']);
3031
+ export function getWorkerPanePid(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
3032
+ if (hasExplicitWorkerPaneId(workerPaneId)) {
3033
+ const proof = requireExactWorkerPaneLivenessIdentity(workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
3034
+ return proof.status === 'live' ? proof.pid : null;
3035
+ }
3036
+ const result = runTmux(['list-panes', '-t', paneTarget(sessionName, workerIndex), '-F', '#{pane_pid}']);
2005
3037
  if (!result.ok)
2006
3038
  return null;
2007
3039
  const firstLine = result.stdout.split('\n')[0]?.trim();
@@ -2013,15 +3045,25 @@ export function getWorkerPanePid(sessionName, workerIndex, workerPaneId) {
2013
3045
  return pid;
2014
3046
  }
2015
3047
  // Check if worker's tmux pane has a running process
2016
- export function isWorkerAlive(sessionName, workerIndex, workerPaneId) {
2017
- if (workerPaneId?.startsWith('%')) {
2018
- const paneStatus = readPaneLivenessById(workerPaneId);
2019
- if (paneStatus !== null)
2020
- return paneStatus;
3048
+ export function isWorkerAlive(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
3049
+ if (hasExplicitWorkerPaneId(workerPaneId)) {
3050
+ if (hudPaneId?.trim() && workerPaneId === hudPaneId.trim()) {
3051
+ throw new Error(`tmux worker pane is HUD target: ${workerPaneId}`);
3052
+ }
3053
+ const proof = requireExactWorkerPaneLivenessIdentity(workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
3054
+ if (proof.status === 'unavailable')
3055
+ return true;
3056
+ if (proof.status === 'gone')
3057
+ return false;
3058
+ if (typeof expectedPanePid !== 'number' || !Number.isSafeInteger(expectedPanePid) || expectedPanePid <= 0)
3059
+ return true;
3060
+ if (typeof expectedTeamOwnerId !== 'string' || expectedTeamOwnerId.trim() === '')
3061
+ return true;
3062
+ return probeProcessLiveness(proof.pid) !== 'gone';
2021
3063
  }
2022
3064
  const result = runTmux([
2023
3065
  'list-panes',
2024
- '-t', paneTarget(sessionName, workerIndex, workerPaneId),
3066
+ '-t', paneTarget(sessionName, workerIndex),
2025
3067
  '-F',
2026
3068
  '#{pane_dead} #{pane_pid}',
2027
3069
  ]);
@@ -2039,23 +3081,17 @@ export function isWorkerAlive(sessionName, workerIndex, workerPaneId) {
2039
3081
  return false;
2040
3082
  if (!Number.isFinite(pid))
2041
3083
  return false;
2042
- try {
2043
- process.kill(pid, 0);
2044
- return true;
2045
- }
2046
- catch {
2047
- return false;
2048
- }
3084
+ // Unknown is conservatively live so callers cannot classify a permission or
3085
+ // transient failure as stale cleanup authority.
3086
+ return probeProcessLiveness(pid) !== 'gone';
2049
3087
  }
2050
- export function isWorkerPaneOpen(sessionName, workerIndex, workerPaneId) {
2051
- if (workerPaneId?.startsWith('%')) {
2052
- const paneStatus = readPaneLivenessById(workerPaneId);
2053
- if (paneStatus !== null)
2054
- return paneStatus;
3088
+ export function isWorkerPaneOpen(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
3089
+ if (hasExplicitWorkerPaneId(workerPaneId)) {
3090
+ return createPinnedWorkerPaneTargetResolverSync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId)() !== null;
2055
3091
  }
2056
3092
  const result = runTmux([
2057
3093
  'list-panes',
2058
- '-t', paneTarget(sessionName, workerIndex, workerPaneId),
3094
+ '-t', paneTarget(sessionName, workerIndex),
2059
3095
  '-F',
2060
3096
  '#{pane_dead}',
2061
3097
  ]);
@@ -2068,28 +3104,69 @@ export function isWorkerPaneOpen(sessionName, workerIndex, workerPaneId) {
2068
3104
  }
2069
3105
  // Kill a specific worker: send C-c, then C-d, then kill-pane if still alive.
2070
3106
  // leaderPaneId: when provided, the kill is skipped entirely if workerPaneId matches it.
2071
- export async function killWorker(sessionName, workerIndex, workerPaneId, leaderPaneId) {
3107
+ export async function killWorker(sessionName, workerIndex, workerPaneId, leaderPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
2072
3108
  // Guard: never kill the leader's own pane.
2073
3109
  if (leaderPaneId && workerPaneId === leaderPaneId)
2074
3110
  return;
2075
- await runTmuxAsync(['send-keys', '-t', paneTarget(sessionName, workerIndex, workerPaneId), 'C-c']);
3111
+ const resolveTarget = createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
3112
+ const initialTarget = await resolveTarget();
3113
+ if (!initialTarget)
3114
+ return;
3115
+ if (await isWorkerAliveAsync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId, resolveTarget) !== 'alive')
3116
+ return;
3117
+ await runTmuxAsync(['send-keys', '-t', initialTarget, 'C-c']);
2076
3118
  await sleep(1000);
2077
- if (await isWorkerAliveAsync(sessionName, workerIndex, workerPaneId)) {
2078
- await runTmuxAsync(['send-keys', '-t', paneTarget(sessionName, workerIndex, workerPaneId), 'C-d']);
2079
- await sleep(1000);
2080
- }
2081
- if (await isWorkerAliveAsync(sessionName, workerIndex, workerPaneId)) {
2082
- await runTmuxAsync(['kill-pane', '-t', paneTarget(sessionName, workerIndex, workerPaneId)]);
3119
+ if (await isWorkerAliveAsync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId, resolveTarget) === 'alive') {
3120
+ const exitTarget = await resolveTarget();
3121
+ if (exitTarget) {
3122
+ await runTmuxAsync(['send-keys', '-t', exitTarget, 'C-d']);
3123
+ await sleep(1000);
3124
+ }
2083
3125
  }
2084
- }
2085
- // leaderPaneId: when provided, the kill is skipped if workerPaneId matches it.
2086
- export function killWorkerByPaneId(workerPaneId, leaderPaneId) {
2087
- if (!workerPaneId.startsWith('%'))
3126
+ if (await isWorkerAliveAsync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId, resolveTarget) === 'alive') {
3127
+ const killTarget = await resolveTarget();
3128
+ if (!killTarget)
3129
+ return;
3130
+ const killed = await runTmuxAsync(['kill-pane', '-t', killTarget]);
3131
+ if (!killed.ok)
3132
+ throw new Error(`failed to kill tmux pane ${killTarget}: ${killed.stderr}`);
3133
+ const absence = await readExactPaneProof(killTarget);
3134
+ if (absence.status === 'unavailable')
3135
+ throw new ExactPaneProofUnavailableError(absence);
3136
+ if (absence.status !== 'gone')
3137
+ throw new Error(`tmux pane remains live after kill: ${killTarget}`);
3138
+ }
3139
+ }
3140
+ // Explicit pane targets require frozen PID, canonical Team owner, and HUD
3141
+ // exclusion. A successful kill is only accepted after a fresh absence proof.
3142
+ export function killWorkerByPaneId(workerPaneId, expectedPanePid, leaderPaneId, expectedTeamOwnerId, hudPaneId) {
3143
+ const expectedOwner = typeof expectedTeamOwnerId === 'string' ? expectedTeamOwnerId.trim() : '';
3144
+ if (!hasExplicitWorkerPaneId(workerPaneId)
3145
+ || typeof expectedPanePid !== 'number'
3146
+ || !Number.isSafeInteger(expectedPanePid)
3147
+ || expectedPanePid <= 0
3148
+ || !expectedOwner
3149
+ || workerPaneId === hudPaneId)
2088
3150
  return;
2089
- // Guard: never kill the leader's own pane.
2090
3151
  if (leaderPaneId && workerPaneId === leaderPaneId)
2091
3152
  return;
2092
- runTmux(['kill-pane', '-t', workerPaneId]);
3153
+ const proof = readExactPaneProofSync(workerPaneId);
3154
+ if (proof.status !== 'live' || proof.pid !== expectedPanePid)
3155
+ return;
3156
+ const owner = readPaneTeamOwnerTagResult(proof.paneId);
3157
+ if (owner.status !== 'value' || owner.value !== expectedOwner)
3158
+ return;
3159
+ const finalProof = readExactPaneProofSync(proof.paneId);
3160
+ if (finalProof.status !== 'live' || finalProof.pid !== expectedPanePid)
3161
+ return;
3162
+ const result = runTmux(['kill-pane', '-t', finalProof.paneId]);
3163
+ if (!result.ok)
3164
+ throw new Error(`failed to kill tmux pane ${finalProof.paneId}: ${result.stderr}`);
3165
+ const absence = readExactPaneProofSync(finalProof.paneId);
3166
+ if (absence.status === 'unavailable')
3167
+ throw new ExactPaneProofUnavailableError(absence);
3168
+ if (absence.status !== 'gone')
3169
+ throw new Error(`tmux pane remains live after kill: ${finalProof.paneId}`);
2093
3170
  }
2094
3171
  export function paneHasOmxInstanceTag(paneId, instanceId) {
2095
3172
  const normalizedPaneId = normalizePaneTarget(paneId);
@@ -2140,13 +3217,34 @@ export function readPaneTeamOwnerTagResult(paneId) {
2140
3217
  return { status: 'missing' };
2141
3218
  return { status: 'error', error: stderr || `tmux show-option exited ${result.status ?? 'unknown'}` };
2142
3219
  }
2143
- export async function killWorkerByPaneIdAsync(workerPaneId, leaderPaneId) {
2144
- if (!workerPaneId.startsWith('%'))
3220
+ export async function killWorkerByPaneIdAsync(workerPaneId, expectedPanePid, leaderPaneId, expectedTeamOwnerId, hudPaneId) {
3221
+ const expectedOwner = typeof expectedTeamOwnerId === 'string' ? expectedTeamOwnerId.trim() : '';
3222
+ if (!hasExplicitWorkerPaneId(workerPaneId)
3223
+ || typeof expectedPanePid !== 'number'
3224
+ || !Number.isSafeInteger(expectedPanePid)
3225
+ || expectedPanePid <= 0
3226
+ || !expectedOwner
3227
+ || workerPaneId === hudPaneId)
2145
3228
  return;
2146
- // Guard: never kill the leader's own pane.
2147
3229
  if (leaderPaneId && workerPaneId === leaderPaneId)
2148
3230
  return;
2149
- await runTmuxAsync(['kill-pane', '-t', workerPaneId]);
3231
+ const proof = await readExactPaneProof(workerPaneId);
3232
+ if (proof.status !== 'live' || proof.pid !== expectedPanePid)
3233
+ return;
3234
+ const owner = readPaneTeamOwnerTagResult(proof.paneId);
3235
+ if (owner.status !== 'value' || owner.value !== expectedOwner)
3236
+ return;
3237
+ const finalProof = await readExactPaneProof(proof.paneId);
3238
+ if (finalProof.status !== 'live' || finalProof.pid !== expectedPanePid)
3239
+ return;
3240
+ const result = await runTmuxAsync(['kill-pane', '-t', finalProof.paneId]);
3241
+ if (!result.ok)
3242
+ throw new Error(`failed to kill tmux pane ${finalProof.paneId}: ${result.stderr}`);
3243
+ const absence = await readExactPaneProof(finalProof.paneId);
3244
+ if (absence.status === 'unavailable')
3245
+ throw new ExactPaneProofUnavailableError(absence);
3246
+ if (absence.status !== 'gone')
3247
+ throw new Error(`tmux pane remains live after kill: ${finalProof.paneId}`);
2150
3248
  }
2151
3249
  function normalizePaneTarget(value) {
2152
3250
  if (typeof value !== 'string')
@@ -2184,7 +3282,10 @@ function normalizePaneTargets(paneIds, options = {}) {
2184
3282
  return { killablePaneIds, excluded };
2185
3283
  }
2186
3284
  export function resolveSharedSessionShutdownTopology(sessionName, preferredLeaderPaneId, teamName) {
2187
- const panes = listPanes(sessionName);
3285
+ const paneList = listPanesResult(sessionName);
3286
+ if (paneList.error !== null)
3287
+ return { status: 'unavailable', detail: paneList.error };
3288
+ const panes = paneList.panes;
2188
3289
  const livePaneIds = panes
2189
3290
  .map((pane) => normalizePaneTarget(pane.paneId))
2190
3291
  .filter((paneId) => Boolean(paneId));
@@ -2192,6 +3293,7 @@ export function resolveSharedSessionShutdownTopology(sessionName, preferredLeade
2192
3293
  if (panes.length === 0) {
2193
3294
  return {
2194
3295
  livePaneIds,
3296
+ status: 'available',
2195
3297
  teamWorkerPaneIds: [],
2196
3298
  leaderPaneId: fallbackLeaderPaneId,
2197
3299
  hudPaneIds: [],
@@ -2222,6 +3324,7 @@ export function resolveSharedSessionShutdownTopology(sessionName, preferredLeade
2222
3324
  : [];
2223
3325
  return {
2224
3326
  livePaneIds,
3327
+ status: 'available',
2225
3328
  teamWorkerPaneIds: normalizedTeamWorkerPaneIds,
2226
3329
  leaderPaneId: resolvedLeaderPaneId,
2227
3330
  hudPaneIds,
@@ -2336,7 +3439,8 @@ function escapeRegExp(value) {
2336
3439
  }
2337
3440
  /**
2338
3441
  * Shared pane-id-direct teardown primitive for worker pane cleanup.
2339
- * Must remain liveness-agnostic: do not gate on isWorkerAlive/killWorker.
3442
+ * A fresh exact global proof is required before each kill; proven-gone panes
3443
+ * are teardown-compatible while unavailable proofs remain fail-closed.
2340
3444
  */
2341
3445
  export async function teardownWorkerPanes(paneIds, options = {}) {
2342
3446
  const { killablePaneIds, excluded } = normalizePaneTargets(paneIds, options);
@@ -2347,45 +3451,247 @@ export async function teardownWorkerPanes(paneIds, options = {}) {
2347
3451
  const summary = {
2348
3452
  attemptedPaneIds: killablePaneIds,
2349
3453
  excluded,
3454
+ provenGonePaneIds: [],
3455
+ killedPaneIds: [],
3456
+ proofUnavailable: [],
2350
3457
  kill: {
2351
- attempted: killablePaneIds.length,
3458
+ attempted: 0,
2352
3459
  succeeded: 0,
2353
3460
  failed: 0,
3461
+ failedPaneIds: [],
2354
3462
  },
2355
3463
  };
2356
3464
  for (const paneId of killablePaneIds) {
2357
- const result = await runTmuxAsync(['kill-pane', '-t', paneId]);
2358
- if (result.ok)
3465
+ const proof = await readExactPaneProof(paneId);
3466
+ const expectedPid = options.expectedPanePids?.[paneId];
3467
+ if (proof.status === 'gone') {
3468
+ summary.provenGonePaneIds.push(proof.paneId);
3469
+ continue;
3470
+ }
3471
+ if (proof.status === 'unavailable') {
3472
+ summary.proofUnavailable.push(proof);
3473
+ break;
3474
+ }
3475
+ if (typeof expectedPid !== 'number' || !Number.isSafeInteger(expectedPid) || expectedPid <= 0) {
3476
+ summary.proofUnavailable.push({
3477
+ status: 'unavailable',
3478
+ paneId,
3479
+ reason: 'pane_pid_changed',
3480
+ detail: 'expected positive pane PID is required before teardown kill',
3481
+ });
3482
+ break;
3483
+ }
3484
+ if (proof.pid !== expectedPid) {
3485
+ summary.proofUnavailable.push({
3486
+ status: 'unavailable',
3487
+ paneId,
3488
+ reason: 'pane_pid_changed',
3489
+ detail: `expected ${expectedPid}, got ${proof.pid}`,
3490
+ });
3491
+ break;
3492
+ }
3493
+ if (options.authorizePaneKill && !options.authorizePaneKill(proof.paneId, proof)) {
3494
+ summary.proofUnavailable.push({
3495
+ status: 'unavailable',
3496
+ paneId: proof.paneId,
3497
+ reason: 'pane_pid_changed',
3498
+ detail: 'pane owner authorization changed',
3499
+ });
3500
+ break;
3501
+ }
3502
+ // Owner authorization can read tmux. Re-prove the persisted identity after
3503
+ // it and immediately before kill-pane.
3504
+ const finalProof = await readExactPaneProof(proof.paneId);
3505
+ if (finalProof.status === 'gone') {
3506
+ summary.provenGonePaneIds.push(finalProof.paneId);
3507
+ continue;
3508
+ }
3509
+ if (finalProof.status === 'unavailable') {
3510
+ summary.proofUnavailable.push(finalProof);
3511
+ break;
3512
+ }
3513
+ if (finalProof.pid !== expectedPid) {
3514
+ summary.proofUnavailable.push({
3515
+ status: 'unavailable',
3516
+ paneId: finalProof.paneId,
3517
+ reason: 'pane_pid_changed',
3518
+ detail: `expected ${expectedPid}, got ${finalProof.pid}`,
3519
+ });
3520
+ break;
3521
+ }
3522
+ summary.kill.attempted += 1;
3523
+ const result = await runTmuxAsync(['kill-pane', '-t', finalProof.paneId]);
3524
+ if (!result.ok) {
3525
+ summary.kill.failed += 1;
3526
+ summary.kill.failedPaneIds.push(proof.paneId);
3527
+ await sleep(perPaneGrace);
3528
+ break;
3529
+ }
3530
+ const afterKill = await readExactPaneProof(finalProof.paneId);
3531
+ if (afterKill.status === 'gone') {
2359
3532
  summary.kill.succeeded += 1;
2360
- else
3533
+ summary.killedPaneIds.push(proof.paneId);
3534
+ }
3535
+ else if (afterKill.status === 'unavailable') {
3536
+ summary.proofUnavailable.push(afterKill);
3537
+ break;
3538
+ }
3539
+ else {
2361
3540
  summary.kill.failed += 1;
3541
+ summary.kill.failedPaneIds.push(proof.paneId);
3542
+ await sleep(perPaneGrace);
3543
+ break;
3544
+ }
2362
3545
  await sleep(perPaneGrace);
2363
3546
  }
2364
3547
  return summary;
2365
3548
  }
2366
- export async function killWorkerPanes(paneIds, leaderPaneId, graceMs = 2000, hudPaneId) {
2367
- return teardownWorkerPanes(paneIds, { leaderPaneId, hudPaneId: hudPaneId ?? null, graceMs });
2368
- }
2369
- // Kill entire tmux session. Tolerates already-dead sessions.
2370
- export function destroyTeamSession(sessionName) {
2371
- try {
2372
- runTmux(['kill-session', '-t', sessionName]);
2373
- }
2374
- catch {
2375
- // tolerate
2376
- }
3549
+ export async function killWorkerPanes(paneIds, leaderPaneId, graceMs = 2000, hudPaneId, expectedPanePids) {
3550
+ return teardownWorkerPanes(paneIds, {
3551
+ leaderPaneId,
3552
+ hudPaneId: hudPaneId ?? null,
3553
+ graceMs,
3554
+ expectedPanePids,
3555
+ });
2377
3556
  }
2378
- // List all tmux sessions matching omx-team-* pattern
2379
- export function listTeamSessions() {
2380
- const result = runTmux(['list-sessions', '-F', '#{session_name}']);
2381
- if (!result.ok)
2382
- return [];
2383
- return result.stdout
3557
+ // Kill an entire detached tmux session only when the caller has already
3558
+ // established ownership. Success requires tmux to accept the kill and a fresh
3559
+ // session-list proof that the exact base session is absent.
3560
+ function parseTeamSessionNames(stdout) {
3561
+ return stdout
2384
3562
  .split('\n')
2385
3563
  .map(line => line.trim())
2386
3564
  .filter(Boolean)
2387
3565
  .map(baseSessionName);
2388
3566
  }
3567
+ function isFinalTmuxServerGone(result) {
3568
+ return !result.ok && /^no server running on .+$/i.test(result.stderr);
3569
+ }
3570
+ /**
3571
+ * Reads pane and session incarnation in one tmux snapshot. This prevents a
3572
+ * same-name replacement from combining a stale session proof with a live pane.
3573
+ */
3574
+ export function queryDetachedSessionLeaderBinding(paneId, pid, sessionName, expected) {
3575
+ const result = runTmuxStructured(['list-panes', '-a', '-F', '#{pane_id}\t#{pane_dead}\t#{pane_pid}\t#{session_name}\t#{session_id}\t#{session_created}']);
3576
+ if (!result.ok)
3577
+ return null;
3578
+ const seenPaneIds = new Set();
3579
+ let binding = null;
3580
+ for (const line of result.stdout.split('\n').filter(Boolean)) {
3581
+ const fields = line.split('\t');
3582
+ if (fields.length !== 6)
3583
+ return null;
3584
+ const [currentPaneId, dead, panePid, currentSessionName, sessionId, sessionCreated] = fields;
3585
+ if (!/^%[0-9]+$/.test(currentPaneId) || seenPaneIds.has(currentPaneId) || (dead !== '0' && dead !== '1')
3586
+ || !/^[0-9]+$/.test(panePid) || !Number.isSafeInteger(Number(panePid)) || Number(panePid) <= 0
3587
+ || !currentSessionName || !/^\$[0-9]+$/.test(sessionId) || !/^[0-9]+$/.test(sessionCreated))
3588
+ return null;
3589
+ seenPaneIds.add(currentPaneId);
3590
+ if (currentPaneId !== paneId)
3591
+ continue;
3592
+ if (dead !== '0' || Number(panePid) !== pid || currentSessionName !== sessionName
3593
+ || sessionId !== expected.sessionId || sessionCreated !== expected.sessionCreated)
3594
+ return null;
3595
+ binding = { paneId: currentPaneId, pid: Number(panePid), sessionName: currentSessionName, incarnation: { sessionId, sessionCreated } };
3596
+ }
3597
+ return binding;
3598
+ }
3599
+ function parseDetachedSessionIncarnation(stdout, sessionName) {
3600
+ const baseName = baseSessionName(sessionName);
3601
+ const matches = [];
3602
+ for (const line of stdout.split('\n').filter(Boolean)) {
3603
+ const fields = line.split('\t');
3604
+ if (fields.length !== 3)
3605
+ return 'malformed';
3606
+ const [name, sessionId, sessionCreated] = fields;
3607
+ if (!name || !sessionId || !sessionCreated || !/^\$[0-9]+$/.test(sessionId) || !/^[0-9]+$/.test(sessionCreated))
3608
+ return 'malformed';
3609
+ if (baseSessionName(name) === baseName)
3610
+ matches.push({ sessionId, sessionCreated });
3611
+ }
3612
+ return matches.length === 0 ? null : matches.length === 1 ? matches[0] : 'malformed';
3613
+ }
3614
+ /**
3615
+ * Reads stable tmux incarnation evidence. Name-only presence is never effect
3616
+ * authority: malformed or unavailable evidence fails closed, and an expected
3617
+ * incarnation distinguishes an exact survivor from a recycled same-name session.
3618
+ */
3619
+ export function queryDetachedTeamSession(sessionName, expected) {
3620
+ const sessions = runTmuxStructured(['list-sessions', '-F', '#{session_name}\t#{session_id}\t#{session_created}']);
3621
+ if (!sessions.ok) {
3622
+ return isFinalTmuxServerGone(sessions)
3623
+ ? { status: 'absent' }
3624
+ : { status: 'unavailable', detail: sessions.stderr };
3625
+ }
3626
+ const incarnation = parseDetachedSessionIncarnation(sessions.stdout, sessionName);
3627
+ if (incarnation === 'malformed')
3628
+ return { status: 'unavailable', detail: 'malformed_session_incarnation' };
3629
+ if (incarnation === null)
3630
+ return { status: 'absent' };
3631
+ if (expected && (incarnation.sessionId !== expected.sessionId || incarnation.sessionCreated !== expected.sessionCreated)) {
3632
+ return { status: 'replacement', incarnation };
3633
+ }
3634
+ return { status: 'exact', incarnation };
3635
+ }
3636
+ /**
3637
+ * Re-proves every stable authorization component at the destructive sink. The
3638
+ * final tmux command queues an in-server predicate with the kill, so no client
3639
+ * can replace the session or pane between authorization and the effect.
3640
+ */
3641
+ function buildDetachedSessionDestroyArgs(expected) {
3642
+ const leaderPaneId = expected.leaderPaneId.trim();
3643
+ const ownerId = expected.ownerId.trim();
3644
+ if (!/^%[0-9]+$/.test(leaderPaneId)
3645
+ || !Number.isSafeInteger(expected.leaderPanePid)
3646
+ || expected.leaderPanePid <= 0
3647
+ || !/^\$[0-9]+$/.test(expected.sessionId)
3648
+ || !/^[0-9]+$/.test(expected.sessionCreated)
3649
+ || !/^[A-Za-z0-9._:@/-]+$/.test(ownerId))
3650
+ return null;
3651
+ const predicate = [
3652
+ '#{==:#{pane_dead},0}',
3653
+ `#{==:#{pane_id},${leaderPaneId}}`,
3654
+ `#{==:#{pane_pid},${expected.leaderPanePid}}`,
3655
+ `#{==:#{${OMX_TEAM_PANE_OWNER_OPTION}},${ownerId}}`,
3656
+ `#{==:#{session_id},${expected.sessionId}}`,
3657
+ `#{==:#{session_created},${expected.sessionCreated}}`,
3658
+ ].reduce((combined, condition) => `#{&&:${combined},${condition}}`);
3659
+ return ['if-shell', '-F', '-t', leaderPaneId, predicate, `kill-session -t ${expected.sessionId}`, 'run-shell "exit 1"'];
3660
+ }
3661
+ export function requestDetachedTeamSessionDestroy(sessionName, expected) {
3662
+ const incarnation = { sessionId: expected.sessionId, sessionCreated: expected.sessionCreated };
3663
+ if (queryDetachedTeamSession(sessionName, incarnation).status !== 'exact')
3664
+ return false;
3665
+ if (!queryDetachedSessionLeaderBinding(expected.leaderPaneId, expected.leaderPanePid, sessionName, incarnation))
3666
+ return false;
3667
+ const pane = readExactPaneProofSync(expected.leaderPaneId);
3668
+ if (pane.status !== 'live' || pane.pid !== expected.leaderPanePid)
3669
+ return false;
3670
+ const owner = readPaneTeamOwnerTagResult(expected.leaderPaneId);
3671
+ if (owner.status !== 'value' || owner.value !== expected.ownerId)
3672
+ return false;
3673
+ const args = buildDetachedSessionDestroyArgs(expected);
3674
+ return args !== null && runTmux(args).ok;
3675
+ }
3676
+ export function destroyTeamSession(sessionName) {
3677
+ const result = runTmux(['kill-session', '-t', sessionName]);
3678
+ if (!result.ok)
3679
+ return false;
3680
+ const sessions = runTmux(['list-sessions', '-F', '#{session_name}']);
3681
+ // Killing tmux's final session also terminates its server. Its documented
3682
+ // no-server response is the only post-kill query failure that proves absence.
3683
+ if (!sessions.ok)
3684
+ return isFinalTmuxServerGone(sessions);
3685
+ return !parseTeamSessionNames(sessions.stdout).includes(baseSessionName(sessionName));
3686
+ }
3687
+ // A failed query is not a successful empty session list. Destructive callers
3688
+ // must require the latter before treating a detached session as absent.
3689
+ export function listTeamSessions() {
3690
+ const result = runTmux(['list-sessions', '-F', '#{session_name}']);
3691
+ if (!result.ok)
3692
+ return null;
3693
+ return parseTeamSessionNames(result.stdout);
3694
+ }
2389
3695
  /**
2390
3696
  * Notify the leader through durable mailbox state only.
2391
3697
  *