oh-my-codex 0.20.1 → 0.20.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (791) hide show
  1. package/Cargo.lock +7 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +36 -1
  4. package/crates/omx-runtime/Cargo.toml +1 -0
  5. package/crates/omx-runtime/src/main.rs +25 -2
  6. package/crates/omx-runtime/tests/execution.rs +219 -0
  7. package/crates/omx-runtime-core/src/dispatch.rs +96 -10
  8. package/crates/omx-runtime-core/src/engine.rs +839 -29
  9. package/crates/omx-runtime-core/src/lib.rs +8 -0
  10. package/dist/adapt/__tests__/hermes.test.js.map +1 -1
  11. package/dist/adapt/contracts.d.ts.map +1 -1
  12. package/dist/agents/__tests__/definitions.test.js +81 -0
  13. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  14. package/dist/agents/__tests__/native-config.test.js +25 -0
  15. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  16. package/dist/agents/native-config.d.ts +2 -1
  17. package/dist/agents/native-config.d.ts.map +1 -1
  18. package/dist/agents/native-config.js.map +1 -1
  19. package/dist/auth/__tests__/hotswap.test.d.ts +2 -0
  20. package/dist/auth/__tests__/hotswap.test.d.ts.map +1 -0
  21. package/dist/auth/__tests__/hotswap.test.js +183 -0
  22. package/dist/auth/__tests__/hotswap.test.js.map +1 -0
  23. package/dist/auth/hotswap.d.ts +6 -0
  24. package/dist/auth/hotswap.d.ts.map +1 -1
  25. package/dist/auth/hotswap.js +102 -19
  26. package/dist/auth/hotswap.js.map +1 -1
  27. package/dist/auth/paths.js.map +1 -1
  28. package/dist/auth/storage.js.map +1 -1
  29. package/dist/autopilot/__tests__/ralplan-gate.test.js +5 -5
  30. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  31. package/dist/autopilot/completion-gate.js.map +1 -1
  32. package/dist/autopilot/fsm.d.ts +1 -1
  33. package/dist/autopilot/fsm.d.ts.map +1 -1
  34. package/dist/autoresearch/goal.js.map +1 -1
  35. package/dist/autoresearch/runtime.js.map +1 -1
  36. package/dist/capabilities/lockfile.js.map +1 -1
  37. package/dist/catalog/reader.js.map +1 -1
  38. package/dist/catalog/skill-mirror.js.map +1 -1
  39. package/dist/cli/__tests__/adapt-help.test.js.map +1 -1
  40. package/dist/cli/__tests__/agents-init.test.js.map +1 -1
  41. package/dist/cli/__tests__/agents.test.js.map +1 -1
  42. package/dist/cli/__tests__/api.test.js.map +1 -1
  43. package/dist/cli/__tests__/ask.test.js.map +1 -1
  44. package/dist/cli/__tests__/auth.test.js +60 -6
  45. package/dist/cli/__tests__/auth.test.js.map +1 -1
  46. package/dist/cli/__tests__/autoresearch-guided.test.js.map +1 -1
  47. package/dist/cli/__tests__/autoresearch.test.js.map +1 -1
  48. package/dist/cli/__tests__/capabilities.test.js.map +1 -1
  49. package/dist/cli/__tests__/codex-plugin-layout.test.js +80 -0
  50. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  51. package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -1
  52. package/dist/cli/__tests__/doctor-invalid-config.test.js +47 -4
  53. package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
  54. package/dist/cli/__tests__/doctor-team.test.js.map +1 -1
  55. package/dist/cli/__tests__/doctor-warning-copy.test.js +752 -15
  56. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  57. package/dist/cli/__tests__/exec.test.js +136 -0
  58. package/dist/cli/__tests__/exec.test.js.map +1 -1
  59. package/dist/cli/__tests__/imagegen-continuation.test.js.map +1 -1
  60. package/dist/cli/__tests__/index.test.js +473 -14
  61. package/dist/cli/__tests__/index.test.js.map +1 -1
  62. package/dist/cli/__tests__/launch-fallback.test.js +336 -4
  63. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  64. package/dist/cli/__tests__/mcp-parity.test.js +87 -12
  65. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
  66. package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts +2 -0
  67. package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts.map +1 -0
  68. package/dist/cli/__tests__/native-hook-claim-journal.test.js +206 -0
  69. package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -0
  70. package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
  71. package/dist/cli/__tests__/question.test.js.map +1 -1
  72. package/dist/cli/__tests__/ralph-deslop-contract.test.js.map +1 -1
  73. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +6 -0
  74. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  75. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -1
  76. package/dist/cli/__tests__/ralph-prd-smoke.test.js.map +1 -1
  77. package/dist/cli/__tests__/ralph.test.js +8 -3
  78. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  79. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts +2 -0
  80. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts.map +1 -0
  81. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +72 -0
  82. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -0
  83. package/dist/cli/__tests__/ralplan.test.d.ts +2 -0
  84. package/dist/cli/__tests__/ralplan.test.d.ts.map +1 -0
  85. package/dist/cli/__tests__/ralplan.test.js +55 -0
  86. package/dist/cli/__tests__/ralplan.test.js.map +1 -0
  87. package/dist/cli/__tests__/resume.test.js +17 -11
  88. package/dist/cli/__tests__/resume.test.js.map +1 -1
  89. package/dist/cli/__tests__/session-scoped-runtime.test.js +25 -0
  90. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  91. package/dist/cli/__tests__/session-search-help.test.js +25 -1
  92. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  93. package/dist/cli/__tests__/session-search.test.js.map +1 -1
  94. package/dist/cli/__tests__/setup-agents-overwrite.test.js +79 -0
  95. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  96. package/dist/cli/__tests__/setup-gh-star.test.js.map +1 -1
  97. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +77 -3
  98. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
  99. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts +2 -0
  100. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts.map +1 -0
  101. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js +285 -0
  102. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js.map +1 -0
  103. package/dist/cli/__tests__/setup-install-mode.test.js +2579 -294
  104. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  105. package/dist/cli/__tests__/setup-preferences.test.d.ts +2 -0
  106. package/dist/cli/__tests__/setup-preferences.test.d.ts.map +1 -0
  107. package/dist/cli/__tests__/setup-preferences.test.js +79 -0
  108. package/dist/cli/__tests__/setup-preferences.test.js.map +1 -0
  109. package/dist/cli/__tests__/setup-refresh.test.js +33 -2
  110. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  111. package/dist/cli/__tests__/setup-scope.test.js +46 -3
  112. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  113. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  114. package/dist/cli/__tests__/state.test.js +37 -0
  115. package/dist/cli/__tests__/state.test.js.map +1 -1
  116. package/dist/cli/__tests__/team.test.js +105 -7
  117. package/dist/cli/__tests__/team.test.js.map +1 -1
  118. package/dist/cli/__tests__/ultragoal.test.js +61 -1
  119. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  120. package/dist/cli/__tests__/uninstall.test.js +1817 -24
  121. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  122. package/dist/cli/__tests__/update.test.js +58 -0
  123. package/dist/cli/__tests__/update.test.js.map +1 -1
  124. package/dist/cli/__tests__/url.test.js.map +1 -1
  125. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +2 -1
  126. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  127. package/dist/cli/adapt.js.map +1 -1
  128. package/dist/cli/agents-init.js.map +1 -1
  129. package/dist/cli/agents.js.map +1 -1
  130. package/dist/cli/api.js.map +1 -1
  131. package/dist/cli/ask.d.ts +1 -1
  132. package/dist/cli/ask.d.ts.map +1 -1
  133. package/dist/cli/ask.js.map +1 -1
  134. package/dist/cli/auth.js.map +1 -1
  135. package/dist/cli/autoresearch-guided.js.map +1 -1
  136. package/dist/cli/autoresearch-intake.js.map +1 -1
  137. package/dist/cli/capabilities.js.map +1 -1
  138. package/dist/cli/cleanup.js.map +1 -1
  139. package/dist/cli/codex-feature-probe.js.map +1 -1
  140. package/dist/cli/codex-home.js.map +1 -1
  141. package/dist/cli/doctor.d.ts +21 -0
  142. package/dist/cli/doctor.d.ts.map +1 -1
  143. package/dist/cli/doctor.js +607 -130
  144. package/dist/cli/doctor.js.map +1 -1
  145. package/dist/cli/explore.js.map +1 -1
  146. package/dist/cli/index.d.ts +14 -3
  147. package/dist/cli/index.d.ts.map +1 -1
  148. package/dist/cli/index.js +245 -57
  149. package/dist/cli/index.js.map +1 -1
  150. package/dist/cli/mcp-parity.js +2 -2
  151. package/dist/cli/mcp-parity.js.map +1 -1
  152. package/dist/cli/mcp-serve.js.map +1 -1
  153. package/dist/cli/mission.js.map +1 -1
  154. package/dist/cli/native-assets.js.map +1 -1
  155. package/dist/cli/native-hook-claim-journal.d.ts +29 -0
  156. package/dist/cli/native-hook-claim-journal.d.ts.map +1 -0
  157. package/dist/cli/native-hook-claim-journal.js +252 -0
  158. package/dist/cli/native-hook-claim-journal.js.map +1 -0
  159. package/dist/cli/omx.js.map +1 -1
  160. package/dist/cli/plugin-marketplace.js.map +1 -1
  161. package/dist/cli/project-runtime-codex-homes.js.map +1 -1
  162. package/dist/cli/question.js.map +1 -1
  163. package/dist/cli/ralph.d.ts.map +1 -1
  164. package/dist/cli/ralph.js +3 -2
  165. package/dist/cli/ralph.js.map +1 -1
  166. package/dist/cli/ralplan.d.ts +10 -0
  167. package/dist/cli/ralplan.d.ts.map +1 -0
  168. package/dist/cli/ralplan.js +110 -0
  169. package/dist/cli/ralplan.js.map +1 -0
  170. package/dist/cli/session-search.d.ts +5 -0
  171. package/dist/cli/session-search.d.ts.map +1 -1
  172. package/dist/cli/session-search.js +87 -0
  173. package/dist/cli/session-search.js.map +1 -1
  174. package/dist/cli/setup-preferences.d.ts +11 -0
  175. package/dist/cli/setup-preferences.d.ts.map +1 -1
  176. package/dist/cli/setup-preferences.js +32 -0
  177. package/dist/cli/setup-preferences.js.map +1 -1
  178. package/dist/cli/setup.d.ts +58 -0
  179. package/dist/cli/setup.d.ts.map +1 -1
  180. package/dist/cli/setup.js +1450 -398
  181. package/dist/cli/setup.js.map +1 -1
  182. package/dist/cli/sparkshell.js.map +1 -1
  183. package/dist/cli/star-prompt.js.map +1 -1
  184. package/dist/cli/state.d.ts.map +1 -1
  185. package/dist/cli/state.js +2 -1
  186. package/dist/cli/state.js.map +1 -1
  187. package/dist/cli/team.js.map +1 -1
  188. package/dist/cli/ultragoal.d.ts +5 -1
  189. package/dist/cli/ultragoal.d.ts.map +1 -1
  190. package/dist/cli/ultragoal.js +34 -2
  191. package/dist/cli/ultragoal.js.map +1 -1
  192. package/dist/cli/uninstall.d.ts +17 -0
  193. package/dist/cli/uninstall.d.ts.map +1 -1
  194. package/dist/cli/uninstall.js +986 -95
  195. package/dist/cli/uninstall.js.map +1 -1
  196. package/dist/cli/update.d.ts.map +1 -1
  197. package/dist/cli/update.js +8 -1
  198. package/dist/cli/update.js.map +1 -1
  199. package/dist/cli/url.js.map +1 -1
  200. package/dist/compat/__tests__/doctor-contract.test.js +5 -1
  201. package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
  202. package/dist/compat/__tests__/rust-runtime-compat.test.js +1 -1
  203. package/dist/compat/__tests__/rust-runtime-compat.test.js.map +1 -1
  204. package/dist/config/__tests__/codex-hooks.test.js +1287 -11
  205. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  206. package/dist/config/__tests__/generator-idempotent.test.js +1179 -32
  207. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  208. package/dist/config/__tests__/generator-notify.test.js +6 -1
  209. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  210. package/dist/config/__tests__/models.test.js +67 -3
  211. package/dist/config/__tests__/models.test.js.map +1 -1
  212. package/dist/config/codex-feature-flags.d.ts.map +1 -1
  213. package/dist/config/codex-feature-flags.js.map +1 -1
  214. package/dist/config/codex-hooks.d.ts +124 -1
  215. package/dist/config/codex-hooks.d.ts.map +1 -1
  216. package/dist/config/codex-hooks.js +2142 -265
  217. package/dist/config/codex-hooks.js.map +1 -1
  218. package/dist/config/commit-lore-guard.js.map +1 -1
  219. package/dist/config/generator.d.ts +23 -34
  220. package/dist/config/generator.d.ts.map +1 -1
  221. package/dist/config/generator.js +1480 -246
  222. package/dist/config/generator.js.map +1 -1
  223. package/dist/config/mcp-registry.js.map +1 -1
  224. package/dist/config/models.d.ts +14 -4
  225. package/dist/config/models.d.ts.map +1 -1
  226. package/dist/config/models.js +15 -1
  227. package/dist/config/models.js.map +1 -1
  228. package/dist/config/omx-first-party-mcp.js.map +1 -1
  229. package/dist/config/team-mode.d.ts.map +1 -1
  230. package/dist/document-refresh/__tests__/enforcer.test.js.map +1 -1
  231. package/dist/document-refresh/enforcer.js.map +1 -1
  232. package/dist/exec/followup.js.map +1 -1
  233. package/dist/goal-workflows/artifacts.js.map +1 -1
  234. package/dist/hooks/__tests__/agents-overlay.test.js +7 -4
  235. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  236. package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +1 -1
  237. package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +1 -1
  238. package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
  239. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +9 -3
  240. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  241. package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
  242. package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
  243. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  244. package/dist/hooks/__tests__/design-skill.test.js.map +1 -1
  245. package/dist/hooks/__tests__/keyword-detector.test.js +1376 -6
  246. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  247. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +252 -543
  248. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  249. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +38 -4
  250. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  251. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +765 -10
  252. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  253. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +2 -0
  254. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  255. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +142 -1
  256. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
  257. package/dist/hooks/__tests__/notify-hook-modules.test.js.map +1 -1
  258. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js +48 -2
  259. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js.map +1 -1
  260. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +61 -44
  261. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
  262. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
  263. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js +3 -2
  264. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js.map +1 -1
  265. package/dist/hooks/__tests__/notify-hook-session-scope.test.js +243 -4
  266. package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
  267. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +531 -12
  268. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  269. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +163 -24
  270. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  271. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +482 -32
  272. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  273. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +68 -1
  274. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  275. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +7 -1
  276. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
  277. package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js.map +1 -1
  278. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +43 -4
  279. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  280. package/dist/hooks/__tests__/openclaw-setup-contract.test.js.map +1 -1
  281. package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +1 -1
  282. package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +1 -1
  283. package/dist/hooks/__tests__/prompt-guidance-fragments.test.js.map +1 -1
  284. package/dist/hooks/__tests__/prompt-guidance-test-helpers.js.map +1 -1
  285. package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts +2 -0
  286. package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts.map +1 -0
  287. package/dist/hooks/__tests__/prompt-session-provenance.test.js +82 -0
  288. package/dist/hooks/__tests__/prompt-session-provenance.test.js.map +1 -0
  289. package/dist/hooks/__tests__/session.test.js +878 -11
  290. package/dist/hooks/__tests__/session.test.js.map +1 -1
  291. package/dist/hooks/__tests__/skill-catalog-hygiene.test.js.map +1 -1
  292. package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js.map +1 -1
  293. package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -1
  294. package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +1 -1
  295. package/dist/hooks/agents-overlay.js +5 -5
  296. package/dist/hooks/agents-overlay.js.map +1 -1
  297. package/dist/hooks/code-simplifier/index.js.map +1 -1
  298. package/dist/hooks/explore-routing.js.map +1 -1
  299. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  300. package/dist/hooks/extensibility/events.js.map +1 -1
  301. package/dist/hooks/extensibility/loader.js.map +1 -1
  302. package/dist/hooks/extensibility/plugin-runner-stdin.js.map +1 -1
  303. package/dist/hooks/extensibility/sdk/logging.js.map +1 -1
  304. package/dist/hooks/keyword-detector.d.ts +33 -7
  305. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  306. package/dist/hooks/keyword-detector.js +2228 -119
  307. package/dist/hooks/keyword-detector.js.map +1 -1
  308. package/dist/hooks/keyword-registry.d.ts +12 -0
  309. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  310. package/dist/hooks/keyword-registry.js +41 -0
  311. package/dist/hooks/keyword-registry.js.map +1 -1
  312. package/dist/hooks/prompt-session-provenance.d.ts +87 -0
  313. package/dist/hooks/prompt-session-provenance.d.ts.map +1 -0
  314. package/dist/hooks/prompt-session-provenance.js +180 -0
  315. package/dist/hooks/prompt-session-provenance.js.map +1 -0
  316. package/dist/hooks/session.d.ts +163 -34
  317. package/dist/hooks/session.d.ts.map +1 -1
  318. package/dist/hooks/session.js +1395 -317
  319. package/dist/hooks/session.js.map +1 -1
  320. package/dist/hooks/task-size-detector.js.map +1 -1
  321. package/dist/hud/__tests__/state.test.js.map +1 -1
  322. package/dist/hud/authority.js.map +1 -1
  323. package/dist/hud/constants.js.map +1 -1
  324. package/dist/hud/index.js.map +1 -1
  325. package/dist/hud/reconcile.js.map +1 -1
  326. package/dist/hud/render.js.map +1 -1
  327. package/dist/hud/state.js.map +1 -1
  328. package/dist/hud/tmux.js.map +1 -1
  329. package/dist/leader/__tests__/contract.test.js +153 -1
  330. package/dist/leader/__tests__/contract.test.js.map +1 -1
  331. package/dist/leader/contract.d.ts +42 -5
  332. package/dist/leader/contract.d.ts.map +1 -1
  333. package/dist/leader/contract.js +184 -3
  334. package/dist/leader/contract.js.map +1 -1
  335. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  336. package/dist/mcp/__tests__/code-intel-server.test.js +31 -3
  337. package/dist/mcp/__tests__/code-intel-server.test.js.map +1 -1
  338. package/dist/mcp/__tests__/server-lifecycle.test.js +15 -2
  339. package/dist/mcp/__tests__/server-lifecycle.test.js.map +1 -1
  340. package/dist/mcp/__tests__/state-paths.test.js +98 -1
  341. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  342. package/dist/mcp/__tests__/wiki-server.test.js +18 -0
  343. package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
  344. package/dist/mcp/bootstrap.js.map +1 -1
  345. package/dist/mcp/code-intel-server.d.ts +58 -58
  346. package/dist/mcp/code-intel-server.d.ts.map +1 -1
  347. package/dist/mcp/code-intel-server.js +13 -3
  348. package/dist/mcp/code-intel-server.js.map +1 -1
  349. package/dist/mcp/hermes-bridge.js.map +1 -1
  350. package/dist/mcp/hermes-server.d.ts +70 -70
  351. package/dist/mcp/hermes-server.d.ts.map +1 -1
  352. package/dist/mcp/lifecycle-telemetry.js.map +1 -1
  353. package/dist/mcp/memory-server.d.ts +34 -34
  354. package/dist/mcp/memory-server.d.ts.map +1 -1
  355. package/dist/mcp/memory-server.js.map +1 -1
  356. package/dist/mcp/state-paths.d.ts +14 -0
  357. package/dist/mcp/state-paths.d.ts.map +1 -1
  358. package/dist/mcp/state-paths.js +96 -20
  359. package/dist/mcp/state-paths.js.map +1 -1
  360. package/dist/mcp/state-server.d.ts +47 -47
  361. package/dist/mcp/state-server.d.ts.map +1 -1
  362. package/dist/mcp/trace-server.d.ts +4 -4
  363. package/dist/mcp/trace-server.d.ts.map +1 -1
  364. package/dist/mcp/trace-server.js.map +1 -1
  365. package/dist/mcp/wiki-server.d.ts +34 -23
  366. package/dist/mcp/wiki-server.d.ts.map +1 -1
  367. package/dist/mcp/wiki-server.js +9 -2
  368. package/dist/mcp/wiki-server.js.map +1 -1
  369. package/dist/modes/__tests__/base-session-scope.test.js +53 -1
  370. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  371. package/dist/modes/__tests__/base-tmux-pane.test.js +4 -4
  372. package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
  373. package/dist/modes/base.d.ts.map +1 -1
  374. package/dist/modes/base.js +5 -5
  375. package/dist/modes/base.js.map +1 -1
  376. package/dist/notifications/__tests__/lifecycle-dedupe.test.js.map +1 -1
  377. package/dist/notifications/__tests__/reply-listener.test.js.map +1 -1
  378. package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js +177 -85
  379. package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js.map +1 -1
  380. package/dist/notifications/__tests__/session-registry.test.js.map +1 -1
  381. package/dist/notifications/__tests__/template-engine.test.js.map +1 -1
  382. package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
  383. package/dist/notifications/http-client.js.map +1 -1
  384. package/dist/notifications/index.d.ts +4 -1
  385. package/dist/notifications/index.d.ts.map +1 -1
  386. package/dist/notifications/index.js +3 -9
  387. package/dist/notifications/index.js.map +1 -1
  388. package/dist/notifications/lifecycle-dedupe.js.map +1 -1
  389. package/dist/notifications/reply-listener.d.ts.map +1 -1
  390. package/dist/notifications/reply-listener.js.map +1 -1
  391. package/dist/notifications/session-registry.js.map +1 -1
  392. package/dist/notifications/session-status.js.map +1 -1
  393. package/dist/notifications/temp-contract.d.ts.map +1 -1
  394. package/dist/notifications/temp-contract.js +4 -0
  395. package/dist/notifications/temp-contract.js.map +1 -1
  396. package/dist/notifications/tmux-detector.js.map +1 -1
  397. package/dist/notifications/tmux.js.map +1 -1
  398. package/dist/openclaw/config.js.map +1 -1
  399. package/dist/openclaw/dispatcher.js.map +1 -1
  400. package/dist/pipeline/__tests__/orchestrator.test.js +4 -4
  401. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  402. package/dist/pipeline/__tests__/stages.test.js +16 -13
  403. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  404. package/dist/pipeline/stages/code-review.js.map +1 -1
  405. package/dist/pipeline/stages/ralph-verify.js.map +1 -1
  406. package/dist/pipeline/stages/ralplan.js.map +1 -1
  407. package/dist/pipeline/stages/team-exec.js.map +1 -1
  408. package/dist/pipeline/stages/ultragoal.js.map +1 -1
  409. package/dist/pipeline/stages/ultraqa.js.map +1 -1
  410. package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js.map +1 -1
  411. package/dist/planning/artifact-names.js.map +1 -1
  412. package/dist/planning/artifacts.js.map +1 -1
  413. package/dist/question/__tests__/state.test.js.map +1 -1
  414. package/dist/question/__tests__/ui.test.js.map +1 -1
  415. package/dist/question/autopilot-wait.js.map +1 -1
  416. package/dist/question/client.d.ts.map +1 -1
  417. package/dist/question/client.js.map +1 -1
  418. package/dist/question/deep-interview.js.map +1 -1
  419. package/dist/question/events.js.map +1 -1
  420. package/dist/question/renderer.js.map +1 -1
  421. package/dist/question/state.d.ts.map +1 -1
  422. package/dist/question/state.js.map +1 -1
  423. package/dist/question/ui.js.map +1 -1
  424. package/dist/ralph/contract.d.ts +1 -1
  425. package/dist/ralph/contract.d.ts.map +1 -1
  426. package/dist/ralplan/__tests__/consensus-gate.test.js +527 -8
  427. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
  428. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts +2 -0
  429. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts.map +1 -0
  430. package/dist/ralplan/__tests__/documented-leader-preflight.test.js +46 -0
  431. package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -0
  432. package/dist/ralplan/__tests__/runtime.test.js +116 -14
  433. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  434. package/dist/ralplan/consensus-gate.d.ts +3 -3
  435. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  436. package/dist/ralplan/consensus-gate.js +173 -70
  437. package/dist/ralplan/consensus-gate.js.map +1 -1
  438. package/dist/ralplan/documented-leader-preflight.d.ts +34 -0
  439. package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -0
  440. package/dist/ralplan/documented-leader-preflight.js +58 -0
  441. package/dist/ralplan/documented-leader-preflight.js.map +1 -0
  442. package/dist/ralplan/runtime.d.ts +3 -2
  443. package/dist/ralplan/runtime.d.ts.map +1 -1
  444. package/dist/ralplan/runtime.js +12 -9
  445. package/dist/ralplan/runtime.js.map +1 -1
  446. package/dist/runtime/__tests__/bridge.test.js +42 -1
  447. package/dist/runtime/__tests__/bridge.test.js.map +1 -1
  448. package/dist/runtime/bridge.d.ts +12 -0
  449. package/dist/runtime/bridge.d.ts.map +1 -1
  450. package/dist/runtime/bridge.js +52 -1
  451. package/dist/runtime/bridge.js.map +1 -1
  452. package/dist/runtime/process-tree.js.map +1 -1
  453. package/dist/runtime/run-loop.js.map +1 -1
  454. package/dist/runtime/run-outcome.d.ts +3 -3
  455. package/dist/runtime/run-outcome.d.ts.map +1 -1
  456. package/dist/runtime/run-outcome.js.map +1 -1
  457. package/dist/runtime/run-state.js.map +1 -1
  458. package/dist/scripts/__tests__/codex-native-hook.test.js +6657 -995
  459. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  460. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts +2 -0
  461. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts.map +1 -0
  462. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js +634 -0
  463. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js.map +1 -0
  464. package/dist/scripts/__tests__/generate-release-body.test.js.map +1 -1
  465. package/dist/scripts/__tests__/hook-derived-watcher.test.js.map +1 -1
  466. package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
  467. package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts +2 -0
  468. package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts.map +1 -0
  469. package/dist/scripts/__tests__/notify-fallback-delivery.test.js +174 -0
  470. package/dist/scripts/__tests__/notify-fallback-delivery.test.js.map +1 -0
  471. package/dist/scripts/__tests__/notify-state-io.test.js +57 -1
  472. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  473. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts +2 -0
  474. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts.map +1 -0
  475. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +108 -0
  476. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -0
  477. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts +2 -0
  478. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts.map +1 -0
  479. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +78 -0
  480. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -0
  481. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts +2 -0
  482. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts.map +1 -0
  483. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +55 -0
  484. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -0
  485. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
  486. package/dist/scripts/__tests__/smoke-packed-install.test.js +1100 -4
  487. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  488. package/dist/scripts/__tests__/test-reply-listener-live.test.js.map +1 -1
  489. package/dist/scripts/build-api.js.map +1 -1
  490. package/dist/scripts/build-explore-harness.js.map +1 -1
  491. package/dist/scripts/build-sparkshell.js.map +1 -1
  492. package/dist/scripts/cleanup-explore-harness.js.map +1 -1
  493. package/dist/scripts/codex-execution-surface.js.map +1 -1
  494. package/dist/scripts/codex-native-hook.d.ts +4 -1
  495. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  496. package/dist/scripts/codex-native-hook.js +11577 -1184
  497. package/dist/scripts/codex-native-hook.js.map +1 -1
  498. package/dist/scripts/generate-native-release-manifest.js.map +1 -1
  499. package/dist/scripts/generate-release-body.js.map +1 -1
  500. package/dist/scripts/hook-payload-guard.d.ts.map +1 -1
  501. package/dist/scripts/hook-payload-guard.js.map +1 -1
  502. package/dist/scripts/notify-fallback-delivery.d.ts +48 -0
  503. package/dist/scripts/notify-fallback-delivery.d.ts.map +1 -0
  504. package/dist/scripts/notify-fallback-delivery.js +419 -0
  505. package/dist/scripts/notify-fallback-delivery.js.map +1 -0
  506. package/dist/scripts/notify-fallback-watcher.js +218 -138
  507. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  508. package/dist/scripts/notify-hook/auto-nudge.d.ts +18 -9
  509. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  510. package/dist/scripts/notify-hook/auto-nudge.js +112 -15
  511. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  512. package/dist/scripts/notify-hook/managed-tmux.d.ts +23 -0
  513. package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
  514. package/dist/scripts/notify-hook/managed-tmux.js +122 -12
  515. package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
  516. package/dist/scripts/notify-hook/orchestration-intent.d.ts +7 -7
  517. package/dist/scripts/notify-hook/orchestration-intent.d.ts.map +1 -1
  518. package/dist/scripts/notify-hook/ralph-session-resume.d.ts +5 -2
  519. package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
  520. package/dist/scripts/notify-hook/ralph-session-resume.js +33 -37
  521. package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
  522. package/dist/scripts/notify-hook/state-io.d.ts +17 -0
  523. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  524. package/dist/scripts/notify-hook/state-io.js +73 -11
  525. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  526. package/dist/scripts/notify-hook/team-dispatch.d.ts +10 -1
  527. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  528. package/dist/scripts/notify-hook/team-dispatch.js +234 -79
  529. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  530. package/dist/scripts/notify-hook/team-leader-nudge.d.ts +2 -2
  531. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  532. package/dist/scripts/notify-hook/team-leader-nudge.js +39 -13
  533. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  534. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +16 -8
  535. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  536. package/dist/scripts/notify-hook/team-tmux-guard.js +365 -44
  537. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  538. package/dist/scripts/notify-hook/team-worker-posttooluse.js.map +1 -1
  539. package/dist/scripts/notify-hook/team-worker-stop.d.ts +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 +19 -18
  542. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  543. package/dist/scripts/notify-hook/team-worker.d.ts +5 -2
  544. package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
  545. package/dist/scripts/notify-hook/team-worker.js +37 -28
  546. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  547. package/dist/scripts/notify-hook/tmux-injection.d.ts +8 -1
  548. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  549. package/dist/scripts/notify-hook/tmux-injection.js +53 -18
  550. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  551. package/dist/scripts/notify-hook/visual-verdict.d.ts +1 -1
  552. package/dist/scripts/notify-hook/visual-verdict.d.ts.map +1 -1
  553. package/dist/scripts/notify-hook/visual-verdict.js +15 -13
  554. package/dist/scripts/notify-hook/visual-verdict.js.map +1 -1
  555. package/dist/scripts/notify-hook.d.ts +24 -1
  556. package/dist/scripts/notify-hook.d.ts.map +1 -1
  557. package/dist/scripts/notify-hook.js +267 -90
  558. package/dist/scripts/notify-hook.js.map +1 -1
  559. package/dist/scripts/postinstall.js.map +1 -1
  560. package/dist/scripts/prompt-inventory.js.map +1 -1
  561. package/dist/scripts/smoke-packed-install.d.ts +927 -5
  562. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  563. package/dist/scripts/smoke-packed-install.js +4317 -13
  564. package/dist/scripts/smoke-packed-install.js.map +1 -1
  565. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  566. package/dist/scripts/test-reply-listener-live.js.map +1 -1
  567. package/dist/scripts/test-sparkshell.js.map +1 -1
  568. package/dist/scripts/verify-native-agents.js.map +1 -1
  569. package/dist/session-history/friction.js.map +1 -1
  570. package/dist/session-history/search.js.map +1 -1
  571. package/dist/sidecar/__tests__/boundary.test.js.map +1 -1
  572. package/dist/sidecar/collector.js.map +1 -1
  573. package/dist/sidecar/index.js.map +1 -1
  574. package/dist/sidecar/render.js.map +1 -1
  575. package/dist/sidecar/tmux.js.map +1 -1
  576. package/dist/state/__tests__/mode-state-context.test.js +54 -0
  577. package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
  578. package/dist/state/__tests__/operations.test.js +66 -15
  579. package/dist/state/__tests__/operations.test.js.map +1 -1
  580. package/dist/state/__tests__/skill-active.test.js +19 -1
  581. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  582. package/dist/state/__tests__/workflow-transition.test.js +61 -0
  583. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  584. package/dist/state/mode-state-context.d.ts +5 -0
  585. package/dist/state/mode-state-context.d.ts.map +1 -1
  586. package/dist/state/mode-state-context.js +59 -0
  587. package/dist/state/mode-state-context.js.map +1 -1
  588. package/dist/state/operations.d.ts +1 -1
  589. package/dist/state/operations.d.ts.map +1 -1
  590. package/dist/state/operations.js +49 -55
  591. package/dist/state/operations.js.map +1 -1
  592. package/dist/state/paths.d.ts +1 -1
  593. package/dist/state/paths.d.ts.map +1 -1
  594. package/dist/state/paths.js +1 -1
  595. package/dist/state/paths.js.map +1 -1
  596. package/dist/state/skill-active.d.ts +13 -1
  597. package/dist/state/skill-active.d.ts.map +1 -1
  598. package/dist/state/skill-active.js +43 -4
  599. package/dist/state/skill-active.js.map +1 -1
  600. package/dist/state/terminal-normalization.js.map +1 -1
  601. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  602. package/dist/state/workflow-transition-reconcile.js +20 -4
  603. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  604. package/dist/state/workflow-transition.d.ts +1 -1
  605. package/dist/state/workflow-transition.d.ts.map +1 -1
  606. package/dist/state/workflow-transition.js.map +1 -1
  607. package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts +2 -0
  608. package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts.map +1 -0
  609. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +52 -0
  610. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -0
  611. package/dist/subagents/__tests__/tracker.test.js +1550 -10
  612. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  613. package/dist/subagents/role-routing-marker.d.ts +10 -0
  614. package/dist/subagents/role-routing-marker.d.ts.map +1 -0
  615. package/dist/subagents/role-routing-marker.js +136 -0
  616. package/dist/subagents/role-routing-marker.js.map +1 -0
  617. package/dist/subagents/tracker.d.ts +91 -0
  618. package/dist/subagents/tracker.d.ts.map +1 -1
  619. package/dist/subagents/tracker.js +806 -62
  620. package/dist/subagents/tracker.js.map +1 -1
  621. package/dist/team/__tests__/api-interop.test.js +91 -1
  622. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  623. package/dist/team/__tests__/approved-execution.test.js.map +1 -1
  624. package/dist/team/__tests__/delivery-e2e-smoke.test.js +52 -10
  625. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  626. package/dist/team/__tests__/idle-nudge.test.js +45 -15
  627. package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
  628. package/dist/team/__tests__/mcp-comm.test.js +93 -27
  629. package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
  630. package/dist/team/__tests__/model-contract.test.js +341 -4
  631. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  632. package/dist/team/__tests__/role-router.test.js.map +1 -1
  633. package/dist/team/__tests__/runtime-cli.test.js +9 -0
  634. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  635. package/dist/team/__tests__/runtime.test.js +3624 -355
  636. package/dist/team/__tests__/runtime.test.js.map +1 -1
  637. package/dist/team/__tests__/scaling.test.js +2516 -118
  638. package/dist/team/__tests__/scaling.test.js.map +1 -1
  639. package/dist/team/__tests__/state-root.test.js.map +1 -1
  640. package/dist/team/__tests__/state.test.js +804 -36
  641. package/dist/team/__tests__/state.test.js.map +1 -1
  642. package/dist/team/__tests__/team-ops-contract.test.js +6 -0
  643. package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
  644. package/dist/team/__tests__/tmux-session.test.js +3664 -969
  645. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  646. package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
  647. package/dist/team/__tests__/worker-runtime-identity.test.js +38 -2
  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 +2 -2
  652. package/dist/team/api-interop.d.ts.map +1 -1
  653. package/dist/team/api-interop.js +15 -9
  654. package/dist/team/api-interop.js.map +1 -1
  655. package/dist/team/commit-hygiene.js.map +1 -1
  656. package/dist/team/contracts.d.ts +5 -5
  657. package/dist/team/contracts.d.ts.map +1 -1
  658. package/dist/team/delivery-log.js.map +1 -1
  659. package/dist/team/exact-pane.d.ts +23 -0
  660. package/dist/team/exact-pane.d.ts.map +1 -0
  661. package/dist/team/exact-pane.js +133 -0
  662. package/dist/team/exact-pane.js.map +1 -0
  663. package/dist/team/followup-planner.js.map +1 -1
  664. package/dist/team/goal-workflow.js.map +1 -1
  665. package/dist/team/idle-nudge.d.ts +12 -1
  666. package/dist/team/idle-nudge.d.ts.map +1 -1
  667. package/dist/team/idle-nudge.js +41 -14
  668. package/dist/team/idle-nudge.js.map +1 -1
  669. package/dist/team/model-contract.d.ts +18 -2
  670. package/dist/team/model-contract.d.ts.map +1 -1
  671. package/dist/team/model-contract.js +314 -35
  672. package/dist/team/model-contract.js.map +1 -1
  673. package/dist/team/orchestrator.js.map +1 -1
  674. package/dist/team/pane-status.js.map +1 -1
  675. package/dist/team/phase-controller.js.map +1 -1
  676. package/dist/team/reminder-intents.d.ts +1 -1
  677. package/dist/team/reminder-intents.d.ts.map +1 -1
  678. package/dist/team/reminder-intents.js.map +1 -1
  679. package/dist/team/runtime-cli.js.map +1 -1
  680. package/dist/team/runtime.d.ts +1 -0
  681. package/dist/team/runtime.d.ts.map +1 -1
  682. package/dist/team/runtime.js +1326 -231
  683. package/dist/team/runtime.js.map +1 -1
  684. package/dist/team/scaling.d.ts +22 -8
  685. package/dist/team/scaling.d.ts.map +1 -1
  686. package/dist/team/scaling.js +1463 -376
  687. package/dist/team/scaling.js.map +1 -1
  688. package/dist/team/state/dispatch-lock.js.map +1 -1
  689. package/dist/team/state/dispatch.d.ts.map +1 -1
  690. package/dist/team/state/dispatch.js +26 -8
  691. package/dist/team/state/dispatch.js.map +1 -1
  692. package/dist/team/state/events.js.map +1 -1
  693. package/dist/team/state/locks.d.ts.map +1 -1
  694. package/dist/team/state/locks.js +61 -13
  695. package/dist/team/state/locks.js.map +1 -1
  696. package/dist/team/state/tasks.d.ts.map +1 -1
  697. package/dist/team/state/tasks.js +4 -3
  698. package/dist/team/state/tasks.js.map +1 -1
  699. package/dist/team/state-root.js.map +1 -1
  700. package/dist/team/state.d.ts +67 -1
  701. package/dist/team/state.d.ts.map +1 -1
  702. package/dist/team/state.js +636 -129
  703. package/dist/team/state.js.map +1 -1
  704. package/dist/team/team-identity.d.ts.map +1 -1
  705. package/dist/team/team-identity.js.map +1 -1
  706. package/dist/team/team-ops.d.ts +5 -0
  707. package/dist/team/team-ops.d.ts.map +1 -1
  708. package/dist/team/team-ops.js +4 -0
  709. package/dist/team/team-ops.js.map +1 -1
  710. package/dist/team/tmux-session.d.ts +125 -23
  711. package/dist/team/tmux-session.d.ts.map +1 -1
  712. package/dist/team/tmux-session.js +1340 -333
  713. package/dist/team/tmux-session.js.map +1 -1
  714. package/dist/team/worker-bootstrap.js.map +1 -1
  715. package/dist/team/worktree.d.ts.map +1 -1
  716. package/dist/team/worktree.js +4 -0
  717. package/dist/team/worktree.js.map +1 -1
  718. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  719. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  720. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  721. package/dist/ultragoal/artifacts.js +4 -1
  722. package/dist/ultragoal/artifacts.js.map +1 -1
  723. package/dist/url-reader/index.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 +57 -0
  727. package/dist/utils/__tests__/file-durability.test.js.map +1 -0
  728. package/dist/utils/agents-model-table.js.map +1 -1
  729. package/dist/utils/file-durability.d.ts +15 -0
  730. package/dist/utils/file-durability.d.ts.map +1 -0
  731. package/dist/utils/file-durability.js +37 -0
  732. package/dist/utils/file-durability.js.map +1 -0
  733. package/dist/utils/package.js.map +1 -1
  734. package/dist/utils/paths.js.map +1 -1
  735. package/dist/utils/platform-command.js.map +1 -1
  736. package/dist/utils/repo-deps.js.map +1 -1
  737. package/dist/utils/version.js.map +1 -1
  738. package/dist/verification/__tests__/ci-rust-gates.test.js +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/verifier.js.map +1 -1
  742. package/dist/visual/constants.d.ts +1 -1
  743. package/dist/visual/constants.d.ts.map +1 -1
  744. package/dist/vscode/__tests__/index.test.js.map +1 -1
  745. package/dist/vscode/index.js.map +1 -1
  746. package/dist/wiki/__tests__/cjk-tokenize.test.js.map +1 -1
  747. package/dist/wiki/__tests__/lint.test.js.map +1 -1
  748. package/dist/wiki/__tests__/query.test.js.map +1 -1
  749. package/dist/wiki/__tests__/storage.test.js.map +1 -1
  750. package/dist/wiki/__tests__/test-helpers.d.ts.map +1 -1
  751. package/dist/wiki/lint.d.ts +4 -1
  752. package/dist/wiki/lint.d.ts.map +1 -1
  753. package/dist/wiki/lint.js +3 -2
  754. package/dist/wiki/lint.js.map +1 -1
  755. package/dist/wiki/query.js.map +1 -1
  756. package/dist/wiki/storage.js.map +1 -1
  757. package/package.json +2 -2
  758. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  759. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +24 -0
  760. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +10 -5
  761. package/plugins/oh-my-codex/skills/ralph/SKILL.md +6 -2
  762. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +6 -2
  763. package/plugins/oh-my-codex/skills/team/SKILL.md +13 -9
  764. package/skills/omx-setup/SKILL.md +10 -5
  765. package/skills/ralph/SKILL.md +6 -2
  766. package/skills/ralplan/SKILL.md +6 -2
  767. package/skills/team/SKILL.md +13 -9
  768. package/src/scripts/__tests__/codex-native-hook.test.ts +20048 -10827
  769. package/src/scripts/__tests__/fixtures/issue-3138-installed-smoke.ts +734 -0
  770. package/src/scripts/__tests__/notify-fallback-delivery.test.ts +177 -0
  771. package/src/scripts/__tests__/notify-state-io.test.ts +73 -0
  772. package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +129 -0
  773. package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +78 -0
  774. package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +60 -0
  775. package/src/scripts/__tests__/smoke-packed-install.test.ts +1309 -4
  776. package/src/scripts/codex-native-hook.ts +11429 -1163
  777. package/src/scripts/notify-fallback-delivery.ts +416 -0
  778. package/src/scripts/notify-fallback-watcher.ts +216 -155
  779. package/src/scripts/notify-hook/auto-nudge.ts +113 -14
  780. package/src/scripts/notify-hook/managed-tmux.ts +149 -12
  781. package/src/scripts/notify-hook/ralph-session-resume.ts +37 -36
  782. package/src/scripts/notify-hook/state-io.ts +100 -13
  783. package/src/scripts/notify-hook/team-dispatch.ts +242 -75
  784. package/src/scripts/notify-hook/team-leader-nudge.ts +40 -18
  785. package/src/scripts/notify-hook/team-tmux-guard.ts +366 -39
  786. package/src/scripts/notify-hook/team-worker-stop.ts +20 -16
  787. package/src/scripts/notify-hook/team-worker.ts +39 -26
  788. package/src/scripts/notify-hook/tmux-injection.ts +66 -23
  789. package/src/scripts/notify-hook/visual-verdict.ts +22 -12
  790. package/src/scripts/notify-hook.ts +336 -90
  791. package/src/scripts/smoke-packed-install.ts +5154 -149
@@ -1,16 +1,18 @@
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 { dirname, isAbsolute, join, relative, resolve, sep } from 'path';
6
6
  import { CODEX_BYPASS_FLAG, CLAUDE_SKIP_PERMISSIONS_FLAG, MADMAX_FLAG, CONFIG_FLAG, LONG_CONFIG_FLAG, MODEL_FLAG, } from '../cli/constants.js';
7
7
  import { getAgent } from '../agents/definitions.js';
8
8
  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';
9
9
  import { readActiveProviderEnvOverrides } from '../config/models.js';
10
- import { extractModelProviderOverrideValue } from './model-contract.js';
10
+ import { classifyTeamWorkerLaunchPolicy, extractModelProviderOverrideValue, normalizeTeamWorkerLaunchArgs, parseTeamWorkerLaunchArgs, } from './model-contract.js';
11
11
  import { sleep, sleepSync } from '../utils/sleep.js';
12
12
  import { buildPlatformCommandSpec, classifySpawnError, resolveCommandPathForPlatform, spawnPlatformCommandSync, } from '../utils/platform-command.js';
13
13
  import { resolveOmxCliEntryPath } from '../utils/paths.js';
14
+ import { readExactPaneProof, readExactPaneProofsSync, readExactPaneProofSync } from './exact-pane.js';
15
+ import { resolveCanonicalTeamStateRoot } from './state-root.js';
14
16
  const execFileAsync = promisify(execFile);
15
17
  import { HUD_RESIZE_RECONCILE_DELAY_SECONDS, HUD_TMUX_TEAM_HEIGHT_LINES } from '../hud/constants.js';
16
18
  import { OMX_TMUX_HUD_OWNER_ENV } from '../hud/reconcile.js';
@@ -18,6 +20,41 @@ import { findHudWatchPaneIds, hudPaneMatchesOwner, OMX_TMUX_HUD_LEADER_PANE_ENV
18
20
  const OMX_INSTANCE_OPTION = '@omx_instance_id';
19
21
  const OMX_PANE_INSTANCE_OPTION = '@omx_pane_instance_id';
20
22
  const OMX_TEAM_PANE_OWNER_OPTION = '@omx_team_pane_owner_id';
23
+ /** `kill(pid, 0)` establishes absence only on ESRCH. */
24
+ function probeProcessLiveness(pid) {
25
+ try {
26
+ process.kill(pid, 0);
27
+ return 'alive';
28
+ }
29
+ catch (error) {
30
+ return error.code === 'ESRCH' ? 'gone' : 'unknown';
31
+ }
32
+ }
33
+ /** Carries live tmux resources when creation cannot safely roll them back. */
34
+ export class CreateTeamSessionPartialError extends Error {
35
+ partialSession;
36
+ proofUnavailable;
37
+ originalError;
38
+ cleanupErrors;
39
+ constructor(partialSession, proofUnavailable, originalError,
40
+ /** Cleanup commands that failed after resources were created and must be retried. */
41
+ cleanupErrors = []) {
42
+ super('create_team_session_cleanup_incomplete');
43
+ this.partialSession = partialSession;
44
+ this.proofUnavailable = proofUnavailable;
45
+ this.originalError = originalError;
46
+ this.cleanupErrors = cleanupErrors;
47
+ this.name = 'CreateTeamSessionPartialError';
48
+ }
49
+ }
50
+ class ExactPaneProofUnavailableError extends Error {
51
+ proof;
52
+ constructor(proof) {
53
+ super(`exact_pane_proof_unavailable:${proof.paneId}:${proof.reason}`);
54
+ this.proof = proof;
55
+ this.name = 'ExactPaneProofUnavailableError';
56
+ }
57
+ }
21
58
  const INJECTION_MARKER = '[OMX_TMUX_INJECT]';
22
59
  const MODEL_INSTRUCTIONS_FILE_KEY = 'model_instructions_file';
23
60
  const OMX_BYPASS_DEFAULT_SYSTEM_PROMPT_ENV = 'OMX_BYPASS_DEFAULT_SYSTEM_PROMPT';
@@ -74,6 +111,206 @@ function runTmux(args) {
74
111
  }
75
112
  return { ok: true, stdout: (result.stdout || '').trim() };
76
113
  }
114
+ /** Preserve structured tmux fields, including an empty final field. */
115
+ /** Preserve structured tmux fields, including an empty final field. */
116
+ function runTmuxStructured(args) {
117
+ const { result } = spawnPlatformCommandSync('tmux', args, { encoding: 'utf-8' });
118
+ if (result.error)
119
+ return { ok: false, stderr: result.error.message };
120
+ if (result.status !== 0) {
121
+ return { ok: false, stderr: (result.stderr || '').trim() || `tmux exited ${result.status}` };
122
+ }
123
+ return { ok: true, stdout: (result.stdout || '').replace(/\r?\n$/, '') };
124
+ }
125
+ function restoredHudCleanupDebtPath(cwd, stateRoot) {
126
+ const canonicalStateRoot = resolveCanonicalTeamStateRoot(resolve(cwd));
127
+ if (typeof stateRoot !== 'string' || !stateRoot.trim()) {
128
+ return join(canonicalStateRoot, '.restored-hud-cleanup-debt.json');
129
+ }
130
+ const canonicalTeamsRoot = join(canonicalStateRoot, 'team');
131
+ const root = resolve(stateRoot);
132
+ const relativeRoot = relative(canonicalTeamsRoot, root);
133
+ if (!isAbsolute(stateRoot.trim())
134
+ || !relativeRoot
135
+ || relativeRoot === '..'
136
+ || relativeRoot.startsWith(`..${sep}`)
137
+ || isAbsolute(relativeRoot)
138
+ || relativeRoot.split(sep).length !== 1) {
139
+ throw new Error('restored_hud_cleanup_debt_state_root_invalid');
140
+ }
141
+ for (let candidate = root;; candidate = dirname(candidate)) {
142
+ try {
143
+ const metadata = lstatSync(candidate);
144
+ if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
145
+ throw new Error('restored_hud_cleanup_debt_state_root_invalid');
146
+ }
147
+ }
148
+ catch (error) {
149
+ if (error.code !== 'ENOENT')
150
+ throw error;
151
+ }
152
+ if (candidate === canonicalTeamsRoot)
153
+ break;
154
+ }
155
+ return join(root, '.restored-hud-cleanup-debt.json');
156
+ }
157
+ function syncRestoredHudDebtParentSync(path) {
158
+ let descriptor;
159
+ try {
160
+ descriptor = openSync(dirname(path), 'r');
161
+ fsyncSync(descriptor);
162
+ }
163
+ catch (error) {
164
+ const code = error.code;
165
+ if (!(code === 'EPERM' && process.platform === 'win32'))
166
+ throw error;
167
+ }
168
+ finally {
169
+ if (descriptor !== undefined)
170
+ closeSync(descriptor);
171
+ }
172
+ }
173
+ function persistRestoredHudCleanupDebtSync(cwd, debt, stateRoot) {
174
+ const debtPath = restoredHudCleanupDebtPath(cwd, stateRoot);
175
+ mkdirSync(dirname(debtPath), { recursive: true });
176
+ // Recheck after mkdir: an untrusted Team root must still be canonical and
177
+ // non-symlinked immediately before the durable file mutation.
178
+ restoredHudCleanupDebtPath(cwd, stateRoot);
179
+ const temporaryPath = `${debtPath}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
180
+ writeFileSync(temporaryPath, `${JSON.stringify(debt)}\n`, { mode: 0o600 });
181
+ const temporaryDescriptor = openSync(temporaryPath, 'r');
182
+ try {
183
+ fsyncSync(temporaryDescriptor);
184
+ }
185
+ finally {
186
+ closeSync(temporaryDescriptor);
187
+ }
188
+ renameSync(temporaryPath, debtPath);
189
+ syncRestoredHudDebtParentSync(debtPath);
190
+ }
191
+ function parseRestoredHudCleanupDebtSync(cwd, stateRoot) {
192
+ const debtPath = restoredHudCleanupDebtPath(cwd, stateRoot);
193
+ if (!existsSync(debtPath))
194
+ return null;
195
+ let debt;
196
+ try {
197
+ debt = JSON.parse(readFileSync(debtPath, 'utf8'));
198
+ }
199
+ catch {
200
+ throw new Error('restored_hud_cleanup_debt_malformed');
201
+ }
202
+ if (debt.schema_version !== 1 || debt.operation !== 'restored_hud_cleanup'
203
+ || !hasExplicitWorkerPaneId(debt.pane_id)
204
+ || !hasExplicitWorkerPaneId(debt.leader_pane_id)
205
+ || !Number.isSafeInteger(debt.leader_pane_pid) || debt.leader_pane_pid <= 0
206
+ || !(typeof debt.leader_pane_owner_id === 'string' || debt.leader_pane_owner_id === null)
207
+ || debt.hud_owner_leader_pane_id !== debt.leader_pane_id
208
+ || (debt.pane_pid !== null && (!Number.isSafeInteger(debt.pane_pid) || debt.pane_pid <= 0))) {
209
+ throw new Error('restored_hud_cleanup_debt_malformed');
210
+ }
211
+ return { path: debtPath, debt };
212
+ }
213
+ function removeRestoredHudCleanupDebtSync(path) {
214
+ unlinkSync(path);
215
+ syncRestoredHudDebtParentSync(path);
216
+ }
217
+ /**
218
+ * Replays a prior restored-HUD obligation. A live pane is killable only when
219
+ * its recorded PID, HUD command identity, and tagged leader identity all
220
+ * still match; PID-less and ambiguous records remain durable debt.
221
+ */
222
+ export function reconcileRestoredHudCleanupDebtSync(cwd, stateRoot) {
223
+ const record = parseRestoredHudCleanupDebtSync(cwd, stateRoot);
224
+ if (!record)
225
+ return;
226
+ const { path, debt } = record;
227
+ const proof = readExactPaneProofSync(debt.pane_id);
228
+ if (proof.status === 'gone') {
229
+ removeRestoredHudCleanupDebtSync(path);
230
+ return;
231
+ }
232
+ if (proof.status !== 'live' || debt.pane_pid === null || proof.pid !== debt.pane_pid || !debt.leader_pane_owner_id) {
233
+ throw new Error(`restored_hud_cleanup_debt_unresolved:${debt.pane_id}`);
234
+ }
235
+ const leader = requireLiveTeamOwnedPaneSync(debt.leader_pane_id, debt.leader_pane_pid, debt.leader_pane_owner_id);
236
+ const topology = listPanesResult(leader);
237
+ const hudMatches = !topology.error && topology.panes.filter((pane) => pane.paneId === proof.paneId
238
+ && hudPaneMatchesOwner(pane, { leaderPaneId: debt.hud_owner_leader_pane_id })).length === 1;
239
+ if (!hudMatches)
240
+ throw new Error(`restored_hud_cleanup_debt_unresolved:${debt.pane_id}`);
241
+ killExactPaneSync(proof.paneId, debt.pane_pid, () => {
242
+ requireLiveTeamOwnedPaneSync(debt.leader_pane_id, debt.leader_pane_pid, debt.leader_pane_owner_id);
243
+ });
244
+ removeRestoredHudCleanupDebtSync(path);
245
+ }
246
+ /** Remove restored-HUD debt only after the canonical config transaction commits the same frozen identity. */
247
+ export function finalizeRestoredHudCleanupDebtSync(cwd, paneId, panePid, stateRoot, _requireFreshHudIdentity) {
248
+ const record = parseRestoredHudCleanupDebtSync(cwd, stateRoot);
249
+ if (!record)
250
+ return;
251
+ const { debt } = record;
252
+ if (debt.pane_id !== paneId || debt.pane_pid !== panePid || !debt.leader_pane_owner_id) {
253
+ throw new Error(`restored_hud_cleanup_debt_unresolved:${debt.pane_id}`);
254
+ }
255
+ const leader = requireLiveTeamOwnedPaneSync(debt.leader_pane_id, debt.leader_pane_pid, debt.leader_pane_owner_id);
256
+ const topology = listPanesResult(leader);
257
+ const hudMatches = !topology.error && topology.panes.filter((pane) => pane.paneId === debt.pane_id
258
+ && hudPaneMatchesOwner(pane, { leaderPaneId: debt.hud_owner_leader_pane_id })).length === 1;
259
+ if (!hudMatches)
260
+ throw new Error(`restored_hud_cleanup_debt_unresolved:${debt.pane_id}`);
261
+ const finalProof = readExactPaneProofSync(debt.pane_id);
262
+ if (finalProof.status !== 'live' || finalProof.pid !== debt.pane_pid) {
263
+ throw new Error(`restored_hud_cleanup_debt_unresolved:${debt.pane_id}`);
264
+ }
265
+ removeRestoredHudCleanupDebtSync(record.path);
266
+ }
267
+ /**
268
+ * Kill only a pane that a fresh global snapshot proves live. Callers treat
269
+ * gone/dead rows as already cleaned and unavailable snapshots as fail-closed.
270
+ */
271
+ function requireLiveExactPaneSync(paneId, expectedPid) {
272
+ const proof = readExactPaneProofSync(paneId);
273
+ if (proof.status === 'unavailable')
274
+ throw new ExactPaneProofUnavailableError(proof);
275
+ if (proof.status === 'gone')
276
+ throw new Error(`tmux pane is not proven live: ${paneId}`);
277
+ if (expectedPid !== undefined && proof.pid !== expectedPid) {
278
+ throw new Error(`tmux pane identity changed: ${paneId}`);
279
+ }
280
+ return proof.paneId;
281
+ }
282
+ function killExactPaneSync(paneId, expectedPid, assertAuthorization) {
283
+ const proof = readExactPaneProofSync(paneId);
284
+ if (proof.status === 'unavailable')
285
+ throw new ExactPaneProofUnavailableError(proof);
286
+ if (proof.status === 'gone')
287
+ return;
288
+ if (expectedPid !== undefined && proof.pid !== expectedPid) {
289
+ throw new Error(`tmux pane identity changed: ${paneId}`);
290
+ }
291
+ assertAuthorization?.();
292
+ // Authorization can read tmux. Re-prove immediately after it so a recycled
293
+ // pane ID cannot be targeted by the subsequent kill.
294
+ const finalProof = readExactPaneProofSync(proof.paneId);
295
+ if (finalProof.status === 'unavailable')
296
+ throw new ExactPaneProofUnavailableError(finalProof);
297
+ if (finalProof.status === 'gone')
298
+ return;
299
+ if (finalProof.pid !== proof.pid || (expectedPid !== undefined && finalProof.pid !== expectedPid)) {
300
+ throw new Error(`tmux pane identity changed: ${paneId}`);
301
+ }
302
+ const result = runTmux(['kill-pane', '-t', finalProof.paneId]);
303
+ if (!result.ok)
304
+ throw new Error(`failed to kill tmux pane ${finalProof.paneId}: ${result.stderr}`);
305
+ const afterKill = readExactPaneProofSync(finalProof.paneId);
306
+ if (afterKill.status === 'unavailable')
307
+ throw new ExactPaneProofUnavailableError(afterKill);
308
+ if (afterKill.status !== 'gone') {
309
+ if (afterKill.pid !== finalProof.pid)
310
+ throw new Error(`tmux pane identity changed: ${paneId}`);
311
+ throw new Error(`tmux pane remains live after kill: ${paneId}`);
312
+ }
313
+ }
77
314
  function appendNoUnderlineStyleFlags(style) {
78
315
  const normalized = style
79
316
  .split(/[,\s]+/)
@@ -86,7 +323,8 @@ function appendNoUnderlineStyleFlags(style) {
86
323
  }
87
324
  return combined.join(',');
88
325
  }
89
- function sanitizeTmuxStyleOption(sessionTarget, optionName) {
326
+ function sanitizeTmuxStyleOption(sessionTarget, optionName, beforeEffect) {
327
+ beforeEffect?.();
90
328
  const shown = runTmux(['show-options', '-gv', '-t', sessionTarget, optionName]);
91
329
  if (!shown.ok)
92
330
  return false;
@@ -96,36 +334,39 @@ function sanitizeTmuxStyleOption(sessionTarget, optionName) {
96
334
  const sanitized = appendNoUnderlineStyleFlags(current);
97
335
  if (sanitized === current)
98
336
  return true;
337
+ beforeEffect?.();
99
338
  const result = runTmux(['set-option', '-t', sessionTarget, optionName, sanitized]);
100
339
  return result.ok;
101
340
  }
102
- function tagPaneInstance(paneTarget, instanceId) {
341
+ function tagPaneInstance(paneTarget, instanceId, expectedPanePid) {
103
342
  const target = paneTarget.trim();
104
343
  const sanitized = instanceId.trim();
105
344
  if (!target || !sanitized)
106
345
  return;
107
- const result = runTmux(['set-option', '-p', '-t', target, OMX_PANE_INSTANCE_OPTION, sanitized]);
346
+ const provenTarget = requireLiveExactPaneSync(target, expectedPanePid);
347
+ const result = runTmux(['set-option', '-p', '-t', provenTarget, OMX_PANE_INSTANCE_OPTION, sanitized]);
108
348
  if (!result.ok) {
109
- throw new Error(`failed to tag tmux pane ${target}: ${result.stderr}`);
349
+ throw new Error(`failed to tag tmux pane ${provenTarget}: ${result.stderr}`);
110
350
  }
111
351
  }
112
- export function tagPaneTeamOwner(paneTarget, teamOwnerId) {
352
+ export function tagPaneTeamOwner(paneTarget, teamOwnerId, expectedPanePid) {
113
353
  const target = paneTarget.trim();
114
354
  const sanitized = teamOwnerId.trim();
115
355
  if (!target || !sanitized)
116
356
  return;
117
- const result = runTmux(['set-option', '-p', '-t', target, OMX_TEAM_PANE_OWNER_OPTION, sanitized]);
357
+ const provenTarget = requireLiveExactPaneSync(target, expectedPanePid);
358
+ const result = runTmux(['set-option', '-p', '-t', provenTarget, OMX_TEAM_PANE_OWNER_OPTION, sanitized]);
118
359
  if (!result.ok) {
119
- throw new Error(`failed to tag tmux pane ${target}: ${result.stderr}`);
360
+ throw new Error(`failed to tag tmux pane ${provenTarget}: ${result.stderr}`);
120
361
  }
121
362
  }
122
- export function mitigateCopyModeUnderlineArtifacts(sessionTarget) {
363
+ export function mitigateCopyModeUnderlineArtifacts(sessionTarget, beforeEffect) {
123
364
  const normalizedTarget = sessionTarget.trim();
124
365
  if (normalizedTarget === '')
125
366
  return false;
126
367
  let applied = false;
127
368
  for (const optionName of TMUX_COPY_MODE_STYLE_OPTIONS) {
128
- if (sanitizeTmuxStyleOption(normalizedTarget, optionName)) {
369
+ if (sanitizeTmuxStyleOption(normalizedTarget, optionName, beforeEffect)) {
129
370
  applied = true;
130
371
  }
131
372
  }
@@ -181,18 +422,97 @@ function baseSessionName(target) {
181
422
  return target.split(':')[0] || target;
182
423
  }
183
424
  function listPanes(target) {
184
- const result = runTmux(['list-panes', '-t', target, '-F', '#{pane_id}\t#{pane_current_command}\t#{pane_start_command}']);
425
+ return listPanesResult(target).panes;
426
+ }
427
+ function listPanesResult(target) {
428
+ const result = runTmuxStructured(['list-panes', '-t', target, '-F', '#{pane_id}\t#{pane_current_command}\t#{pane_start_command}']);
185
429
  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('%'));
430
+ return { panes: [], error: result.stderr };
431
+ const panes = [];
432
+ for (const rawLine of result.stdout.split('\n')) {
433
+ const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine;
434
+ if (line.length === 0)
435
+ continue;
436
+ const firstSeparator = line.indexOf('\t');
437
+ const secondSeparator = firstSeparator >= 0 ? line.indexOf('\t', firstSeparator + 1) : -1;
438
+ const paneId = firstSeparator >= 0 ? line.slice(0, firstSeparator) : line;
439
+ if (firstSeparator < 0 || secondSeparator < 0 || !/^%[0-9]+$/.test(paneId)) {
440
+ return { panes: [], error: 'malformed pane topology' };
441
+ }
442
+ if (panes.some((pane) => pane.paneId === paneId)) {
443
+ return { panes: [], error: 'malformed pane topology' };
444
+ }
445
+ panes.push({
446
+ paneId,
447
+ currentCommand: line.slice(firstSeparator + 1, secondSeparator),
448
+ startCommand: line.slice(secondSeparator + 1),
449
+ });
450
+ }
451
+ return { panes, error: null };
452
+ }
453
+ /**
454
+ * Window-wide layout effects may only touch the exact startup pane set. A
455
+ * target-scoped topology read rejects foreign/new panes, and targeted global
456
+ * snapshots pin every surviving pane identity. The final scoped read happens
457
+ * immediately before the mutation so panes added during owner reads cannot
458
+ * inherit that authority.
459
+ */
460
+ /**
461
+ * Re-prove an explicitly Team-owned pane immediately before an effect. The
462
+ * double PID proof keeps ownership and process identity continuous across an
463
+ * untrusted tmux option read.
464
+ */
465
+ function requireLiveTeamOwnedPaneSync(paneId, expectedPanePid, expectedTeamOwnerId) {
466
+ const target = requireLiveExactPaneSync(paneId, expectedPanePid);
467
+ const owner = readPaneTeamOwnerTagResult(target);
468
+ if (owner.status !== 'value' || owner.value !== expectedTeamOwnerId) {
469
+ const detail = owner.status === 'error' ? owner.error : 'missing';
470
+ throw new Error(`tmux pane team owner changed: ${target}: ${detail}`);
471
+ }
472
+ return requireLiveExactPaneSync(target, expectedPanePid);
473
+ }
474
+ function requireFrozenWindowTopologySync(teamTarget, expectedPanePids, expectedPaneOwners) {
475
+ const topology = listPanesResult(teamTarget);
476
+ if (topology.error)
477
+ throw new Error(`failed to read tmux pane topology: ${topology.error}`);
478
+ const actualPaneIds = new Set(topology.panes.map((pane) => pane.paneId));
479
+ if (actualPaneIds.size !== expectedPanePids.size
480
+ || [...expectedPanePids.keys()].some((paneId) => !actualPaneIds.has(paneId))) {
481
+ throw new Error('tmux window topology changed before layout mutation');
482
+ }
483
+ for (const [paneId, expectedPanePid] of expectedPanePids) {
484
+ const expectedOwner = expectedPaneOwners?.get(paneId);
485
+ if (expectedOwner)
486
+ requireLiveTeamOwnedPaneSync(paneId, expectedPanePid, expectedOwner);
487
+ else {
488
+ const proof = readExactPaneProofSync(paneId);
489
+ if (proof.status === 'unavailable')
490
+ throw new ExactPaneProofUnavailableError(proof);
491
+ if (proof.status === 'gone')
492
+ throw new Error(`tmux pane is not proven live: ${proof.paneId}`);
493
+ if (proof.pid !== expectedPanePid)
494
+ throw new Error(`tmux pane identity changed: ${proof.paneId}`);
495
+ }
496
+ }
497
+ const finalProofs = readExactPaneProofsSync([...expectedPanePids.keys()]);
498
+ for (const [index, [paneId, expectedPanePid]] of [...expectedPanePids.entries()].entries()) {
499
+ const proof = finalProofs[index];
500
+ if (proof.status === 'unavailable')
501
+ throw new ExactPaneProofUnavailableError(proof);
502
+ if (proof.status === 'gone')
503
+ throw new Error(`tmux pane is not proven live: ${proof.paneId}`);
504
+ if (proof.paneId !== paneId || proof.pid !== expectedPanePid) {
505
+ throw new Error(`tmux pane identity changed: ${paneId}`);
506
+ }
507
+ }
508
+ const finalTopology = listPanesResult(teamTarget);
509
+ if (finalTopology.error)
510
+ throw new Error(`failed to read tmux pane topology: ${finalTopology.error}`);
511
+ const finalPaneIds = new Set(finalTopology.panes.map((pane) => pane.paneId));
512
+ if (finalPaneIds.size !== expectedPanePids.size
513
+ || [...expectedPanePids.keys()].some((paneId) => !finalPaneIds.has(paneId))) {
514
+ throw new Error('tmux window topology changed before layout mutation');
515
+ }
196
516
  }
197
517
  export function listPaneIds(target) {
198
518
  return listPanes(target).map((pane) => pane.paneId);
@@ -237,12 +557,6 @@ export function chooseTeamLeaderPaneId(panes, preferredPaneId) {
237
557
  return nonHud.paneId;
238
558
  return preferredPaneId;
239
559
  }
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
560
  function readPaneCurrentPath(paneId) {
247
561
  if (!paneId.startsWith('%'))
248
562
  return null;
@@ -281,7 +595,8 @@ function shouldAttemptRestoreCwdCandidate(candidate) {
281
595
  }
282
596
  return pathIsUsableDirectory(candidate.rawPath);
283
597
  }
284
- function resolveStandaloneHudRestoreCwdCandidates(leaderPaneId, fallbackCwd, explicitCwd) {
598
+ function resolveStandaloneHudRestoreCwdCandidates(leaderPaneId, fallbackCwd, explicitCwd, beforeReadLiveLeaderCwd) {
599
+ beforeReadLiveLeaderCwd?.();
285
600
  const liveLeaderCwd = readPaneCurrentPath(leaderPaneId);
286
601
  return uniqueRestoreCwdCandidates([
287
602
  { source: 'explicit', rawPath: explicitCwd },
@@ -318,105 +633,161 @@ async function runTmuxAsync(args) {
318
633
  return { ok: false, stderr: (err.stderr || err.message || '').trim() || 'tmux command failed' };
319
634
  }
320
635
  }
321
- async function sendKeyAsync(target, key) {
636
+ function hasExplicitWorkerPaneId(workerPaneId) {
637
+ return typeof workerPaneId === 'string' && workerPaneId.trim().length > 0;
638
+ }
639
+ function resolveWorkerPaneTargetSync(sessionName, workerIndex, workerPaneId) {
640
+ if (!hasExplicitWorkerPaneId(workerPaneId))
641
+ return paneTarget(sessionName, workerIndex);
642
+ const proof = readExactPaneProofSync(workerPaneId);
643
+ return proof.status === 'live' ? proof.paneId : null;
644
+ }
645
+ async function resolveWorkerPaneTargetAsync(sessionName, workerIndex, workerPaneId) {
646
+ if (!hasExplicitWorkerPaneId(workerPaneId))
647
+ return paneTarget(sessionName, workerIndex);
648
+ const proof = await readExactPaneProof(workerPaneId);
649
+ return proof.status === 'live' ? proof.paneId : null;
650
+ }
651
+ function createPinnedWorkerPaneTargetResolverSync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
652
+ if (!hasExplicitWorkerPaneId(workerPaneId)) {
653
+ return () => resolveWorkerPaneTargetSync(sessionName, workerIndex, workerPaneId);
654
+ }
655
+ const pinnedPid = expectedPanePid;
656
+ const expectedOwner = typeof expectedTeamOwnerId === 'string' ? expectedTeamOwnerId.trim() : '';
657
+ const canonicalHudPaneId = typeof hudPaneId === 'string' ? hudPaneId.trim() : '';
658
+ return () => {
659
+ if (typeof pinnedPid !== 'number' || !Number.isSafeInteger(pinnedPid) || pinnedPid <= 0)
660
+ return null;
661
+ if (!expectedOwner)
662
+ return null;
663
+ if (canonicalHudPaneId && workerPaneId === canonicalHudPaneId) {
664
+ throw new Error(`tmux worker pane is HUD target: ${workerPaneId}`);
665
+ }
666
+ const proof = readExactPaneProofSync(workerPaneId);
667
+ if (proof.status !== 'live')
668
+ return null;
669
+ if (proof.pid !== pinnedPid) {
670
+ throw new Error(`tmux pane identity changed: ${workerPaneId}`);
671
+ }
672
+ const owner = readPaneTeamOwnerTagResult(proof.paneId);
673
+ if (owner.status !== 'value' || owner.value !== expectedOwner)
674
+ return null;
675
+ const finalProof = readExactPaneProofSync(workerPaneId);
676
+ if (finalProof.status !== 'live')
677
+ return null;
678
+ if (finalProof.pid !== pinnedPid) {
679
+ throw new Error(`tmux pane identity changed: ${workerPaneId}`);
680
+ }
681
+ return finalProof.paneId;
682
+ };
683
+ }
684
+ function createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
685
+ if (!hasExplicitWorkerPaneId(workerPaneId)) {
686
+ return () => resolveWorkerPaneTargetAsync(sessionName, workerIndex, workerPaneId);
687
+ }
688
+ const pinnedPid = expectedPanePid;
689
+ const expectedOwner = typeof expectedTeamOwnerId === 'string' ? expectedTeamOwnerId.trim() : '';
690
+ const canonicalHudPaneId = typeof hudPaneId === 'string' ? hudPaneId.trim() : '';
691
+ let lastResolvedPid;
692
+ const resolveTarget = async () => {
693
+ if (typeof pinnedPid !== 'number' || !Number.isSafeInteger(pinnedPid) || pinnedPid <= 0)
694
+ return null;
695
+ if (!expectedOwner)
696
+ return null;
697
+ if (canonicalHudPaneId && workerPaneId === canonicalHudPaneId) {
698
+ throw new Error(`tmux worker pane is HUD target: ${workerPaneId}`);
699
+ }
700
+ const proof = await readExactPaneProof(workerPaneId);
701
+ if (proof.status !== 'live')
702
+ return null;
703
+ if (proof.pid !== pinnedPid) {
704
+ throw new Error(`tmux pane identity changed: ${workerPaneId}`);
705
+ }
706
+ const owner = readPaneTeamOwnerTagResult(proof.paneId);
707
+ if (owner.status !== 'value' || owner.value !== expectedOwner) {
708
+ const detail = owner.status === 'error' ? owner.error : 'missing';
709
+ throw new Error(`tmux pane team owner changed: ${workerPaneId}: ${detail}`);
710
+ }
711
+ // The option read is untrusted and can race pane ID reuse. Its immediately
712
+ // adjacent PID proof authorizes the following capture/control/input effect.
713
+ const finalProof = await readExactPaneProof(workerPaneId);
714
+ if (finalProof.status !== 'live')
715
+ return null;
716
+ if (finalProof.pid !== pinnedPid) {
717
+ throw new Error(`tmux pane identity changed: ${workerPaneId}`);
718
+ }
719
+ lastResolvedPid = finalProof.pid;
720
+ return finalProof.paneId;
721
+ };
722
+ resolveTarget.lastResolvedPid = () => lastResolvedPid;
723
+ return resolveTarget;
724
+ }
725
+ async function requireAsyncPaneTarget(resolveTarget) {
726
+ const target = await resolveTarget();
727
+ if (!target)
728
+ throw new Error('tmux pane is not proven live');
729
+ return target;
730
+ }
731
+ async function sendKeyAsync(resolveTarget, key) {
732
+ const target = await requireAsyncPaneTarget(resolveTarget);
322
733
  const result = await runTmuxAsync(['send-keys', '-t', target, key]);
323
734
  if (!result.ok) {
324
735
  throw new Error(`sendKeyAsync: failed to send ${key}: ${result.stderr}`);
325
736
  }
326
737
  }
327
- async function capturePaneAsync(target) {
738
+ async function capturePaneAsync(resolveTarget) {
739
+ const target = await resolveTarget();
740
+ if (!target)
741
+ return '';
328
742
  const result = await runTmuxAsync(sharedBuildCapturePaneArgv(target, 80));
329
743
  if (!result.ok)
330
744
  return '';
331
745
  return result.stdout;
332
746
  }
333
- async function captureVisiblePaneAsync(target) {
747
+ /** Capture an explicit Team worker pane only after PID-owner-PID authorization. */
748
+ export async function captureWorkerPane(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
749
+ const resolveTarget = createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
750
+ return capturePaneAsync(resolveTarget);
751
+ }
752
+ async function captureVisiblePaneAsync(resolveTarget) {
753
+ const target = await resolveTarget();
754
+ if (!target)
755
+ return '';
334
756
  const result = await runTmuxAsync(sharedBuildVisibleCapturePaneArgv(target));
335
757
  if (!result.ok)
336
758
  return '';
337
759
  return result.stdout;
338
760
  }
339
- async function isWorkerAliveAsync(sessionName, workerIndex, workerPaneId) {
340
- if (workerPaneId?.startsWith('%')) {
341
- const paneStatus = await readPaneLivenessByIdAsync(workerPaneId);
342
- if (paneStatus !== null)
343
- return paneStatus;
761
+ async function isWorkerAliveAsync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId, resolveTarget) {
762
+ if (hasExplicitWorkerPaneId(workerPaneId)) {
763
+ const resolver = resolveTarget ?? createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
764
+ const target = await resolver();
765
+ const pid = resolver.lastResolvedPid?.() ?? expectedPanePid;
766
+ if (!target || typeof pid !== 'number' || !Number.isSafeInteger(pid) || pid <= 0)
767
+ return 'unknown';
768
+ return probeProcessLiveness(pid);
344
769
  }
345
770
  const result = await runTmuxAsync([
346
771
  'list-panes',
347
- '-t', paneTarget(sessionName, workerIndex, workerPaneId),
772
+ '-t', paneTarget(sessionName, workerIndex),
348
773
  '-F',
349
774
  '#{pane_dead} #{pane_pid}',
350
775
  ]);
351
776
  if (!result.ok)
352
- return false;
777
+ return 'unknown';
353
778
  const line = result.stdout.split('\n')[0]?.trim();
354
779
  if (!line)
355
- return false;
780
+ return 'unknown';
356
781
  const parts = line.split(/\s+/);
357
782
  if (parts.length < 2)
358
- return false;
783
+ return 'unknown';
359
784
  const paneDead = parts[0];
360
785
  const pid = Number.parseInt(parts[1], 10);
361
786
  if (paneDead === '1')
362
- return false;
363
- 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;
787
+ return 'gone';
381
788
  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;
789
+ return 'unknown';
790
+ return probeProcessLiveness(pid);
420
791
  }
421
792
  function shellQuoteSingle(value) {
422
793
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -510,6 +881,20 @@ function buildNestedTmuxShellCommand(command) {
510
881
  function buildBestEffortShellCommand(command) {
511
882
  return `${command} >/dev/null 2>&1 || true`;
512
883
  }
884
+ function buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES, expectedPanePid, expectedPaneOwnerId, effectCommand = buildNestedTmuxShellCommand(buildHudResizeCommand(buildHudPaneTarget(hudPaneId), heightLines))) {
885
+ const target = buildHudPaneTarget(hudPaneId);
886
+ const snapshot = buildNestedTmuxShellCommand("list-panes -a -F '#{pane_id}\\t#{pane_dead}\\t#{pane_pid}'");
887
+ const expectedPid = typeof expectedPanePid === 'number' && Number.isSafeInteger(expectedPanePid) && expectedPanePid > 0
888
+ ? ` && $3 == "${expectedPanePid}"`
889
+ : '';
890
+ 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 }'`;
891
+ const expectedOwner = expectedPaneOwnerId?.trim();
892
+ if (!expectedOwner) {
893
+ return `if snapshot=$(${snapshot}); then printf '%s\\n' "$snapshot" | ${proof} && ${effectCommand}; fi`;
894
+ }
895
+ const owner = buildNestedTmuxShellCommand(`show-option -qv -p -t ${target} ${OMX_TEAM_PANE_OWNER_OPTION}`);
896
+ 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`;
897
+ }
513
898
  /** Upper bound for tmux hook indices (signed 32-bit max). */
514
899
  const TMUX_HOOK_INDEX_MAX = 2147483647;
515
900
  function buildResizeHookSlot(hookName) {
@@ -526,8 +911,8 @@ function buildClientAttachedHookSlot(hookName) {
526
911
  }
527
912
  return `client-attached[${Math.abs(hash) % TMUX_HOOK_INDEX_MAX}]`;
528
913
  }
529
- export function buildRegisterResizeHookArgs(hookTarget, hookName, hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES) {
530
- const resizeCommand = buildBestEffortShellCommand(buildNestedTmuxShellCommand(buildHudResizeCommand(hudPaneId, heightLines)));
914
+ export function buildRegisterResizeHookArgs(hookTarget, hookName, hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES, expectedPanePid, expectedPaneOwnerId) {
915
+ const resizeCommand = buildBestEffortShellCommand(buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines, expectedPanePid, expectedPaneOwnerId));
531
916
  const hookCommand = shellQuoteSingle(`${resizeCommand}; sleep ${HUD_RESIZE_RECONCILE_DELAY_SECONDS}; ${resizeCommand}`);
532
917
  return ['set-hook', '-t', hookTarget, buildResizeHookSlot(hookName), `run-shell -b ${hookCommand}`];
533
918
  }
@@ -543,9 +928,11 @@ export function buildClientAttachedReconcileHookName(teamName, sessionName, wind
543
928
  normalizeHudPaneToken(hudPaneId),
544
929
  ].join('_');
545
930
  }
546
- export function buildRegisterClientAttachedReconcileArgs(hookTarget, hookName, hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES) {
931
+ export function buildRegisterClientAttachedReconcileArgs(hookTarget, hookName, hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES, expectedPanePid, expectedPaneOwnerId) {
547
932
  const hookSlot = buildClientAttachedHookSlot(hookName);
548
- const oneShotCommand = shellQuoteSingle(`${buildBestEffortShellCommand(buildNestedTmuxShellCommand(buildHudResizeCommand(hudPaneId, heightLines)))}; ${buildNestedTmuxShellCommand(`set-hook -u -t ${hookTarget} ${hookSlot}`)}`);
933
+ const resizeCommand = buildBestEffortShellCommand(buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines, expectedPanePid, expectedPaneOwnerId));
934
+ const unregisterCommand = buildBestEffortShellCommand(buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines, expectedPanePid, expectedPaneOwnerId, buildNestedTmuxShellCommand(`set-hook -u -t ${hookTarget} ${hookSlot}`)));
935
+ const oneShotCommand = shellQuoteSingle(`${resizeCommand}; ${unregisterCommand}`);
549
936
  return ['set-hook', '-t', hookTarget, hookSlot, `run-shell -b ${oneShotCommand}`];
550
937
  }
551
938
  export function buildUnregisterClientAttachedReconcileArgs(hookTarget, hookName) {
@@ -555,18 +942,19 @@ export function unregisterResizeHook(hookTarget, hookName) {
555
942
  const result = runTmux(buildUnregisterResizeHookArgs(hookTarget, hookName));
556
943
  return result.ok;
557
944
  }
558
- export function buildScheduleDelayedHudResizeArgs(hudPaneId, delaySeconds = HUD_RESIZE_RECONCILE_DELAY_SECONDS, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES) {
945
+ export function buildScheduleDelayedHudResizeArgs(hudPaneId, delaySeconds = HUD_RESIZE_RECONCILE_DELAY_SECONDS, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES, expectedPanePid, expectedPaneOwnerId) {
559
946
  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)))}`];
947
+ return ['run-shell', '-b', `sleep ${delay}; ${buildBestEffortShellCommand(buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines, expectedPanePid, expectedPaneOwnerId))}`];
561
948
  }
562
- export function buildReconcileHudResizeArgs(hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES) {
563
- return ['run-shell', buildBestEffortShellCommand(buildNestedTmuxShellCommand(buildHudResizeCommand(hudPaneId, heightLines)))];
949
+ export function buildReconcileHudResizeArgs(hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES, expectedPanePid, expectedPaneOwnerId) {
950
+ return ['run-shell', buildBestEffortShellCommand(buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines, expectedPanePid, expectedPaneOwnerId))];
564
951
  }
565
- function redrawLeaderPaneAfterTeamLayout(leaderPaneId) {
952
+ function redrawLeaderPaneAfterTeamLayout(leaderPaneId, expectedPanePid, expectedTeamOwnerId) {
566
953
  const target = leaderPaneId.trim();
567
954
  if (!target.startsWith('%'))
568
955
  return;
569
- runTmux(['send-keys', '-t', target, 'C-l']);
956
+ const provenTarget = requireLiveTeamOwnedPaneSync(target, expectedPanePid, expectedTeamOwnerId);
957
+ runTmux(['send-keys', '-t', provenTarget, 'C-l']);
570
958
  }
571
959
  const ZSH_CANDIDATE_PATHS = ['/bin/zsh', '/usr/bin/zsh', '/usr/local/bin/zsh', '/opt/local/bin/zsh', '/opt/homebrew/bin/zsh'];
572
960
  const BASH_CANDIDATE_PATHS = ['/bin/bash', '/usr/bin/bash'];
@@ -613,24 +1001,35 @@ function escapeTomlString(value) {
613
1001
  function isModelInstructionsOverride(value) {
614
1002
  return new RegExp(`^${MODEL_INSTRUCTIONS_FILE_KEY}\\s*=`).test(value.trim());
615
1003
  }
616
- function hasModelInstructionsOverride(args) {
617
- for (let i = 0; i < args.length; i++) {
618
- const arg = args[i];
1004
+ function someConfigOverrideBeforeEndOfOptions(args, matches) {
1005
+ for (let index = 0; index < args.length; index += 1) {
1006
+ const arg = args[index];
1007
+ if (arg === '--')
1008
+ break;
1009
+ let value;
619
1010
  if (arg === CONFIG_FLAG || arg === LONG_CONFIG_FLAG) {
620
- const maybeValue = args[i + 1];
621
- if (typeof maybeValue === 'string' && isModelInstructionsOverride(maybeValue)) {
622
- return true;
623
- }
624
- continue;
1011
+ value = args[index + 1];
1012
+ if (value === '--')
1013
+ break;
1014
+ index += 1;
625
1015
  }
626
- if (arg.startsWith(`${LONG_CONFIG_FLAG}=`)) {
627
- const inlineValue = arg.slice(`${LONG_CONFIG_FLAG}=`.length);
628
- if (isModelInstructionsOverride(inlineValue))
629
- return true;
1016
+ else if (arg.startsWith(`${CONFIG_FLAG}=`)) {
1017
+ value = arg.slice(`${CONFIG_FLAG}=`.length);
630
1018
  }
1019
+ else if (arg.startsWith(`${LONG_CONFIG_FLAG}=`)) {
1020
+ value = arg.slice(`${LONG_CONFIG_FLAG}=`.length);
1021
+ }
1022
+ else {
1023
+ continue;
1024
+ }
1025
+ if (typeof value === 'string' && matches(value))
1026
+ return true;
631
1027
  }
632
1028
  return false;
633
1029
  }
1030
+ function hasModelInstructionsOverride(args) {
1031
+ return someConfigOverrideBeforeEndOfOptions(args, isModelInstructionsOverride);
1032
+ }
634
1033
  function normalizeTeamWorkerCliMode(raw, sourceEnv = OMX_TEAM_WORKER_CLI_ENV) {
635
1034
  const normalized = String(raw ?? 'auto').trim().toLowerCase();
636
1035
  if (normalized === '' || normalized === 'auto')
@@ -651,6 +1050,8 @@ function extractModelOverride(args) {
651
1050
  let model = null;
652
1051
  for (let i = 0; i < args.length; i++) {
653
1052
  const arg = args[i];
1053
+ if (arg === '--')
1054
+ break;
654
1055
  if (arg === MODEL_FLAG) {
655
1056
  const maybeValue = args[i + 1];
656
1057
  if (typeof maybeValue === 'string' && maybeValue.trim() !== '' && !maybeValue.startsWith('-')) {
@@ -764,6 +1165,18 @@ function shouldGrantExecutionBypassForRole(workerRole) {
764
1165
  return true;
765
1166
  return agent.tools === 'execution';
766
1167
  }
1168
+ export function assertTeamWorkerCliPolicyCompatibility(workerCli, launchArgs) {
1169
+ const policy = classifyTeamWorkerLaunchPolicy(launchArgs);
1170
+ if ((workerCli === 'claude' || workerCli === 'gemini') && (policy === 'direct-policy' || policy === 'mixed-policy')) {
1171
+ throw new Error(`Selected team worker CLI "${workerCli}" is incompatible with an explicit approval or sandbox policy.`);
1172
+ }
1173
+ }
1174
+ export function assertTeamWorkerLaunchPolicyInvariant(workerCli, launchArgs) {
1175
+ assertTeamWorkerCliPolicyCompatibility(workerCli, launchArgs);
1176
+ if (workerCli === 'codex' && classifyTeamWorkerLaunchPolicy(launchArgs) === 'mixed-policy') {
1177
+ throw new Error('internal_mixed_codex_worker_policy_argv');
1178
+ }
1179
+ }
767
1180
  export function translateWorkerLaunchArgsForCli(workerCli, args, initialPrompt, workerRole) {
768
1181
  if (workerCli === 'codex')
769
1182
  return [...args];
@@ -858,23 +1271,10 @@ export function scrubTeamWorkerHudOwnershipEnv(env) {
858
1271
  return scrubbed;
859
1272
  }
860
1273
  function hasConfigOverride(args, key) {
861
- const prefix = `${key}=`;
862
- for (let index = 0; index < args.length; index += 1) {
863
- const arg = args[index];
864
- if (arg === CONFIG_FLAG || arg === LONG_CONFIG_FLAG) {
865
- const value = args[index + 1];
866
- if (typeof value === 'string' && value.trim().startsWith(prefix))
867
- return true;
868
- index += 1;
869
- continue;
870
- }
871
- if (typeof arg === 'string' && arg.startsWith(`${LONG_CONFIG_FLAG}=`)) {
872
- const value = arg.slice(`${LONG_CONFIG_FLAG}=`.length);
873
- if (value.trim().startsWith(prefix))
874
- return true;
875
- }
876
- }
877
- return false;
1274
+ return someConfigOverrideBeforeEndOfOptions(args, (value) => {
1275
+ const trimmed = value.trim();
1276
+ return trimmed.startsWith(key) && /^\s*=/.test(trimmed.slice(key.length));
1277
+ });
878
1278
  }
879
1279
  function shouldDisableOmxMcpForTeamWorker(env) {
880
1280
  const raw = env[OMX_TEAM_WORKER_MCP_COMPAT_ENV]?.trim().toLowerCase();
@@ -907,17 +1307,42 @@ function appendTeamWorkerMcpDisableOverrides(args, env) {
907
1307
  const key = `mcp_servers.${server}.enabled`;
908
1308
  if (hasConfigOverride(args, key))
909
1309
  continue;
910
- args.push(CONFIG_FLAG, `${key}=false`);
1310
+ const endOfOptionsIndex = args.indexOf('--');
1311
+ args.splice(endOfOptionsIndex < 0 ? args.length : endOfOptionsIndex, 0, CONFIG_FLAG, `${key}=false`);
911
1312
  }
912
1313
  }
1314
+ function insertArgsBeforeEndOfOptions(args, insertedArgs) {
1315
+ const endOfOptionsIndex = args.indexOf('--');
1316
+ if (endOfOptionsIndex < 0)
1317
+ return [...args, ...insertedArgs];
1318
+ return [...args.slice(0, endOfOptionsIndex), ...insertedArgs, ...args.slice(endOfOptionsIndex)];
1319
+ }
1320
+ function insertCanonicalCodexBypassBeforeEndOfOptions(args) {
1321
+ const endOfOptionsIndex = args.indexOf('--');
1322
+ const preMarkerArgs = endOfOptionsIndex < 0 ? args : args.slice(0, endOfOptionsIndex);
1323
+ const postMarkerArgs = endOfOptionsIndex < 0 ? [] : args.slice(endOfOptionsIndex);
1324
+ return [
1325
+ ...preMarkerArgs.filter((arg) => arg !== CODEX_BYPASS_FLAG && arg !== MADMAX_FLAG),
1326
+ CODEX_BYPASS_FLAG,
1327
+ ...postMarkerArgs,
1328
+ ];
1329
+ }
913
1330
  function resolveWorkerLaunchArgs(extraArgs = [], cwd = process.cwd(), env = process.env) {
914
- const merged = [...extraArgs];
915
- const wantsBypass = process.argv.includes(CODEX_BYPASS_FLAG) || process.argv.includes(MADMAX_FLAG);
916
- if (wantsBypass && !merged.includes(CODEX_BYPASS_FLAG)) {
917
- merged.push(CODEX_BYPASS_FLAG);
1331
+ let merged = [...extraArgs];
1332
+ const initialPolicy = classifyTeamWorkerLaunchPolicy(merged);
1333
+ if (initialPolicy === 'direct-policy') {
1334
+ merged = normalizeTeamWorkerLaunchArgs(merged);
1335
+ }
1336
+ const policy = classifyTeamWorkerLaunchPolicy(merged);
1337
+ const ambientWantsBypass = parseTeamWorkerLaunchArgs(process.argv, 'ambient process arguments', { directPolicyMode: 'ignore' }).wantsBypass;
1338
+ if (policy === 'none' || policy === 'bypass') {
1339
+ const wantsBypass = ambientWantsBypass || policy === 'bypass';
1340
+ if (wantsBypass) {
1341
+ merged = insertCanonicalCodexBypassBeforeEndOfOptions(merged);
1342
+ }
918
1343
  }
919
1344
  if (shouldBypassDefaultSystemPrompt(env) && !hasModelInstructionsOverride(merged)) {
920
- merged.push(CONFIG_FLAG, buildModelInstructionsOverride(cwd, env));
1345
+ merged = insertArgsBeforeEndOfOptions(merged, [CONFIG_FLAG, buildModelInstructionsOverride(cwd, env)]);
921
1346
  }
922
1347
  return merged;
923
1348
  }
@@ -1040,11 +1465,15 @@ function buildWorkerProcessLaunchSpecForMode(mode, teamName, workerIndex, launch
1040
1465
  const effectiveEnv = { ...process.env, ...extraEnv };
1041
1466
  const fullLaunchArgs = resolveWorkerLaunchArgs(launchArgs, cwd, effectiveEnv);
1042
1467
  const workerCli = workerCliOverride ?? resolveTeamWorkerCli(fullLaunchArgs, effectiveEnv);
1468
+ assertTeamWorkerLaunchPolicyInvariant(workerCli, fullLaunchArgs);
1043
1469
  const cliLaunchArgs = translateWorkerLaunchArgsForCli(workerCli, fullLaunchArgs, initialPrompt, workerRole);
1470
+ const launchPolicy = workerCli === 'codex'
1471
+ ? classifyTeamWorkerLaunchPolicy(cliLaunchArgs)
1472
+ : 'none';
1044
1473
  const effectiveCliLaunchArgs = workerCli === 'codex'
1045
1474
  && shouldGrantExecutionBypassForRole(workerRole)
1046
- && !cliLaunchArgs.includes(CODEX_BYPASS_FLAG)
1047
- ? [...cliLaunchArgs, CODEX_BYPASS_FLAG]
1475
+ && launchPolicy === 'none'
1476
+ ? insertCanonicalCodexBypassBeforeEndOfOptions(cliLaunchArgs)
1048
1477
  : cliLaunchArgs;
1049
1478
  const workerCodexHomeOverride = typeof effectiveEnv.CODEX_HOME === 'string'
1050
1479
  ? effectiveEnv.CODEX_HOME.trim()
@@ -1160,16 +1589,35 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
1160
1589
  }
1161
1590
  const normalizedWorkerLaunchArgs = resolveWorkerLaunchArgs(workerLaunchArgs, cwd);
1162
1591
  const defaultWorkerCliPlan = resolveTeamWorkerCliPlan(workerCount, normalizedWorkerLaunchArgs, process.env);
1163
- const workerCliPlan = workerStartups.length > 0
1164
- ? workerStartups.map((startup, index) => startup.workerCli ?? defaultWorkerCliPlan[index])
1165
- : defaultWorkerCliPlan;
1592
+ const workerCliPlan = Array.from({ length: workerCount }, (_, index) => workerStartups[index]?.workerCli ?? defaultWorkerCliPlan[index]);
1166
1593
  for (const workerCli of new Set(workerCliPlan)) {
1167
1594
  assertTeamWorkerCliBinaryAvailable(workerCli);
1168
1595
  }
1596
+ const workerLaunchPolicyPlan = Array.from({ length: workerCount }, (_, index) => {
1597
+ const startup = workerStartups[index] ?? {};
1598
+ const workerCwd = startup.cwd || cwd;
1599
+ const workerEnv = startup.env || {};
1600
+ const launchArgs = startup.launchArgs || workerLaunchArgs;
1601
+ const effectiveLaunchArgs = resolveWorkerLaunchArgs(launchArgs, workerCwd, { ...process.env, ...workerEnv });
1602
+ assertTeamWorkerLaunchPolicyInvariant(workerCliPlan[index], effectiveLaunchArgs);
1603
+ return Object.freeze([...launchArgs]);
1604
+ });
1169
1605
  const safeTeamName = sanitizeTeamName(teamName);
1170
1606
  let registeredResizeHook = null;
1171
1607
  let registeredClientAttachedHook = null;
1172
- const rollbackPaneIds = [];
1608
+ const rollbackPanes = new Map();
1609
+ // An owner-tag command may have succeeded even if tmux reports an ambiguous
1610
+ // failure; rollback can only kill panes whose owner is re-authorized.
1611
+ const rollbackTaggedPaneOwnerIds = new Map();
1612
+ let partialTeamTarget = null;
1613
+ let partialLeaderPaneId = null;
1614
+ let partialLeaderPanePid;
1615
+ let partialTeamPaneOwnerId = '';
1616
+ const partialWorkerPaneIds = [];
1617
+ const partialWorkerPaneIdsByIndex = Array.from({ length: workerCount }, () => null);
1618
+ const partialWorkerPanePidsByIndex = Array.from({ length: workerCount }, () => null);
1619
+ let partialHudPaneId = null;
1620
+ let partialHudPanePid = null;
1173
1621
  try {
1174
1622
  const tmuxPaneTarget = process.env.TMUX_PANE;
1175
1623
  const displayArgs = tmuxPaneTarget
@@ -1186,42 +1634,71 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
1186
1634
  throw new Error(`failed to parse current tmux target: ${context.stdout}`);
1187
1635
  }
1188
1636
  const teamTarget = `${sessionName}:${windowIndex}`;
1637
+ partialTeamTarget = teamTarget;
1189
1638
  const ownerSessionId = (options.ownerSessionId ?? process.env.OMX_SESSION_ID ?? '').trim();
1190
1639
  const teamPaneOwnerId = (options.teamPaneOwnerId ?? `team:${safeTeamName}`).trim();
1640
+ partialTeamPaneOwnerId = teamPaneOwnerId;
1641
+ const paneListResult = listPanesResult(teamTarget);
1642
+ if (paneListResult.error)
1643
+ throw new Error(`failed to read tmux pane topology: ${paneListResult.error}`);
1644
+ const leaderPaneId = chooseTeamLeaderPaneId(paneListResult.panes, detectedLeaderPaneId);
1645
+ const leaderProof = readExactPaneProofSync(leaderPaneId);
1646
+ if (leaderProof.status === 'unavailable')
1647
+ throw new ExactPaneProofUnavailableError(leaderProof);
1648
+ if (leaderProof.status === 'gone')
1649
+ throw new Error(`tmux pane is not proven live: ${leaderPaneId}`);
1650
+ const leaderPanePid = leaderProof.pid;
1651
+ partialLeaderPaneId = leaderPaneId;
1652
+ partialLeaderPanePid = leaderPanePid;
1653
+ const initialHudPaneIds = findHudWatchPaneIds(paneListResult.panes, leaderPaneId, { leaderPaneId });
1654
+ const initialWindowPanePids = new Map([[leaderPaneId, leaderPanePid]]);
1655
+ const omxEntry = resolveOmxCliEntryPath();
1656
+ const canRecreateTeamHud = Boolean(omxEntry && omxEntry.trim() !== '');
1657
+ // Freeze every HUD owned by this leader even when startup cannot recreate
1658
+ // it; the untouched pane remains part of the authorized topology.
1659
+ for (const hudPaneId of initialHudPaneIds) {
1660
+ const proof = readExactPaneProofSync(hudPaneId);
1661
+ if (proof.status === 'unavailable')
1662
+ throw new ExactPaneProofUnavailableError(proof);
1663
+ if (proof.status === 'gone')
1664
+ throw new Error(`tmux pane is not proven live: ${hudPaneId}`);
1665
+ initialWindowPanePids.set(hudPaneId, proof.pid);
1666
+ }
1667
+ // Team mode prioritizes leader + worker visibility. Remove HUD panes only
1668
+ // when we can recreate the team HUD. Otherwise keep the existing HUD alive
1669
+ // instead of making it disappear on team startup failures or broken installs.
1670
+ requireFrozenWindowTopologySync(teamTarget, initialWindowPanePids);
1191
1671
  if (ownerSessionId) {
1192
1672
  const tagResult = runTmux(['set-option', '-t', sessionName, OMX_INSTANCE_OPTION, ownerSessionId]);
1193
1673
  if (!tagResult.ok) {
1194
1674
  throw new Error(`failed to tag tmux session ${sessionName}: ${tagResult.stderr}`);
1195
1675
  }
1196
1676
  }
1197
- const panes = listPanes(teamTarget);
1198
- const leaderPaneId = chooseTeamLeaderPaneId(panes, detectedLeaderPaneId);
1199
- tagPaneInstance(leaderPaneId, ownerSessionId);
1200
- tagPaneTeamOwner(leaderPaneId, teamPaneOwnerId);
1201
- const initialHudPaneIds = findHudPaneIds(teamTarget, leaderPaneId);
1202
- const omxEntry = resolveOmxCliEntryPath();
1203
- const canRecreateTeamHud = Boolean(omxEntry && omxEntry.trim() !== '');
1204
- // Team mode prioritizes leader + worker visibility. Remove HUD panes only
1205
- // when we can recreate the team HUD. Otherwise keep the existing HUD alive
1206
- // instead of making it disappear on team startup failures or broken installs.
1677
+ tagPaneInstance(leaderPaneId, ownerSessionId, leaderPanePid);
1678
+ tagPaneTeamOwner(leaderPaneId, teamPaneOwnerId, leaderPanePid);
1207
1679
  if (canRecreateTeamHud) {
1208
- for (const hudPaneId of initialHudPaneIds) {
1209
- runTmux(['kill-pane', '-t', hudPaneId]);
1680
+ for (const [hudPaneId, hudPanePid] of initialWindowPanePids) {
1681
+ if (hudPaneId !== leaderPaneId)
1682
+ killExactPaneSync(hudPaneId, hudPanePid);
1210
1683
  }
1211
1684
  }
1212
1685
  const workerPaneIds = [];
1686
+ const workerPanePidsByIndex = Array.from({ length: workerCount }, () => null);
1213
1687
  let rightStackRootPaneId = null;
1688
+ let rightStackRootPanePid = null;
1689
+ const frozenWindowPanePids = new Map([[leaderPaneId, leaderPanePid]]);
1690
+ const frozenWindowPaneOwners = new Map([[leaderPaneId, teamPaneOwnerId]]);
1214
1691
  for (let i = 1; i <= workerCount; i++) {
1215
1692
  const startup = workerStartups[i - 1] || {};
1216
1693
  const workerCwd = startup.cwd || cwd;
1217
1694
  const tmuxWorkerCwd = translatePathForMsys(workerCwd);
1218
1695
  const workerEnv = startup.env || {};
1219
- const launchArgsForWorker = startup.launchArgs || workerLaunchArgs;
1696
+ const launchArgsForWorker = [...(workerLaunchPolicyPlan[i - 1] ?? workerLaunchArgs)];
1220
1697
  trustWorkerMiseConfigIfAvailable(workerCwd);
1221
1698
  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);
1222
1699
  // First split creates the right side from leader. Remaining splits stack on the right.
1223
1700
  const splitDirection = i === 1 ? '-h' : '-v';
1224
- const splitTarget = i === 1 ? leaderPaneId : (rightStackRootPaneId ?? leaderPaneId);
1701
+ const splitTarget = requireLiveTeamOwnedPaneSync(i === 1 ? leaderPaneId : (rightStackRootPaneId ?? leaderPaneId), i === 1 ? leaderPanePid : (rightStackRootPanePid ?? leaderPanePid), teamPaneOwnerId);
1225
1702
  const split = runTmux([
1226
1703
  'split-window',
1227
1704
  splitDirection,
@@ -1242,17 +1719,37 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
1242
1719
  if (!paneId || !paneId.startsWith('%')) {
1243
1720
  throw new Error(`failed to capture worker pane id for worker ${i}`);
1244
1721
  }
1245
- rollbackPaneIds.push(paneId);
1722
+ // The pane exists once split-window returns its concrete ID. Persist it in
1723
+ // rollback/partial state before its first proof: a malformed or unavailable
1724
+ // topology must retain cleanup debt, but cannot authorize an effect.
1725
+ rollbackPanes.set(paneId, null);
1726
+ partialWorkerPaneIds.push(paneId);
1727
+ partialWorkerPaneIdsByIndex[i - 1] = paneId;
1728
+ const paneProof = readExactPaneProofSync(paneId);
1729
+ if (paneProof.status === 'unavailable')
1730
+ throw new ExactPaneProofUnavailableError(paneProof);
1731
+ if (paneProof.status === 'gone')
1732
+ throw new Error(`tmux pane is not proven live: ${paneId}`);
1733
+ const panePid = paneProof.pid;
1734
+ rollbackPanes.set(paneId, panePid);
1735
+ partialWorkerPanePidsByIndex[i - 1] = panePid;
1736
+ workerPanePidsByIndex[i - 1] = panePid;
1737
+ frozenWindowPanePids.set(paneId, panePid);
1246
1738
  if (isNativeWindows() && !waitForPaneToRemainPresent(teamTarget, paneId)) {
1247
1739
  throw new Error(`worker pane ${i} did not remain present after tmux split-window returned ${paneId}`);
1248
1740
  }
1249
- tagPaneInstance(paneId, ownerSessionId);
1250
- tagPaneTeamOwner(paneId, teamPaneOwnerId);
1741
+ tagPaneInstance(paneId, ownerSessionId, panePid);
1742
+ rollbackTaggedPaneOwnerIds.set(paneId, teamPaneOwnerId);
1743
+ tagPaneTeamOwner(paneId, teamPaneOwnerId, panePid);
1744
+ frozenWindowPaneOwners.set(paneId, teamPaneOwnerId);
1251
1745
  workerPaneIds.push(paneId);
1252
- if (i === 1)
1746
+ if (i === 1) {
1253
1747
  rightStackRootPaneId = paneId;
1748
+ rightStackRootPanePid = panePid;
1749
+ }
1254
1750
  }
1255
1751
  // Keep leader as full left/main pane; workers stay stacked on the right.
1752
+ requireFrozenWindowTopologySync(teamTarget, frozenWindowPanePids, frozenWindowPaneOwners);
1256
1753
  runTmux(['select-layout', '-t', teamTarget, 'main-vertical']);
1257
1754
  // Force leader pane to use half the window width.
1258
1755
  const windowWidthResult = runTmux(['display-message', '-p', '-t', teamTarget, '#{window_width}']);
@@ -1260,7 +1757,9 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
1260
1757
  const width = Number.parseInt(windowWidthResult.stdout.split('\n')[0]?.trim() || '', 10);
1261
1758
  if (Number.isFinite(width) && width >= 40) {
1262
1759
  const half = String(Math.floor(width / 2));
1760
+ requireFrozenWindowTopologySync(teamTarget, frozenWindowPanePids, frozenWindowPaneOwners);
1263
1761
  runTmux(['set-window-option', '-t', teamTarget, 'main-pane-width', half]);
1762
+ requireFrozenWindowTopologySync(teamTarget, frozenWindowPanePids, frozenWindowPaneOwners);
1264
1763
  runTmux(['select-layout', '-t', teamTarget, 'main-vertical']);
1265
1764
  }
1266
1765
  }
@@ -1274,23 +1773,39 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
1274
1773
  if (canRecreateTeamHud && omxEntry) {
1275
1774
  const hudCmd = `exec env ${formatHudEnvAssignments(process.env, { sessionId: ownerSessionId, leaderPaneId })} node ${shellQuoteSingle(translatePathForMsys(omxEntry))} hud --watch`;
1276
1775
  const hudCwd = translatePathForMsys(cwd);
1776
+ requireFrozenWindowTopologySync(teamTarget, frozenWindowPanePids, frozenWindowPaneOwners);
1777
+ const hudSplitTarget = leaderPaneId;
1277
1778
  const hudResult = runTmux([
1278
- 'split-window', '-v', '-f', '-l', String(HUD_TMUX_TEAM_HEIGHT_LINES), '-t', teamTarget, '-d', '-P', '-F', '#{pane_id}', '-c', hudCwd, hudCmd,
1779
+ 'split-window', '-v', '-f', '-l', String(HUD_TMUX_TEAM_HEIGHT_LINES), '-t', hudSplitTarget, '-d', '-P', '-F', '#{pane_id}', '-c', hudCwd, hudCmd,
1279
1780
  ]);
1280
1781
  if (hudResult.ok) {
1281
1782
  const id = hudResult.stdout.split('\n')[0]?.trim() ?? '';
1282
1783
  if (id.startsWith('%')) {
1283
- rollbackPaneIds.push(id);
1784
+ // split-window has created a concrete pane even though no proof has
1785
+ // authorized an effect yet. Retain PID-less cleanup debt first.
1786
+ rollbackPanes.set(id, null);
1787
+ partialHudPaneId = id;
1788
+ partialHudPanePid = null;
1789
+ const hudProof = readExactPaneProofSync(id);
1790
+ if (hudProof.status === 'unavailable')
1791
+ throw new ExactPaneProofUnavailableError(hudProof);
1792
+ if (hudProof.status === 'gone')
1793
+ throw new Error(`tmux pane is not proven live: ${id}`);
1794
+ const hudPanePid = hudProof.pid;
1795
+ frozenWindowPanePids.set(id, hudPanePid);
1796
+ rollbackPanes.set(id, hudPanePid);
1797
+ partialHudPanePid = hudPanePid;
1284
1798
  if (isNativeWindows() && !waitForPaneToRemainPresent(teamTarget, id)) {
1285
1799
  throw new Error(`HUD pane did not remain present after tmux split-window returned ${id}`);
1286
1800
  }
1287
- tagPaneInstance(id, ownerSessionId);
1288
- tagPaneTeamOwner(id, teamPaneOwnerId);
1801
+ tagPaneInstance(id, ownerSessionId, hudPanePid);
1802
+ rollbackTaggedPaneOwnerIds.set(id, teamPaneOwnerId);
1803
+ tagPaneTeamOwner(id, teamPaneOwnerId, hudPanePid);
1804
+ frozenWindowPaneOwners.set(id, teamPaneOwnerId);
1289
1805
  hudPaneId = id;
1290
1806
  if (isNativeWindows()) {
1291
- // Native Windows tmux support may flow through psmux; issuing a
1292
- // direct control-plane resize avoids nested run-shell PATH drift.
1293
- const reconcile = runTmux(buildHudResizeArgs(hudPaneId));
1807
+ const provenHudPaneId = requireLiveTeamOwnedPaneSync(hudPaneId, hudPanePid, teamPaneOwnerId);
1808
+ const reconcile = runTmux(buildHudResizeArgs(provenHudPaneId));
1294
1809
  if (!reconcile.ok) {
1295
1810
  throw new Error(`failed to reconcile HUD resize: ${reconcile.stderr}`);
1296
1811
  }
@@ -1298,36 +1813,50 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
1298
1813
  else {
1299
1814
  const hookTarget = buildResizeHookTarget(sessionName, windowIndex);
1300
1815
  const hookName = buildResizeHookName(safeTeamName, sessionName, windowIndex, hudPaneId);
1301
- const registerHook = runTmux(buildRegisterResizeHookArgs(hookTarget, hookName, hudPaneId));
1816
+ requireLiveTeamOwnedPaneSync(hudPaneId, hudPanePid, teamPaneOwnerId);
1817
+ const registerHook = runTmux(buildRegisterResizeHookArgs(hookTarget, hookName, hudPaneId, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid, teamPaneOwnerId));
1302
1818
  const clientAttachedHookName = buildClientAttachedReconcileHookName(safeTeamName, sessionName, windowIndex, hudPaneId);
1303
1819
  if (registerHook.ok) {
1304
1820
  resizeHookTarget = hookTarget;
1305
1821
  resizeHookName = hookName;
1306
- registeredResizeHook = { name: resizeHookName, target: resizeHookTarget };
1822
+ registeredResizeHook = {
1823
+ name: resizeHookName,
1824
+ target: resizeHookTarget,
1825
+ leaderPaneId,
1826
+ leaderPanePid,
1827
+ hudPaneId,
1828
+ hudPanePid,
1829
+ teamPaneOwnerId,
1830
+ };
1307
1831
  }
1308
1832
  else {
1309
- // tmux versions/builds that reject indexed client-resized hooks should not
1310
- // abort madmax/team startup after panes were successfully created. Keep the
1311
- // fallback narrow: skip only the long-lived resize hook metadata, then
1312
- // still try the one-shot client-attached reconcile plus the explicit
1313
- // delayed/direct resize checks below so real tmux/run-shell failures
1314
- // still surface.
1315
1833
  console.warn(`[omx] tmux resize hook unavailable for ${hookTarget} (${hookName}): ${registerHook.stderr}; `
1316
1834
  + 'continuing with best-effort HUD resize fallback.');
1317
1835
  }
1318
- const registerClientAttachedHook = runTmux(buildRegisterClientAttachedReconcileArgs(hookTarget, clientAttachedHookName, hudPaneId));
1836
+ requireLiveTeamOwnedPaneSync(hudPaneId, hudPanePid, teamPaneOwnerId);
1837
+ const registerClientAttachedHook = runTmux(buildRegisterClientAttachedReconcileArgs(hookTarget, clientAttachedHookName, hudPaneId, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid, teamPaneOwnerId));
1319
1838
  if (registerClientAttachedHook.ok) {
1320
- registeredClientAttachedHook = { name: clientAttachedHookName, target: hookTarget };
1839
+ registeredClientAttachedHook = {
1840
+ name: clientAttachedHookName,
1841
+ target: hookTarget,
1842
+ leaderPaneId,
1843
+ leaderPanePid,
1844
+ hudPaneId,
1845
+ hudPanePid,
1846
+ teamPaneOwnerId,
1847
+ };
1321
1848
  }
1322
1849
  else {
1323
1850
  console.warn(`[omx] tmux client-attached resize fallback unavailable for ${hookTarget} `
1324
1851
  + `(${clientAttachedHookName}): ${registerClientAttachedHook.stderr}; continuing with delayed HUD resize fallback.`);
1325
1852
  }
1326
- const delayed = runTmux(buildScheduleDelayedHudResizeArgs(hudPaneId));
1853
+ requireLiveTeamOwnedPaneSync(hudPaneId, hudPanePid, teamPaneOwnerId);
1854
+ const delayed = runTmux(buildScheduleDelayedHudResizeArgs(hudPaneId, HUD_RESIZE_RECONCILE_DELAY_SECONDS, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid, teamPaneOwnerId));
1327
1855
  if (!delayed.ok) {
1328
1856
  console.warn(`[omx] tmux delayed HUD resize unavailable for ${hudPaneId}: ${delayed.stderr}; continuing.`);
1329
1857
  }
1330
- const reconcile = runTmux(buildReconcileHudResizeArgs(hudPaneId));
1858
+ requireLiveTeamOwnedPaneSync(hudPaneId, hudPanePid, teamPaneOwnerId);
1859
+ const reconcile = runTmux(buildReconcileHudResizeArgs(hudPaneId, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid, teamPaneOwnerId));
1331
1860
  if (!reconcile.ok) {
1332
1861
  console.warn(`[omx] tmux HUD resize reconcile unavailable for ${hudPaneId}: ${reconcile.stderr}; continuing.`);
1333
1862
  }
@@ -1335,21 +1864,31 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
1335
1864
  }
1336
1865
  }
1337
1866
  }
1338
- runTmux(['select-pane', '-t', leaderPaneId]);
1339
- redrawLeaderPaneAfterTeamLayout(leaderPaneId);
1867
+ runTmux(['select-pane', '-t', requireLiveTeamOwnedPaneSync(leaderPaneId, leaderPanePid, teamPaneOwnerId)]);
1868
+ redrawLeaderPaneAfterTeamLayout(leaderPaneId, leaderPanePid, teamPaneOwnerId);
1340
1869
  sleepSeconds(0.5);
1341
1870
  // Enable mouse scrolling so agent output panes can be scrolled with the
1342
1871
  // mouse wheel without conflicting with keyboard up/down arrow-key input
1343
1872
  // history navigation in the Codex CLI input field. (issue #103)
1344
1873
  // Opt-out: set OMX_TEAM_MOUSE=0 in the environment.
1345
1874
  if (process.env.OMX_TEAM_MOUSE !== '0') {
1346
- enableMouseScrolling(sessionName);
1875
+ enableMouseScrolling(sessionName, () => { requireLiveTeamOwnedPaneSync(leaderPaneId, leaderPanePid, teamPaneOwnerId); });
1876
+ }
1877
+ const sessionIncarnation = teamTarget.includes(':') ? null : queryDetachedTeamSession(sessionName);
1878
+ if (sessionIncarnation && sessionIncarnation.status !== 'exact') {
1879
+ throw new Error(`tmux_session_incarnation_unavailable:${sessionName}`);
1347
1880
  }
1348
1881
  return {
1349
1882
  name: teamTarget,
1883
+ tmuxSessionId: sessionIncarnation?.incarnation.sessionId,
1884
+ tmuxSessionCreated: sessionIncarnation?.incarnation.sessionCreated,
1350
1885
  workerCount,
1351
1886
  cwd,
1352
1887
  workerPaneIds,
1888
+ workerPaneIdsByIndex: [...workerPaneIds],
1889
+ workerPanePidsByIndex,
1890
+ leaderPanePid,
1891
+ hudPanePid: partialHudPanePid,
1353
1892
  leaderPaneId,
1354
1893
  hudPaneId,
1355
1894
  resizeHookName,
@@ -1358,14 +1897,82 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
1358
1897
  };
1359
1898
  }
1360
1899
  catch (error) {
1361
- if (registeredClientAttachedHook) {
1362
- runTmux(buildUnregisterClientAttachedReconcileArgs(registeredClientAttachedHook.target, registeredClientAttachedHook.name));
1900
+ const cleanupErrors = [];
1901
+ const unregisterAuthorizedHook = (hook, unregister, label) => {
1902
+ try {
1903
+ requireLiveTeamOwnedPaneSync(hook.leaderPaneId, hook.leaderPanePid, hook.teamPaneOwnerId);
1904
+ requireLiveTeamOwnedPaneSync(hook.hudPaneId, hook.hudPanePid, hook.teamPaneOwnerId);
1905
+ const unregistered = runTmux(unregister(hook.target, hook.name));
1906
+ if (unregistered.ok)
1907
+ return true;
1908
+ cleanupErrors.push(`failed to unregister tmux ${label} hook ${hook.name}: ${unregistered.stderr}`);
1909
+ }
1910
+ catch (cleanupError) {
1911
+ cleanupErrors.push(`unable to authorize tmux ${label} hook cleanup ${hook.name}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
1912
+ }
1913
+ return false;
1914
+ };
1915
+ if (registeredClientAttachedHook && unregisterAuthorizedHook(registeredClientAttachedHook, buildUnregisterClientAttachedReconcileArgs, 'client-attached')) {
1916
+ registeredClientAttachedHook = null;
1363
1917
  }
1364
- if (registeredResizeHook) {
1365
- runTmux(buildUnregisterResizeHookArgs(registeredResizeHook.target, registeredResizeHook.name));
1918
+ if (registeredResizeHook && unregisterAuthorizedHook(registeredResizeHook, buildUnregisterResizeHookArgs, 'resize')) {
1919
+ registeredResizeHook = null;
1366
1920
  }
1367
- for (const paneId of rollbackPaneIds) {
1368
- runTmux(['kill-pane', '-t', paneId]);
1921
+ const proofUnavailable = [];
1922
+ if (error instanceof ExactPaneProofUnavailableError)
1923
+ proofUnavailable.push(error.proof);
1924
+ const unresolvedPaneIds = new Set(rollbackPanes.keys());
1925
+ for (const [paneId, panePid] of rollbackPanes) {
1926
+ // split-window IDs without a positive exact PID proof are cleanup debt,
1927
+ // never authorization to affect a potentially recycled pane ID.
1928
+ if (panePid === null)
1929
+ continue;
1930
+ try {
1931
+ const expectedOwnerId = rollbackTaggedPaneOwnerIds.get(paneId);
1932
+ killExactPaneSync(paneId, panePid, expectedOwnerId
1933
+ ? () => { requireLiveTeamOwnedPaneSync(paneId, panePid, expectedOwnerId); }
1934
+ : undefined);
1935
+ unresolvedPaneIds.delete(paneId);
1936
+ }
1937
+ catch (cleanupError) {
1938
+ if (cleanupError instanceof ExactPaneProofUnavailableError) {
1939
+ proofUnavailable.push(cleanupError.proof);
1940
+ break;
1941
+ }
1942
+ else {
1943
+ cleanupErrors.push(cleanupError instanceof Error ? cleanupError.message : String(cleanupError));
1944
+ }
1945
+ }
1946
+ }
1947
+ const unresolvedWorkerPaneIds = partialWorkerPaneIds.filter((paneId) => unresolvedPaneIds.has(paneId));
1948
+ const unresolvedWorkerPaneIdsByIndex = partialWorkerPaneIdsByIndex
1949
+ .map((paneId) => paneId && unresolvedPaneIds.has(paneId) ? paneId : null);
1950
+ const unresolvedWorkerPanePidsByIndex = partialWorkerPanePidsByIndex
1951
+ .map((panePid, index) => partialWorkerPaneIdsByIndex[index] && unresolvedPaneIds.has(partialWorkerPaneIdsByIndex[index]) ? panePid : null);
1952
+ const unresolvedHudPaneId = partialHudPaneId && (unresolvedPaneIds.has(partialHudPaneId)
1953
+ || registeredResizeHook !== null
1954
+ || registeredClientAttachedHook !== null)
1955
+ ? partialHudPaneId
1956
+ : null;
1957
+ const hasRecoverablePartialArtifact = unresolvedPaneIds.size > 0
1958
+ || registeredResizeHook !== null
1959
+ || registeredClientAttachedHook !== null;
1960
+ if (hasRecoverablePartialArtifact && partialTeamTarget && partialLeaderPaneId) {
1961
+ throw new CreateTeamSessionPartialError({
1962
+ name: partialTeamTarget,
1963
+ workerCount,
1964
+ cwd,
1965
+ workerPaneIds: unresolvedWorkerPaneIds,
1966
+ workerPaneIdsByIndex: unresolvedWorkerPaneIdsByIndex,
1967
+ workerPanePidsByIndex: unresolvedWorkerPanePidsByIndex,
1968
+ leaderPanePid: partialLeaderPanePid,
1969
+ hudPanePid: unresolvedHudPaneId ? partialHudPanePid : null,
1970
+ leaderPaneId: partialLeaderPaneId,
1971
+ hudPaneId: unresolvedHudPaneId,
1972
+ resizeHookName: registeredResizeHook?.name ?? null,
1973
+ resizeHookTarget: registeredResizeHook?.target ?? null,
1974
+ teamPaneOwnerId: partialTeamPaneOwnerId,
1975
+ }, proofUnavailable, error, cleanupErrors);
1369
1976
  }
1370
1977
  throw error;
1371
1978
  }
@@ -1374,34 +1981,73 @@ export function restoreStandaloneHudPane(leaderPaneId, cwd, options = {}) {
1374
1981
  const normalizedLeaderPaneId = normalizePaneTarget(leaderPaneId);
1375
1982
  if (!normalizedLeaderPaneId)
1376
1983
  return null;
1984
+ // The split is irreversible without a durable cleanup obligation. Validate
1985
+ // its canonical Team-root location before authorizing any pane effect.
1986
+ restoredHudCleanupDebtPath(cwd, options.stateRoot);
1377
1987
  const omxEntry = resolveOmxCliEntryPath();
1378
1988
  if (!omxEntry || omxEntry.trim() === '')
1379
1989
  return null;
1380
- const [existingHudPaneId, ...duplicateHudPaneIds] = findOwnedHudPaneIds(normalizedLeaderPaneId, normalizedLeaderPaneId);
1990
+ const leaderPanePid = (() => {
1991
+ const proof = readExactPaneProofSync(normalizedLeaderPaneId);
1992
+ if (proof.status === 'unavailable')
1993
+ throw new ExactPaneProofUnavailableError(proof);
1994
+ if (proof.status === 'gone')
1995
+ throw new Error(`tmux pane is not proven live: ${normalizedLeaderPaneId}`);
1996
+ if (options.expectedLeaderPanePid !== undefined && proof.pid !== options.expectedLeaderPanePid) {
1997
+ throw new Error(`tmux pane identity changed: ${normalizedLeaderPaneId}`);
1998
+ }
1999
+ return proof.pid;
2000
+ })();
2001
+ const requireAuthorizedLeaderPane = () => {
2002
+ options.assertLeaderPaneAuthorization?.();
2003
+ return requireLiveExactPaneSync(normalizedLeaderPaneId, options.expectedLeaderPanePid ?? leaderPanePid);
2004
+ };
2005
+ requireAuthorizedLeaderPane();
2006
+ const paneListResult = listPanesResult(normalizedLeaderPaneId);
2007
+ if (paneListResult.error)
2008
+ throw new Error(`failed to read tmux pane topology: ${paneListResult.error}`);
2009
+ const [existingHudPaneId, ...duplicateHudPaneIds] = findHudWatchPaneIds(paneListResult.panes, normalizedLeaderPaneId, { leaderPaneId: normalizedLeaderPaneId });
2010
+ const ownedHudPanePids = new Map();
2011
+ for (const paneId of [existingHudPaneId, ...duplicateHudPaneIds]) {
2012
+ if (!paneId)
2013
+ continue;
2014
+ const proof = readExactPaneProofSync(paneId);
2015
+ if (proof.status === 'unavailable')
2016
+ throw new ExactPaneProofUnavailableError(proof);
2017
+ if (proof.status === 'gone')
2018
+ throw new Error(`tmux pane is not proven live: ${paneId}`);
2019
+ ownedHudPanePids.set(paneId, proof.pid);
2020
+ }
1381
2021
  for (const paneId of duplicateHudPaneIds) {
1382
- runTmux(['kill-pane', '-t', paneId]);
2022
+ killExactPaneSync(paneId, ownedHudPanePids.get(paneId), requireAuthorizedLeaderPane);
1383
2023
  }
1384
2024
  if (existingHudPaneId) {
1385
2025
  if (isNativeWindows()) {
1386
- runTmux(buildHudResizeArgs(existingHudPaneId));
2026
+ const exactHudPaneId = requireLiveExactPaneSync(existingHudPaneId, ownedHudPanePids.get(existingHudPaneId));
2027
+ requireAuthorizedLeaderPane();
2028
+ const reconcile = runTmux(buildHudResizeArgs(exactHudPaneId));
2029
+ if (!reconcile.ok)
2030
+ throw new Error(`failed to reconcile standalone HUD resize: ${reconcile.stderr}`);
1387
2031
  }
1388
2032
  else {
1389
- runTmux(buildScheduleDelayedHudResizeArgs(existingHudPaneId));
1390
- runTmux(buildReconcileHudResizeArgs(existingHudPaneId));
2033
+ requireAuthorizedLeaderPane();
2034
+ runTmux(buildScheduleDelayedHudResizeArgs(existingHudPaneId, HUD_RESIZE_RECONCILE_DELAY_SECONDS, HUD_TMUX_TEAM_HEIGHT_LINES, ownedHudPanePids.get(existingHudPaneId)));
2035
+ requireAuthorizedLeaderPane();
2036
+ runTmux(buildReconcileHudResizeArgs(existingHudPaneId, HUD_TMUX_TEAM_HEIGHT_LINES, ownedHudPanePids.get(existingHudPaneId)));
1391
2037
  }
1392
- runTmux(['select-pane', '-t', normalizedLeaderPaneId]);
2038
+ runTmux(['select-pane', '-t', requireAuthorizedLeaderPane()]);
1393
2039
  return existingHudPaneId;
1394
2040
  }
1395
2041
  const hudCmd = `exec env ${formatHudEnvAssignments(process.env, { sessionId: options.sessionId, leaderPaneId: normalizedLeaderPaneId })} ${shellQuoteSingle(translatePathForMsys(resolveLeaderNodePath()))} ${shellQuoteSingle(translatePathForMsys(omxEntry))} hud --watch`;
1396
2042
  let hudResult = null;
1397
- for (const restoreCwd of resolveStandaloneHudRestoreCwdCandidates(normalizedLeaderPaneId, cwd, options.cwd)) {
2043
+ for (const restoreCwd of resolveStandaloneHudRestoreCwdCandidates(normalizedLeaderPaneId, cwd, options.cwd, requireAuthorizedLeaderPane)) {
1398
2044
  const candidateResult = runTmux([
1399
2045
  'split-window',
1400
2046
  '-v',
1401
2047
  '-l',
1402
2048
  String(HUD_TMUX_TEAM_HEIGHT_LINES),
1403
2049
  '-t',
1404
- normalizedLeaderPaneId,
2050
+ requireAuthorizedLeaderPane(),
1405
2051
  '-d',
1406
2052
  '-P',
1407
2053
  '-F',
@@ -1420,14 +2066,48 @@ export function restoreStandaloneHudPane(leaderPaneId, cwd, options = {}) {
1420
2066
  const paneId = hudResult.stdout.split('\n')[0]?.trim() ?? '';
1421
2067
  if (!paneId.startsWith('%'))
1422
2068
  return null;
2069
+ persistRestoredHudCleanupDebtSync(cwd, {
2070
+ schema_version: 1,
2071
+ operation: 'restored_hud_cleanup',
2072
+ pane_id: paneId,
2073
+ pane_pid: null,
2074
+ leader_pane_id: normalizedLeaderPaneId,
2075
+ leader_pane_pid: options.expectedLeaderPanePid ?? leaderPanePid,
2076
+ leader_pane_owner_id: options.expectedLeaderPaneOwnerId?.trim() || null,
2077
+ hud_owner_leader_pane_id: normalizedLeaderPaneId,
2078
+ }, options.stateRoot);
2079
+ const hudPanePid = (() => {
2080
+ const proof = readExactPaneProofSync(paneId);
2081
+ if (proof.status === 'unavailable')
2082
+ throw new ExactPaneProofUnavailableError(proof);
2083
+ if (proof.status === 'gone')
2084
+ throw new Error(`tmux pane is not proven live: ${paneId}`);
2085
+ return proof.pid;
2086
+ })();
2087
+ persistRestoredHudCleanupDebtSync(cwd, {
2088
+ schema_version: 1,
2089
+ operation: 'restored_hud_cleanup',
2090
+ pane_id: paneId,
2091
+ pane_pid: hudPanePid,
2092
+ leader_pane_id: normalizedLeaderPaneId,
2093
+ leader_pane_pid: options.expectedLeaderPanePid ?? leaderPanePid,
2094
+ leader_pane_owner_id: options.expectedLeaderPaneOwnerId?.trim() || null,
2095
+ hud_owner_leader_pane_id: normalizedLeaderPaneId,
2096
+ }, options.stateRoot);
1423
2097
  if (isNativeWindows()) {
1424
- runTmux(buildHudResizeArgs(paneId));
2098
+ const exactHudPaneId = requireLiveExactPaneSync(paneId, hudPanePid);
2099
+ requireAuthorizedLeaderPane();
2100
+ const reconcile = runTmux(buildHudResizeArgs(exactHudPaneId));
2101
+ if (!reconcile.ok)
2102
+ throw new Error(`failed to reconcile standalone HUD resize: ${reconcile.stderr}`);
1425
2103
  }
1426
2104
  else {
1427
- runTmux(buildScheduleDelayedHudResizeArgs(paneId));
1428
- runTmux(buildReconcileHudResizeArgs(paneId));
2105
+ requireAuthorizedLeaderPane();
2106
+ runTmux(buildScheduleDelayedHudResizeArgs(paneId, HUD_RESIZE_RECONCILE_DELAY_SECONDS, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid));
2107
+ requireAuthorizedLeaderPane();
2108
+ runTmux(buildReconcileHudResizeArgs(paneId, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid));
1429
2109
  }
1430
- runTmux(['select-pane', '-t', normalizedLeaderPaneId]);
2110
+ runTmux(['select-pane', '-t', requireAuthorizedLeaderPane()]);
1431
2111
  return paneId;
1432
2112
  }
1433
2113
  /**
@@ -1439,21 +2119,21 @@ export function restoreStandaloneHudPane(leaderPaneId, cwd, options = {}) {
1439
2119
  * oh-my-tmux, or other sessions. Returns true if the session mouse option
1440
2120
  * was set successfully, false otherwise.
1441
2121
  */
1442
- export function enableMouseScrolling(sessionTarget) {
2122
+ export function enableMouseScrolling(sessionTarget, beforeEffect) {
2123
+ beforeEffect?.();
1443
2124
  const result = runTmux(['set-option', '-t', sessionTarget, 'mouse', 'on']);
1444
2125
  if (!result.ok)
1445
2126
  return false;
1446
2127
  // Enable OSC 52 so copy-selection-and-cancel propagates selected text to
1447
2128
  // the terminal's clipboard without requiring xclip or pbcopy. (closes #206)
2129
+ beforeEffect?.();
1448
2130
  runTmux(['set-option', '-t', sessionTarget, 'set-clipboard', 'on']);
1449
2131
  // Mouse selection enters tmux copy-mode. Keep the mitigation session-scoped
1450
2132
  // so OMX does not mutate users' global tmux style defaults. (issue #1448)
1451
- mitigateCopyModeUnderlineArtifacts(sessionTarget);
2133
+ mitigateCopyModeUnderlineArtifacts(sessionTarget, beforeEffect);
1452
2134
  return true;
1453
2135
  }
1454
- function paneTarget(sessionName, workerIndex, workerPaneId) {
1455
- if (workerPaneId && workerPaneId.startsWith('%'))
1456
- return workerPaneId;
2136
+ function paneTarget(sessionName, workerIndex) {
1457
2137
  if (sessionName.includes(':')) {
1458
2138
  return `${sessionName}.${workerIndex}`;
1459
2139
  }
@@ -1499,24 +2179,50 @@ export function evaluateStartupDirectTriggerSafetyCapture(captured, workerCli) {
1499
2179
  export async function evaluateStartupDirectTriggerSafety(sessionName, workerIndex, workerPaneId, workerCli) {
1500
2180
  if (!isTmuxAvailable())
1501
2181
  return { safe: false, reason: 'tmux_unavailable' };
1502
- const target = paneTarget(sessionName, workerIndex, workerPaneId);
2182
+ const target = await resolveWorkerPaneTargetAsync(sessionName, workerIndex, workerPaneId);
2183
+ if (!target)
2184
+ return { safe: false, reason: 'capture_failed' };
1503
2185
  const result = await runTmuxAsync(sharedBuildVisibleCapturePaneArgv(target));
1504
2186
  if (!result.ok)
1505
2187
  return { safe: false, reason: 'capture_failed' };
1506
2188
  return evaluateStartupDirectTriggerSafetyCapture(result.stdout, workerCli);
1507
2189
  }
1508
- function acceptClaudeBypassPermissionsPrompt(target) {
1509
- runTmux(['send-keys', '-t', target, '-l', '--', '2']);
2190
+ function acceptClaudeBypassPermissionsPrompt(resolveTarget) {
2191
+ const literalTarget = resolveTarget();
2192
+ if (!literalTarget)
2193
+ return false;
2194
+ runTmux(['send-keys', '-t', literalTarget, '-l', '--', '2']);
1510
2195
  sleepFractionalSeconds(0.12);
1511
- runTmux(['send-keys', '-t', target, 'C-m']);
2196
+ const submitTarget = resolveTarget();
2197
+ if (!submitTarget)
2198
+ return false;
2199
+ runTmux(['send-keys', '-t', submitTarget, 'C-m']);
2200
+ return true;
1512
2201
  }
1513
- function dismissClaudeBypassPermissionsPromptIfPresent(target, captured) {
2202
+ function dismissClaudeBypassPermissionsPromptIfPresent(resolveTarget, captured) {
1514
2203
  if (process.env.OMX_TEAM_AUTO_ACCEPT_BYPASS === '0')
1515
2204
  return false;
1516
2205
  if (!paneHasClaudeBypassPermissionsPrompt(captured))
1517
2206
  return false;
1518
- acceptClaudeBypassPermissionsPrompt(target);
1519
- return true;
2207
+ return acceptClaudeBypassPermissionsPrompt(resolveTarget);
2208
+ }
2209
+ async function dismissClaudeBypassPermissionsPromptIfPresentAsync(resolveTarget, captured) {
2210
+ if (process.env.OMX_TEAM_AUTO_ACCEPT_BYPASS === '0')
2211
+ return false;
2212
+ if (!paneHasClaudeBypassPermissionsPrompt(captured))
2213
+ return false;
2214
+ const literalTarget = await resolveTarget();
2215
+ if (!literalTarget)
2216
+ return false;
2217
+ const literalSend = await runTmuxAsync(['send-keys', '-t', literalTarget, '-l', '--', '2']);
2218
+ if (!literalSend.ok)
2219
+ return false;
2220
+ await sleep(120);
2221
+ const submitTarget = await resolveTarget();
2222
+ if (!submitTarget)
2223
+ return false;
2224
+ const submitSend = await runTmuxAsync(['send-keys', '-t', submitTarget, 'C-m']);
2225
+ return submitSend.ok;
1520
2226
  }
1521
2227
  export const paneHasActiveTask = sharedPaneHasActiveTask;
1522
2228
  export function classifyWorkerStartupInjectSafety(captured) {
@@ -1533,8 +2239,8 @@ export function classifyWorkerStartupInjectSafety(captured) {
1533
2239
  return 'safe';
1534
2240
  }
1535
2241
  export async function checkWorkerStartupInjectSafety(sessionName, workerIndex, workerPaneId) {
1536
- const target = paneTarget(sessionName, workerIndex, workerPaneId);
1537
- const visibleCapture = await captureVisiblePaneAsync(target);
2242
+ const resolveTarget = () => resolveWorkerPaneTargetAsync(sessionName, workerIndex, workerPaneId);
2243
+ const visibleCapture = await captureVisiblePaneAsync(resolveTarget);
1538
2244
  const visibleSafety = classifyWorkerStartupInjectSafety(visibleCapture);
1539
2245
  if (visibleSafety === 'safe')
1540
2246
  return { safe: true, reason: 'safe' };
@@ -1543,7 +2249,7 @@ export async function checkWorkerStartupInjectSafety(sessionName, workerIndex, w
1543
2249
  if (!sharedPaneShowsCodexViewport(visibleCapture)) {
1544
2250
  return { safe: false, reason: visibleSafety };
1545
2251
  }
1546
- const scrollbackCapture = await capturePaneAsync(target);
2252
+ const scrollbackCapture = await capturePaneAsync(resolveTarget);
1547
2253
  const scrollbackSafety = classifyWorkerStartupInjectSafety(scrollbackCapture);
1548
2254
  return scrollbackSafety === 'safe'
1549
2255
  ? { safe: true, reason: 'safe' }
@@ -1621,8 +2327,9 @@ export function shouldAttemptAdaptiveRetry(strategy, paneBusyAtStart, allowAdapt
1621
2327
  return false;
1622
2328
  return true;
1623
2329
  }
1624
- function sendLiteralTextOrThrow(target, text) {
1625
- const send = runTmux(['send-keys', '-t', target, '-l', '--', text]);
2330
+ async function sendLiteralTextOrThrow(resolveTarget, text) {
2331
+ const target = await requireAsyncPaneTarget(resolveTarget);
2332
+ const send = await runTmuxAsync(['send-keys', '-t', target, '-l', '--', text]);
1626
2333
  if (!send.ok) {
1627
2334
  throw new Error(`sendToWorker: failed to send text: ${send.stderr}`);
1628
2335
  }
@@ -1634,18 +2341,18 @@ function paneHasQueuedCodexSubmission(captured) {
1634
2341
  return /messages to be submitted after next tool call/i.test(normalized)
1635
2342
  || /press esc to interrupt and send immediately/i.test(normalized);
1636
2343
  }
1637
- async function attemptSubmitRounds(target, text, rounds, queueFirstRound, submitKeyPressesPerRound) {
2344
+ async function attemptSubmitRounds(resolveTarget, text, rounds, queueFirstRound, submitKeyPressesPerRound) {
1638
2345
  const presses = Math.max(1, Math.floor(submitKeyPressesPerRound));
1639
2346
  for (let round = 0; round < rounds; round++) {
1640
2347
  await sleep(100);
1641
2348
  if (round === 0 && queueFirstRound) {
1642
- await sendKeyAsync(target, 'Tab');
2349
+ await sendKeyAsync(resolveTarget, 'Tab');
1643
2350
  await sleep(80);
1644
- await sendKeyAsync(target, 'C-m');
2351
+ await sendKeyAsync(resolveTarget, 'C-m');
1645
2352
  }
1646
2353
  else {
1647
2354
  for (let press = 0; press < presses; press++) {
1648
- await sendKeyAsync(target, 'C-m');
2355
+ await sendKeyAsync(resolveTarget, 'C-m');
1649
2356
  if (press < presses - 1) {
1650
2357
  await sleep(200);
1651
2358
  }
@@ -1653,8 +2360,8 @@ async function attemptSubmitRounds(target, text, rounds, queueFirstRound, submit
1653
2360
  }
1654
2361
  await sleep(140);
1655
2362
  const [captured, visibleCapture] = await Promise.all([
1656
- capturePaneAsync(target),
1657
- captureVisiblePaneAsync(target),
2363
+ capturePaneAsync(resolveTarget),
2364
+ captureVisiblePaneAsync(resolveTarget),
1658
2365
  ]);
1659
2366
  const normalizedCapture = normalizeWorkerTriggerForDraftMatch(captured);
1660
2367
  if (!normalizedCapture.includes(normalizeWorkerTriggerForDraftMatch(text))
@@ -1665,26 +2372,34 @@ async function attemptSubmitRounds(target, text, rounds, queueFirstRound, submit
1665
2372
  }
1666
2373
  return false;
1667
2374
  }
1668
- export function waitForWorkerReady(sessionName, workerIndex, timeoutMs = 30_000, workerPaneId) {
2375
+ export function waitForWorkerReady(sessionName, workerIndex, timeoutMs = 30_000, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
1669
2376
  const initialBackoffMs = 150;
1670
2377
  const maxBackoffMs = 8000;
1671
2378
  const startedAt = Date.now();
1672
2379
  let blockedByTrustPrompt = false;
1673
2380
  let promptDismissed = false;
2381
+ const resolveTarget = createPinnedWorkerPaneTargetResolverSync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
1674
2382
  const sendRobustEnter = () => {
1675
- const target = paneTarget(sessionName, workerIndex, workerPaneId);
1676
2383
  // Trust + follow-up splash can require two submits in Codex TUI.
1677
2384
  // Use C-m (carriage return) for raw-mode compatibility.
1678
- runTmux(['send-keys', '-t', target, 'C-m']);
2385
+ const firstTarget = resolveTarget();
2386
+ if (!firstTarget)
2387
+ return;
2388
+ runTmux(['send-keys', '-t', firstTarget, 'C-m']);
1679
2389
  sleepFractionalSeconds(0.12);
1680
- runTmux(['send-keys', '-t', target, 'C-m']);
2390
+ const secondTarget = resolveTarget();
2391
+ if (!secondTarget)
2392
+ return;
2393
+ runTmux(['send-keys', '-t', secondTarget, 'C-m']);
1681
2394
  };
1682
2395
  const check = () => {
1683
- const target = paneTarget(sessionName, workerIndex, workerPaneId);
2396
+ const target = resolveTarget();
2397
+ if (!target)
2398
+ return false;
1684
2399
  const result = runTmux(sharedBuildVisibleCapturePaneArgv(target));
1685
2400
  if (!result.ok)
1686
2401
  return false;
1687
- if (dismissClaudeBypassPermissionsPromptIfPresent(target, result.stdout)) {
2402
+ if (dismissClaudeBypassPermissionsPromptIfPresent(resolveTarget, result.stdout)) {
1688
2403
  promptDismissed = true;
1689
2404
  return false;
1690
2405
  }
@@ -1709,7 +2424,10 @@ export function waitForWorkerReady(sessionName, workerIndex, timeoutMs = 30_000,
1709
2424
  // scrollback for the prompt/helper text that may have slipped below the fold.
1710
2425
  if (!sharedPaneShowsCodexViewport(result.stdout))
1711
2426
  return false;
1712
- const scrollbackResult = runTmux(sharedBuildCapturePaneArgv(target, 80));
2427
+ const scrollbackTarget = resolveTarget();
2428
+ if (!scrollbackTarget)
2429
+ return false;
2430
+ const scrollbackResult = runTmux(sharedBuildCapturePaneArgv(scrollbackTarget, 80));
1713
2431
  if (!scrollbackResult.ok)
1714
2432
  return false;
1715
2433
  return paneLooksReady(scrollbackResult.stdout);
@@ -1737,26 +2455,34 @@ export function waitForWorkerReady(sessionName, workerIndex, timeoutMs = 30_000,
1737
2455
  // Async twin of waitForWorkerReady for team startup fan-out. Keep the readiness
1738
2456
  // semantics mirrored with the synchronous helper above, but yield between polls
1739
2457
  // so one slow worker pane cannot block later workers' startup attempts.
1740
- export async function waitForWorkerReadyAsync(sessionName, workerIndex, timeoutMs = 30_000, workerPaneId) {
2458
+ export async function waitForWorkerReadyAsync(sessionName, workerIndex, timeoutMs = 30_000, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
1741
2459
  const initialBackoffMs = 150;
1742
2460
  const maxBackoffMs = 8000;
1743
2461
  const startedAt = Date.now();
1744
2462
  let blockedByTrustPrompt = false;
1745
2463
  let promptDismissed = false;
2464
+ const resolveTarget = createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
1746
2465
  const sendRobustEnter = async () => {
1747
- const target = paneTarget(sessionName, workerIndex, workerPaneId);
1748
2466
  // Trust + follow-up splash can require two submits in Codex TUI.
1749
2467
  // Use C-m (carriage return) for raw-mode compatibility.
1750
- await runTmuxAsync(['send-keys', '-t', target, 'C-m']);
2468
+ const firstTarget = await resolveTarget();
2469
+ if (!firstTarget)
2470
+ return;
2471
+ await runTmuxAsync(['send-keys', '-t', firstTarget, 'C-m']);
1751
2472
  await sleep(120);
1752
- await runTmuxAsync(['send-keys', '-t', target, 'C-m']);
2473
+ const secondTarget = await resolveTarget();
2474
+ if (!secondTarget)
2475
+ return;
2476
+ await runTmuxAsync(['send-keys', '-t', secondTarget, 'C-m']);
1753
2477
  };
1754
2478
  const check = async () => {
1755
- const target = paneTarget(sessionName, workerIndex, workerPaneId);
2479
+ const target = await resolveTarget();
2480
+ if (!target)
2481
+ return false;
1756
2482
  const result = await runTmuxAsync(sharedBuildVisibleCapturePaneArgv(target));
1757
2483
  if (!result.ok)
1758
2484
  return false;
1759
- if (dismissClaudeBypassPermissionsPromptIfPresent(target, result.stdout)) {
2485
+ if (await dismissClaudeBypassPermissionsPromptIfPresentAsync(resolveTarget, result.stdout)) {
1760
2486
  promptDismissed = true;
1761
2487
  return false;
1762
2488
  }
@@ -1781,7 +2507,10 @@ export async function waitForWorkerReadyAsync(sessionName, workerIndex, timeoutM
1781
2507
  // scrollback for the prompt/helper text that may have slipped below the fold.
1782
2508
  if (!sharedPaneShowsCodexViewport(result.stdout))
1783
2509
  return false;
1784
- const scrollbackResult = await runTmuxAsync(sharedBuildCapturePaneArgv(target, 80));
2510
+ const scrollbackTarget = await resolveTarget();
2511
+ if (!scrollbackTarget)
2512
+ return false;
2513
+ const scrollbackResult = await runTmuxAsync(sharedBuildCapturePaneArgv(scrollbackTarget, 80));
1785
2514
  if (!scrollbackResult.ok)
1786
2515
  return false;
1787
2516
  return paneLooksReady(scrollbackResult.stdout);
@@ -1811,21 +2540,30 @@ export async function waitForWorkerReadyAsync(sessionName, workerIndex, timeoutM
1811
2540
  * Returns true if a trust prompt was found and dismissed, false otherwise.
1812
2541
  * Opt-out: set OMX_TEAM_AUTO_TRUST=0 to disable auto-dismissal.
1813
2542
  */
1814
- export function dismissTrustPromptIfPresent(sessionName, workerIndex, workerPaneId) {
2543
+ export function dismissTrustPromptIfPresent(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
1815
2544
  if (process.env.OMX_TEAM_AUTO_TRUST === '0')
1816
2545
  return false;
1817
2546
  if (!isTmuxAvailable())
1818
2547
  return false;
1819
- const target = paneTarget(sessionName, workerIndex, workerPaneId);
1820
- const result = runTmux(sharedBuildVisibleCapturePaneArgv(target));
2548
+ const resolveTarget = createPinnedWorkerPaneTargetResolverSync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
2549
+ const captureTarget = resolveTarget();
2550
+ if (!captureTarget)
2551
+ return false;
2552
+ const result = runTmux(sharedBuildVisibleCapturePaneArgv(captureTarget));
1821
2553
  if (!result.ok)
1822
2554
  return false;
1823
2555
  if (!paneHasTrustPrompt(result.stdout))
1824
2556
  return false;
1825
2557
  // Trust prompt detected; send C-m twice to dismiss (trust + follow-up splash)
1826
- runTmux(['send-keys', '-t', target, 'C-m']);
2558
+ const firstTarget = resolveTarget();
2559
+ if (!firstTarget)
2560
+ return false;
2561
+ runTmux(['send-keys', '-t', firstTarget, 'C-m']);
1827
2562
  sleepFractionalSeconds(0.12);
1828
- runTmux(['send-keys', '-t', target, 'C-m']);
2563
+ const secondTarget = resolveTarget();
2564
+ if (!secondTarget)
2565
+ return false;
2566
+ runTmux(['send-keys', '-t', secondTarget, 'C-m']);
1829
2567
  return true;
1830
2568
  }
1831
2569
  export const normalizeTmuxCapture = sharedNormalizeTmuxCapture;
@@ -1856,49 +2594,49 @@ export function sendToWorkerStdin(stdin, text) {
1856
2594
  // Send SHORT text (<200 chars) to worker via tmux send-keys
1857
2595
  // Validates: text < 200 chars, no injection marker
1858
2596
  // Throws on violation
1859
- export async function sendToWorker(sessionName, workerIndex, text, workerPaneId, workerCli) {
2597
+ export async function sendToWorker(sessionName, workerIndex, text, workerPaneId, workerCli, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
1860
2598
  assertWorkerTriggerText(text);
1861
- const target = paneTarget(sessionName, workerIndex, workerPaneId);
2599
+ const resolveTarget = createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
1862
2600
  const strategy = resolveSendStrategyFromEnv();
1863
2601
  const resolvedWorkerCli = resolveWorkerCliForSend(workerIndex, workerCli);
1864
2602
  // Guard: if the trust prompt is still present, advance it first so our trigger text
1865
2603
  // doesn't get typed into the trust screen and ignored.
1866
- const capturedStr = await capturePaneAsync(target);
2604
+ const capturedStr = await capturePaneAsync(resolveTarget);
1867
2605
  const paneBusy = paneHasActiveTask(capturedStr);
1868
- if (dismissClaudeBypassPermissionsPromptIfPresent(target, capturedStr)) {
2606
+ if (await dismissClaudeBypassPermissionsPromptIfPresentAsync(resolveTarget, capturedStr)) {
1869
2607
  await sleep(200);
1870
2608
  }
1871
2609
  if (paneHasTrustPrompt(capturedStr)) {
1872
- await sendKeyAsync(target, 'C-m');
2610
+ await sendKeyAsync(resolveTarget, 'C-m');
1873
2611
  await sleep(120);
1874
- await sendKeyAsync(target, 'C-m');
2612
+ await sendKeyAsync(resolveTarget, 'C-m');
1875
2613
  await sleep(200);
1876
2614
  }
1877
- sendLiteralTextOrThrow(target, text);
2615
+ await sendLiteralTextOrThrow(resolveTarget, text);
1878
2616
  // Allow the input buffer to settle before sending C-m
1879
2617
  await sleep(150);
1880
2618
  const allowAutoInterruptRetry = process.env[OMX_TEAM_AUTO_INTERRUPT_RETRY_ENV] !== '0';
1881
2619
  const submitPlan = buildWorkerSubmitPlan(strategy, resolvedWorkerCli, paneBusy, allowAutoInterruptRetry);
1882
2620
  if (submitPlan.shouldInterrupt) {
1883
2621
  // Explicit interrupt mode: abort current turn first, then submit the new command.
1884
- await sendKeyAsync(target, 'C-c');
2622
+ await sendKeyAsync(resolveTarget, 'C-c');
1885
2623
  await sleep(100);
1886
2624
  }
1887
2625
  // Submit deterministically using CLI-specific plan:
1888
2626
  // - Codex: queue-first Tab+C-m when configured/busy, then double C-m rounds.
1889
2627
  // - Claude: direct C-m rounds only (never queue-first Tab).
1890
- if (await attemptSubmitRounds(target, text, submitPlan.rounds, submitPlan.queueFirstRound, submitPlan.submitKeyPressesPerRound))
2628
+ if (await attemptSubmitRounds(resolveTarget, text, submitPlan.rounds, submitPlan.queueFirstRound, submitPlan.submitKeyPressesPerRound))
1891
2629
  return;
1892
2630
  // Adaptive escalation for "likely unsent trigger text at ready prompt" cases:
1893
2631
  // clear line, re-send trigger, then re-submit with deterministic C-m rounds.
1894
- const latestCapture = await capturePaneAsync(target);
2632
+ const latestCapture = await capturePaneAsync(resolveTarget);
1895
2633
  if (shouldAttemptAdaptiveRetry(strategy, paneBusy, submitPlan.allowAdaptiveRetry, latestCapture || null, text)) {
1896
2634
  // Keep this branch non-interrupting to avoid canceling active turns on false positives.
1897
- await sendKeyAsync(target, 'C-u');
2635
+ await sendKeyAsync(resolveTarget, 'C-u');
1898
2636
  await sleep(80);
1899
- sendLiteralTextOrThrow(target, text);
2637
+ await sendLiteralTextOrThrow(resolveTarget, text);
1900
2638
  await sleep(120);
1901
- if (await attemptSubmitRounds(target, text, 4, false, submitPlan.submitKeyPressesPerRound))
2639
+ if (await attemptSubmitRounds(resolveTarget, text, 4, false, submitPlan.submitKeyPressesPerRound))
1902
2640
  return;
1903
2641
  }
1904
2642
  // Fail-open by default: Codex may keep the last submitted line visible even after executing it.
@@ -1908,15 +2646,15 @@ export async function sendToWorker(sessionName, workerIndex, text, workerPaneId,
1908
2646
  throw new Error('sendToWorker: submit_failed (trigger text still visible after retries)');
1909
2647
  }
1910
2648
  // One last best-effort double C-m nudge, then verify.
1911
- await sendKeyAsync(target, 'C-m');
2649
+ await sendKeyAsync(resolveTarget, 'C-m');
1912
2650
  await sleep(120);
1913
- await sendKeyAsync(target, 'C-m');
2651
+ await sendKeyAsync(resolveTarget, 'C-m');
1914
2652
  // Post-submit verification: wait briefly and confirm the worker consumed the
1915
2653
  // trigger (draft disappeared or active-task indicator appeared). Fixes #391.
1916
2654
  await sleep(300);
1917
2655
  const [verifyCapture, verifyVisibleCapture] = await Promise.all([
1918
- capturePaneAsync(target),
1919
- captureVisiblePaneAsync(target),
2656
+ capturePaneAsync(resolveTarget),
2657
+ captureVisiblePaneAsync(resolveTarget),
1920
2658
  ]);
1921
2659
  if (verifyCapture) {
1922
2660
  if (paneHasActiveTask(verifyCapture))
@@ -1926,14 +2664,14 @@ export async function sendToWorker(sessionName, workerIndex, text, workerPaneId,
1926
2664
  return;
1927
2665
  }
1928
2666
  // Draft still visible and no active task — one more C-m attempt.
1929
- await sendKeyAsync(target, 'C-m');
2667
+ await sendKeyAsync(resolveTarget, 'C-m');
1930
2668
  await sleep(150);
1931
- await sendKeyAsync(target, 'C-m');
1932
- const finalVisibleCapture = await captureVisiblePaneAsync(target);
2669
+ await sendKeyAsync(resolveTarget, 'C-m');
2670
+ const finalVisibleCapture = await captureVisiblePaneAsync(resolveTarget);
1933
2671
  if (paneHasQueuedCodexSubmission(finalVisibleCapture)) {
1934
2672
  throw new Error('sendToWorker: submit_queued_after_tool_call');
1935
2673
  }
1936
- const finalCapture = await capturePaneAsync(target);
2674
+ const finalCapture = await capturePaneAsync(resolveTarget);
1937
2675
  if (normalizeWorkerTriggerForDraftMatch(finalCapture).includes(normalizeWorkerTriggerForDraftMatch(text))
1938
2676
  && !paneHasActiveTask(finalCapture)
1939
2677
  && paneLooksReady(finalCapture)) {
@@ -1952,8 +2690,12 @@ export function notifyLeaderStatus(sessionName, message) {
1952
2690
  return result.ok;
1953
2691
  }
1954
2692
  // Get PID of the shell process in a worker's tmux pane
1955
- export function getWorkerPanePid(sessionName, workerIndex, workerPaneId) {
1956
- const result = runTmux(['list-panes', '-t', paneTarget(sessionName, workerIndex, workerPaneId), '-F', '#{pane_pid}']);
2693
+ export function getWorkerPanePid(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
2694
+ if (hasExplicitWorkerPaneId(workerPaneId)) {
2695
+ const target = createPinnedWorkerPaneTargetResolverSync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId)();
2696
+ return target ? expectedPanePid ?? null : null;
2697
+ }
2698
+ const result = runTmux(['list-panes', '-t', paneTarget(sessionName, workerIndex), '-F', '#{pane_pid}']);
1957
2699
  if (!result.ok)
1958
2700
  return null;
1959
2701
  const firstLine = result.stdout.split('\n')[0]?.trim();
@@ -1965,15 +2707,16 @@ export function getWorkerPanePid(sessionName, workerIndex, workerPaneId) {
1965
2707
  return pid;
1966
2708
  }
1967
2709
  // Check if worker's tmux pane has a running process
1968
- export function isWorkerAlive(sessionName, workerIndex, workerPaneId) {
1969
- if (workerPaneId?.startsWith('%')) {
1970
- const paneStatus = readPaneLivenessById(workerPaneId);
1971
- if (paneStatus !== null)
1972
- return paneStatus;
2710
+ export function isWorkerAlive(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
2711
+ if (hasExplicitWorkerPaneId(workerPaneId)) {
2712
+ const target = createPinnedWorkerPaneTargetResolverSync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId)();
2713
+ if (!target || typeof expectedPanePid !== 'number')
2714
+ return false;
2715
+ return probeProcessLiveness(expectedPanePid) !== 'gone';
1973
2716
  }
1974
2717
  const result = runTmux([
1975
2718
  'list-panes',
1976
- '-t', paneTarget(sessionName, workerIndex, workerPaneId),
2719
+ '-t', paneTarget(sessionName, workerIndex),
1977
2720
  '-F',
1978
2721
  '#{pane_dead} #{pane_pid}',
1979
2722
  ]);
@@ -1991,23 +2734,17 @@ export function isWorkerAlive(sessionName, workerIndex, workerPaneId) {
1991
2734
  return false;
1992
2735
  if (!Number.isFinite(pid))
1993
2736
  return false;
1994
- try {
1995
- process.kill(pid, 0);
1996
- return true;
1997
- }
1998
- catch {
1999
- return false;
2000
- }
2737
+ // Unknown is conservatively live so callers cannot classify a permission or
2738
+ // transient failure as stale cleanup authority.
2739
+ return probeProcessLiveness(pid) !== 'gone';
2001
2740
  }
2002
- export function isWorkerPaneOpen(sessionName, workerIndex, workerPaneId) {
2003
- if (workerPaneId?.startsWith('%')) {
2004
- const paneStatus = readPaneLivenessById(workerPaneId);
2005
- if (paneStatus !== null)
2006
- return paneStatus;
2741
+ export function isWorkerPaneOpen(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
2742
+ if (hasExplicitWorkerPaneId(workerPaneId)) {
2743
+ return createPinnedWorkerPaneTargetResolverSync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId)() !== null;
2007
2744
  }
2008
2745
  const result = runTmux([
2009
2746
  'list-panes',
2010
- '-t', paneTarget(sessionName, workerIndex, workerPaneId),
2747
+ '-t', paneTarget(sessionName, workerIndex),
2011
2748
  '-F',
2012
2749
  '#{pane_dead}',
2013
2750
  ]);
@@ -2020,28 +2757,69 @@ export function isWorkerPaneOpen(sessionName, workerIndex, workerPaneId) {
2020
2757
  }
2021
2758
  // Kill a specific worker: send C-c, then C-d, then kill-pane if still alive.
2022
2759
  // leaderPaneId: when provided, the kill is skipped entirely if workerPaneId matches it.
2023
- export async function killWorker(sessionName, workerIndex, workerPaneId, leaderPaneId) {
2760
+ export async function killWorker(sessionName, workerIndex, workerPaneId, leaderPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
2024
2761
  // Guard: never kill the leader's own pane.
2025
2762
  if (leaderPaneId && workerPaneId === leaderPaneId)
2026
2763
  return;
2027
- await runTmuxAsync(['send-keys', '-t', paneTarget(sessionName, workerIndex, workerPaneId), 'C-c']);
2764
+ const resolveTarget = createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
2765
+ const initialTarget = await resolveTarget();
2766
+ if (!initialTarget)
2767
+ return;
2768
+ if (await isWorkerAliveAsync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId, resolveTarget) !== 'alive')
2769
+ return;
2770
+ await runTmuxAsync(['send-keys', '-t', initialTarget, 'C-c']);
2028
2771
  await sleep(1000);
2029
- if (await isWorkerAliveAsync(sessionName, workerIndex, workerPaneId)) {
2030
- await runTmuxAsync(['send-keys', '-t', paneTarget(sessionName, workerIndex, workerPaneId), 'C-d']);
2031
- await sleep(1000);
2032
- }
2033
- if (await isWorkerAliveAsync(sessionName, workerIndex, workerPaneId)) {
2034
- await runTmuxAsync(['kill-pane', '-t', paneTarget(sessionName, workerIndex, workerPaneId)]);
2772
+ if (await isWorkerAliveAsync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId, resolveTarget) === 'alive') {
2773
+ const exitTarget = await resolveTarget();
2774
+ if (exitTarget) {
2775
+ await runTmuxAsync(['send-keys', '-t', exitTarget, 'C-d']);
2776
+ await sleep(1000);
2777
+ }
2035
2778
  }
2036
- }
2037
- // leaderPaneId: when provided, the kill is skipped if workerPaneId matches it.
2038
- export function killWorkerByPaneId(workerPaneId, leaderPaneId) {
2039
- if (!workerPaneId.startsWith('%'))
2779
+ if (await isWorkerAliveAsync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId, resolveTarget) === 'alive') {
2780
+ const killTarget = await resolveTarget();
2781
+ if (!killTarget)
2782
+ return;
2783
+ const killed = await runTmuxAsync(['kill-pane', '-t', killTarget]);
2784
+ if (!killed.ok)
2785
+ throw new Error(`failed to kill tmux pane ${killTarget}: ${killed.stderr}`);
2786
+ const absence = await readExactPaneProof(killTarget);
2787
+ if (absence.status === 'unavailable')
2788
+ throw new ExactPaneProofUnavailableError(absence);
2789
+ if (absence.status !== 'gone')
2790
+ throw new Error(`tmux pane remains live after kill: ${killTarget}`);
2791
+ }
2792
+ }
2793
+ // Explicit pane targets require frozen PID, canonical Team owner, and HUD
2794
+ // exclusion. A successful kill is only accepted after a fresh absence proof.
2795
+ export function killWorkerByPaneId(workerPaneId, expectedPanePid, leaderPaneId, expectedTeamOwnerId, hudPaneId) {
2796
+ const expectedOwner = typeof expectedTeamOwnerId === 'string' ? expectedTeamOwnerId.trim() : '';
2797
+ if (!hasExplicitWorkerPaneId(workerPaneId)
2798
+ || typeof expectedPanePid !== 'number'
2799
+ || !Number.isSafeInteger(expectedPanePid)
2800
+ || expectedPanePid <= 0
2801
+ || !expectedOwner
2802
+ || workerPaneId === hudPaneId)
2040
2803
  return;
2041
- // Guard: never kill the leader's own pane.
2042
2804
  if (leaderPaneId && workerPaneId === leaderPaneId)
2043
2805
  return;
2044
- runTmux(['kill-pane', '-t', workerPaneId]);
2806
+ const proof = readExactPaneProofSync(workerPaneId);
2807
+ if (proof.status !== 'live' || proof.pid !== expectedPanePid)
2808
+ return;
2809
+ const owner = readPaneTeamOwnerTagResult(proof.paneId);
2810
+ if (owner.status !== 'value' || owner.value !== expectedOwner)
2811
+ return;
2812
+ const finalProof = readExactPaneProofSync(proof.paneId);
2813
+ if (finalProof.status !== 'live' || finalProof.pid !== expectedPanePid)
2814
+ return;
2815
+ const result = runTmux(['kill-pane', '-t', finalProof.paneId]);
2816
+ if (!result.ok)
2817
+ throw new Error(`failed to kill tmux pane ${finalProof.paneId}: ${result.stderr}`);
2818
+ const absence = readExactPaneProofSync(finalProof.paneId);
2819
+ if (absence.status === 'unavailable')
2820
+ throw new ExactPaneProofUnavailableError(absence);
2821
+ if (absence.status !== 'gone')
2822
+ throw new Error(`tmux pane remains live after kill: ${finalProof.paneId}`);
2045
2823
  }
2046
2824
  export function paneHasOmxInstanceTag(paneId, instanceId) {
2047
2825
  const normalizedPaneId = normalizePaneTarget(paneId);
@@ -2092,13 +2870,34 @@ export function readPaneTeamOwnerTagResult(paneId) {
2092
2870
  return { status: 'missing' };
2093
2871
  return { status: 'error', error: stderr || `tmux show-option exited ${result.status ?? 'unknown'}` };
2094
2872
  }
2095
- export async function killWorkerByPaneIdAsync(workerPaneId, leaderPaneId) {
2096
- if (!workerPaneId.startsWith('%'))
2873
+ export async function killWorkerByPaneIdAsync(workerPaneId, expectedPanePid, leaderPaneId, expectedTeamOwnerId, hudPaneId) {
2874
+ const expectedOwner = typeof expectedTeamOwnerId === 'string' ? expectedTeamOwnerId.trim() : '';
2875
+ if (!hasExplicitWorkerPaneId(workerPaneId)
2876
+ || typeof expectedPanePid !== 'number'
2877
+ || !Number.isSafeInteger(expectedPanePid)
2878
+ || expectedPanePid <= 0
2879
+ || !expectedOwner
2880
+ || workerPaneId === hudPaneId)
2097
2881
  return;
2098
- // Guard: never kill the leader's own pane.
2099
2882
  if (leaderPaneId && workerPaneId === leaderPaneId)
2100
2883
  return;
2101
- await runTmuxAsync(['kill-pane', '-t', workerPaneId]);
2884
+ const proof = await readExactPaneProof(workerPaneId);
2885
+ if (proof.status !== 'live' || proof.pid !== expectedPanePid)
2886
+ return;
2887
+ const owner = readPaneTeamOwnerTagResult(proof.paneId);
2888
+ if (owner.status !== 'value' || owner.value !== expectedOwner)
2889
+ return;
2890
+ const finalProof = await readExactPaneProof(proof.paneId);
2891
+ if (finalProof.status !== 'live' || finalProof.pid !== expectedPanePid)
2892
+ return;
2893
+ const result = await runTmuxAsync(['kill-pane', '-t', finalProof.paneId]);
2894
+ if (!result.ok)
2895
+ throw new Error(`failed to kill tmux pane ${finalProof.paneId}: ${result.stderr}`);
2896
+ const absence = await readExactPaneProof(finalProof.paneId);
2897
+ if (absence.status === 'unavailable')
2898
+ throw new ExactPaneProofUnavailableError(absence);
2899
+ if (absence.status !== 'gone')
2900
+ throw new Error(`tmux pane remains live after kill: ${finalProof.paneId}`);
2102
2901
  }
2103
2902
  function normalizePaneTarget(value) {
2104
2903
  if (typeof value !== 'string')
@@ -2136,7 +2935,10 @@ function normalizePaneTargets(paneIds, options = {}) {
2136
2935
  return { killablePaneIds, excluded };
2137
2936
  }
2138
2937
  export function resolveSharedSessionShutdownTopology(sessionName, preferredLeaderPaneId, teamName) {
2139
- const panes = listPanes(sessionName);
2938
+ const paneList = listPanesResult(sessionName);
2939
+ if (paneList.error !== null)
2940
+ return { status: 'unavailable', detail: paneList.error };
2941
+ const panes = paneList.panes;
2140
2942
  const livePaneIds = panes
2141
2943
  .map((pane) => normalizePaneTarget(pane.paneId))
2142
2944
  .filter((paneId) => Boolean(paneId));
@@ -2144,6 +2946,7 @@ export function resolveSharedSessionShutdownTopology(sessionName, preferredLeade
2144
2946
  if (panes.length === 0) {
2145
2947
  return {
2146
2948
  livePaneIds,
2949
+ status: 'available',
2147
2950
  teamWorkerPaneIds: [],
2148
2951
  leaderPaneId: fallbackLeaderPaneId,
2149
2952
  hudPaneIds: [],
@@ -2174,6 +2977,7 @@ export function resolveSharedSessionShutdownTopology(sessionName, preferredLeade
2174
2977
  : [];
2175
2978
  return {
2176
2979
  livePaneIds,
2980
+ status: 'available',
2177
2981
  teamWorkerPaneIds: normalizedTeamWorkerPaneIds,
2178
2982
  leaderPaneId: resolvedLeaderPaneId,
2179
2983
  hudPaneIds,
@@ -2288,7 +3092,8 @@ function escapeRegExp(value) {
2288
3092
  }
2289
3093
  /**
2290
3094
  * Shared pane-id-direct teardown primitive for worker pane cleanup.
2291
- * Must remain liveness-agnostic: do not gate on isWorkerAlive/killWorker.
3095
+ * A fresh exact global proof is required before each kill; proven-gone panes
3096
+ * are teardown-compatible while unavailable proofs remain fail-closed.
2292
3097
  */
2293
3098
  export async function teardownWorkerPanes(paneIds, options = {}) {
2294
3099
  const { killablePaneIds, excluded } = normalizePaneTargets(paneIds, options);
@@ -2299,45 +3104,247 @@ export async function teardownWorkerPanes(paneIds, options = {}) {
2299
3104
  const summary = {
2300
3105
  attemptedPaneIds: killablePaneIds,
2301
3106
  excluded,
3107
+ provenGonePaneIds: [],
3108
+ killedPaneIds: [],
3109
+ proofUnavailable: [],
2302
3110
  kill: {
2303
- attempted: killablePaneIds.length,
3111
+ attempted: 0,
2304
3112
  succeeded: 0,
2305
3113
  failed: 0,
3114
+ failedPaneIds: [],
2306
3115
  },
2307
3116
  };
2308
3117
  for (const paneId of killablePaneIds) {
2309
- const result = await runTmuxAsync(['kill-pane', '-t', paneId]);
2310
- if (result.ok)
3118
+ const proof = await readExactPaneProof(paneId);
3119
+ const expectedPid = options.expectedPanePids?.[paneId];
3120
+ if (proof.status === 'gone') {
3121
+ summary.provenGonePaneIds.push(proof.paneId);
3122
+ continue;
3123
+ }
3124
+ if (proof.status === 'unavailable') {
3125
+ summary.proofUnavailable.push(proof);
3126
+ break;
3127
+ }
3128
+ if (typeof expectedPid !== 'number' || !Number.isSafeInteger(expectedPid) || expectedPid <= 0) {
3129
+ summary.proofUnavailable.push({
3130
+ status: 'unavailable',
3131
+ paneId,
3132
+ reason: 'pane_pid_changed',
3133
+ detail: 'expected positive pane PID is required before teardown kill',
3134
+ });
3135
+ break;
3136
+ }
3137
+ if (proof.pid !== expectedPid) {
3138
+ summary.proofUnavailable.push({
3139
+ status: 'unavailable',
3140
+ paneId,
3141
+ reason: 'pane_pid_changed',
3142
+ detail: `expected ${expectedPid}, got ${proof.pid}`,
3143
+ });
3144
+ break;
3145
+ }
3146
+ if (options.authorizePaneKill && !options.authorizePaneKill(proof.paneId, proof)) {
3147
+ summary.proofUnavailable.push({
3148
+ status: 'unavailable',
3149
+ paneId: proof.paneId,
3150
+ reason: 'pane_pid_changed',
3151
+ detail: 'pane owner authorization changed',
3152
+ });
3153
+ break;
3154
+ }
3155
+ // Owner authorization can read tmux. Re-prove the persisted identity after
3156
+ // it and immediately before kill-pane.
3157
+ const finalProof = await readExactPaneProof(proof.paneId);
3158
+ if (finalProof.status === 'gone') {
3159
+ summary.provenGonePaneIds.push(finalProof.paneId);
3160
+ continue;
3161
+ }
3162
+ if (finalProof.status === 'unavailable') {
3163
+ summary.proofUnavailable.push(finalProof);
3164
+ break;
3165
+ }
3166
+ if (finalProof.pid !== expectedPid) {
3167
+ summary.proofUnavailable.push({
3168
+ status: 'unavailable',
3169
+ paneId: finalProof.paneId,
3170
+ reason: 'pane_pid_changed',
3171
+ detail: `expected ${expectedPid}, got ${finalProof.pid}`,
3172
+ });
3173
+ break;
3174
+ }
3175
+ summary.kill.attempted += 1;
3176
+ const result = await runTmuxAsync(['kill-pane', '-t', finalProof.paneId]);
3177
+ if (!result.ok) {
3178
+ summary.kill.failed += 1;
3179
+ summary.kill.failedPaneIds.push(proof.paneId);
3180
+ await sleep(perPaneGrace);
3181
+ break;
3182
+ }
3183
+ const afterKill = await readExactPaneProof(finalProof.paneId);
3184
+ if (afterKill.status === 'gone') {
2311
3185
  summary.kill.succeeded += 1;
2312
- else
3186
+ summary.killedPaneIds.push(proof.paneId);
3187
+ }
3188
+ else if (afterKill.status === 'unavailable') {
3189
+ summary.proofUnavailable.push(afterKill);
3190
+ break;
3191
+ }
3192
+ else {
2313
3193
  summary.kill.failed += 1;
3194
+ summary.kill.failedPaneIds.push(proof.paneId);
3195
+ await sleep(perPaneGrace);
3196
+ break;
3197
+ }
2314
3198
  await sleep(perPaneGrace);
2315
3199
  }
2316
3200
  return summary;
2317
3201
  }
2318
- export async function killWorkerPanes(paneIds, leaderPaneId, graceMs = 2000, hudPaneId) {
2319
- return teardownWorkerPanes(paneIds, { leaderPaneId, hudPaneId: hudPaneId ?? null, graceMs });
2320
- }
2321
- // Kill entire tmux session. Tolerates already-dead sessions.
2322
- export function destroyTeamSession(sessionName) {
2323
- try {
2324
- runTmux(['kill-session', '-t', sessionName]);
2325
- }
2326
- catch {
2327
- // tolerate
2328
- }
3202
+ export async function killWorkerPanes(paneIds, leaderPaneId, graceMs = 2000, hudPaneId, expectedPanePids) {
3203
+ return teardownWorkerPanes(paneIds, {
3204
+ leaderPaneId,
3205
+ hudPaneId: hudPaneId ?? null,
3206
+ graceMs,
3207
+ expectedPanePids,
3208
+ });
2329
3209
  }
2330
- // List all tmux sessions matching omx-team-* pattern
2331
- export function listTeamSessions() {
2332
- const result = runTmux(['list-sessions', '-F', '#{session_name}']);
2333
- if (!result.ok)
2334
- return [];
2335
- return result.stdout
3210
+ // Kill an entire detached tmux session only when the caller has already
3211
+ // established ownership. Success requires tmux to accept the kill and a fresh
3212
+ // session-list proof that the exact base session is absent.
3213
+ function parseTeamSessionNames(stdout) {
3214
+ return stdout
2336
3215
  .split('\n')
2337
3216
  .map(line => line.trim())
2338
3217
  .filter(Boolean)
2339
3218
  .map(baseSessionName);
2340
3219
  }
3220
+ function isFinalTmuxServerGone(result) {
3221
+ return !result.ok && /^no server running on .+$/i.test(result.stderr);
3222
+ }
3223
+ /**
3224
+ * Reads pane and session incarnation in one tmux snapshot. This prevents a
3225
+ * same-name replacement from combining a stale session proof with a live pane.
3226
+ */
3227
+ export function queryDetachedSessionLeaderBinding(paneId, pid, sessionName, expected) {
3228
+ const result = runTmuxStructured(['list-panes', '-a', '-F', '#{pane_id}\t#{pane_dead}\t#{pane_pid}\t#{session_name}\t#{session_id}\t#{session_created}']);
3229
+ if (!result.ok)
3230
+ return null;
3231
+ const seenPaneIds = new Set();
3232
+ let binding = null;
3233
+ for (const line of result.stdout.split('\n').filter(Boolean)) {
3234
+ const fields = line.split('\t');
3235
+ if (fields.length !== 6)
3236
+ return null;
3237
+ const [currentPaneId, dead, panePid, currentSessionName, sessionId, sessionCreated] = fields;
3238
+ if (!/^%[0-9]+$/.test(currentPaneId) || seenPaneIds.has(currentPaneId) || (dead !== '0' && dead !== '1')
3239
+ || !/^[0-9]+$/.test(panePid) || !Number.isSafeInteger(Number(panePid)) || Number(panePid) <= 0
3240
+ || !currentSessionName || !/^\$[0-9]+$/.test(sessionId) || !/^[0-9]+$/.test(sessionCreated))
3241
+ return null;
3242
+ seenPaneIds.add(currentPaneId);
3243
+ if (currentPaneId !== paneId)
3244
+ continue;
3245
+ if (dead !== '0' || Number(panePid) !== pid || currentSessionName !== sessionName
3246
+ || sessionId !== expected.sessionId || sessionCreated !== expected.sessionCreated)
3247
+ return null;
3248
+ binding = { paneId: currentPaneId, pid: Number(panePid), sessionName: currentSessionName, incarnation: { sessionId, sessionCreated } };
3249
+ }
3250
+ return binding;
3251
+ }
3252
+ function parseDetachedSessionIncarnation(stdout, sessionName) {
3253
+ const baseName = baseSessionName(sessionName);
3254
+ const matches = [];
3255
+ for (const line of stdout.split('\n').filter(Boolean)) {
3256
+ const fields = line.split('\t');
3257
+ if (fields.length !== 3)
3258
+ return 'malformed';
3259
+ const [name, sessionId, sessionCreated] = fields;
3260
+ if (!name || !sessionId || !sessionCreated || !/^\$[0-9]+$/.test(sessionId) || !/^[0-9]+$/.test(sessionCreated))
3261
+ return 'malformed';
3262
+ if (baseSessionName(name) === baseName)
3263
+ matches.push({ sessionId, sessionCreated });
3264
+ }
3265
+ return matches.length === 0 ? null : matches.length === 1 ? matches[0] : 'malformed';
3266
+ }
3267
+ /**
3268
+ * Reads stable tmux incarnation evidence. Name-only presence is never effect
3269
+ * authority: malformed or unavailable evidence fails closed, and an expected
3270
+ * incarnation distinguishes an exact survivor from a recycled same-name session.
3271
+ */
3272
+ export function queryDetachedTeamSession(sessionName, expected) {
3273
+ const sessions = runTmuxStructured(['list-sessions', '-F', '#{session_name}\t#{session_id}\t#{session_created}']);
3274
+ if (!sessions.ok) {
3275
+ return isFinalTmuxServerGone(sessions)
3276
+ ? { status: 'absent' }
3277
+ : { status: 'unavailable', detail: sessions.stderr };
3278
+ }
3279
+ const incarnation = parseDetachedSessionIncarnation(sessions.stdout, sessionName);
3280
+ if (incarnation === 'malformed')
3281
+ return { status: 'unavailable', detail: 'malformed_session_incarnation' };
3282
+ if (incarnation === null)
3283
+ return { status: 'absent' };
3284
+ if (expected && (incarnation.sessionId !== expected.sessionId || incarnation.sessionCreated !== expected.sessionCreated)) {
3285
+ return { status: 'replacement', incarnation };
3286
+ }
3287
+ return { status: 'exact', incarnation };
3288
+ }
3289
+ /**
3290
+ * Re-proves every stable authorization component at the destructive sink. The
3291
+ * final tmux command queues an in-server predicate with the kill, so no client
3292
+ * can replace the session or pane between authorization and the effect.
3293
+ */
3294
+ function buildDetachedSessionDestroyArgs(expected) {
3295
+ const leaderPaneId = expected.leaderPaneId.trim();
3296
+ const ownerId = expected.ownerId.trim();
3297
+ if (!/^%[0-9]+$/.test(leaderPaneId)
3298
+ || !Number.isSafeInteger(expected.leaderPanePid)
3299
+ || expected.leaderPanePid <= 0
3300
+ || !/^\$[0-9]+$/.test(expected.sessionId)
3301
+ || !/^[0-9]+$/.test(expected.sessionCreated)
3302
+ || !/^[A-Za-z0-9._:@/-]+$/.test(ownerId))
3303
+ return null;
3304
+ const predicate = [
3305
+ '#{==:#{pane_dead},0}',
3306
+ `#{==:#{pane_id},${leaderPaneId}}`,
3307
+ `#{==:#{pane_pid},${expected.leaderPanePid}}`,
3308
+ `#{==:#{${OMX_TEAM_PANE_OWNER_OPTION}},${ownerId}}`,
3309
+ `#{==:#{session_id},${expected.sessionId}}`,
3310
+ `#{==:#{session_created},${expected.sessionCreated}}`,
3311
+ ].reduce((combined, condition) => `#{&&:${combined},${condition}}`);
3312
+ return ['if-shell', '-F', '-t', leaderPaneId, predicate, `kill-session -t ${expected.sessionId}`, 'run-shell "exit 1"'];
3313
+ }
3314
+ export function requestDetachedTeamSessionDestroy(sessionName, expected) {
3315
+ const incarnation = { sessionId: expected.sessionId, sessionCreated: expected.sessionCreated };
3316
+ if (queryDetachedTeamSession(sessionName, incarnation).status !== 'exact')
3317
+ return false;
3318
+ if (!queryDetachedSessionLeaderBinding(expected.leaderPaneId, expected.leaderPanePid, sessionName, incarnation))
3319
+ return false;
3320
+ const pane = readExactPaneProofSync(expected.leaderPaneId);
3321
+ if (pane.status !== 'live' || pane.pid !== expected.leaderPanePid)
3322
+ return false;
3323
+ const owner = readPaneTeamOwnerTagResult(expected.leaderPaneId);
3324
+ if (owner.status !== 'value' || owner.value !== expected.ownerId)
3325
+ return false;
3326
+ const args = buildDetachedSessionDestroyArgs(expected);
3327
+ return args !== null && runTmux(args).ok;
3328
+ }
3329
+ export function destroyTeamSession(sessionName) {
3330
+ const result = runTmux(['kill-session', '-t', sessionName]);
3331
+ if (!result.ok)
3332
+ return false;
3333
+ const sessions = runTmux(['list-sessions', '-F', '#{session_name}']);
3334
+ // Killing tmux's final session also terminates its server. Its documented
3335
+ // no-server response is the only post-kill query failure that proves absence.
3336
+ if (!sessions.ok)
3337
+ return isFinalTmuxServerGone(sessions);
3338
+ return !parseTeamSessionNames(sessions.stdout).includes(baseSessionName(sessionName));
3339
+ }
3340
+ // A failed query is not a successful empty session list. Destructive callers
3341
+ // must require the latter before treating a detached session as absent.
3342
+ export function listTeamSessions() {
3343
+ const result = runTmux(['list-sessions', '-F', '#{session_name}']);
3344
+ if (!result.ok)
3345
+ return null;
3346
+ return parseTeamSessionNames(result.stdout);
3347
+ }
2341
3348
  /**
2342
3349
  * Notify the leader through durable mailbox state only.
2343
3350
  *