oh-my-codex 0.20.2 → 0.20.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (367) hide show
  1. package/Cargo.lock +7 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +26 -0
  4. package/crates/omx-runtime/Cargo.toml +1 -0
  5. package/crates/omx-runtime/src/main.rs +25 -2
  6. package/crates/omx-runtime/tests/execution.rs +219 -0
  7. package/crates/omx-runtime-core/src/dispatch.rs +96 -10
  8. package/crates/omx-runtime-core/src/engine.rs +839 -29
  9. package/crates/omx-runtime-core/src/lib.rs +8 -0
  10. package/dist/agents/__tests__/definitions.test.js +81 -0
  11. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  12. package/dist/agents/__tests__/native-config.test.js +25 -0
  13. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  14. package/dist/agents/native-config.d.ts +2 -1
  15. package/dist/agents/native-config.d.ts.map +1 -1
  16. package/dist/agents/native-config.js.map +1 -1
  17. package/dist/auth/hotswap.d.ts +6 -0
  18. package/dist/auth/hotswap.d.ts.map +1 -1
  19. package/dist/auth/hotswap.js +85 -15
  20. package/dist/auth/hotswap.js.map +1 -1
  21. package/dist/autopilot/__tests__/ralplan-gate.test.js +1 -1
  22. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  23. package/dist/cli/__tests__/auth.test.js +60 -6
  24. package/dist/cli/__tests__/auth.test.js.map +1 -1
  25. package/dist/cli/__tests__/codex-plugin-layout.test.js +80 -0
  26. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  27. package/dist/cli/__tests__/doctor-warning-copy.test.js +103 -1
  28. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  29. package/dist/cli/__tests__/exec.test.js +136 -0
  30. package/dist/cli/__tests__/exec.test.js.map +1 -1
  31. package/dist/cli/__tests__/index.test.js +258 -4
  32. package/dist/cli/__tests__/index.test.js.map +1 -1
  33. package/dist/cli/__tests__/launch-fallback.test.js +287 -1
  34. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  35. package/dist/cli/__tests__/mcp-parity.test.js +4 -1
  36. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
  37. package/dist/cli/__tests__/native-hook-claim-journal.test.js +84 -6
  38. package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
  39. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +3 -3
  40. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  41. package/dist/cli/__tests__/ralph.test.js +3 -3
  42. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  43. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +56 -171
  44. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
  45. package/dist/cli/__tests__/ralplan.test.js +37 -111
  46. package/dist/cli/__tests__/ralplan.test.js.map +1 -1
  47. package/dist/cli/__tests__/resume.test.js +11 -10
  48. package/dist/cli/__tests__/resume.test.js.map +1 -1
  49. package/dist/cli/__tests__/session-search-help.test.js +25 -1
  50. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  51. package/dist/cli/__tests__/setup-install-mode.test.js +42 -4
  52. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  53. package/dist/cli/__tests__/team.test.js +105 -7
  54. package/dist/cli/__tests__/team.test.js.map +1 -1
  55. package/dist/cli/__tests__/uninstall.test.js +46 -0
  56. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  57. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +2 -1
  58. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  59. package/dist/cli/doctor.d.ts +3 -0
  60. package/dist/cli/doctor.d.ts.map +1 -1
  61. package/dist/cli/doctor.js +15 -2
  62. package/dist/cli/doctor.js.map +1 -1
  63. package/dist/cli/index.d.ts +4 -1
  64. package/dist/cli/index.d.ts.map +1 -1
  65. package/dist/cli/index.js +138 -38
  66. package/dist/cli/index.js.map +1 -1
  67. package/dist/cli/mcp-parity.js +2 -2
  68. package/dist/cli/mcp-parity.js.map +1 -1
  69. package/dist/cli/native-hook-claim-journal.d.ts +16 -5
  70. package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
  71. package/dist/cli/native-hook-claim-journal.js +42 -29
  72. package/dist/cli/native-hook-claim-journal.js.map +1 -1
  73. package/dist/cli/ralph.js +2 -2
  74. package/dist/cli/ralph.js.map +1 -1
  75. package/dist/cli/ralplan.d.ts +4 -8
  76. package/dist/cli/ralplan.d.ts.map +1 -1
  77. package/dist/cli/ralplan.js +19 -112
  78. package/dist/cli/ralplan.js.map +1 -1
  79. package/dist/cli/session-search.d.ts +5 -0
  80. package/dist/cli/session-search.d.ts.map +1 -1
  81. package/dist/cli/session-search.js +87 -0
  82. package/dist/cli/session-search.js.map +1 -1
  83. package/dist/cli/setup.d.ts +5 -0
  84. package/dist/cli/setup.d.ts.map +1 -1
  85. package/dist/cli/setup.js +80 -31
  86. package/dist/cli/setup.js.map +1 -1
  87. package/dist/cli/uninstall.d.ts +9 -0
  88. package/dist/cli/uninstall.d.ts.map +1 -1
  89. package/dist/cli/uninstall.js +61 -19
  90. package/dist/cli/uninstall.js.map +1 -1
  91. package/dist/compat/__tests__/rust-runtime-compat.test.js +1 -1
  92. package/dist/config/__tests__/generator-idempotent.test.js +330 -1
  93. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  94. package/dist/config/__tests__/generator-notify.test.js +2 -2
  95. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  96. package/dist/config/__tests__/models.test.js +67 -3
  97. package/dist/config/__tests__/models.test.js.map +1 -1
  98. package/dist/config/generator.d.ts +4 -20
  99. package/dist/config/generator.d.ts.map +1 -1
  100. package/dist/config/generator.js +499 -119
  101. package/dist/config/generator.js.map +1 -1
  102. package/dist/config/models.d.ts +12 -2
  103. package/dist/config/models.d.ts.map +1 -1
  104. package/dist/config/models.js +15 -1
  105. package/dist/config/models.js.map +1 -1
  106. package/dist/hooks/__tests__/agents-overlay.test.js +2 -2
  107. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  108. package/dist/hooks/__tests__/keyword-detector.test.js +4 -4
  109. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  110. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +238 -543
  111. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  112. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +38 -4
  113. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  114. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +96 -9
  115. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  116. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +531 -12
  117. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  118. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +155 -23
  119. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  120. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +482 -32
  121. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  122. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +68 -1
  123. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  124. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +7 -1
  125. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
  126. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +43 -4
  127. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  128. package/dist/hooks/__tests__/session.test.js +348 -2
  129. package/dist/hooks/__tests__/session.test.js.map +1 -1
  130. package/dist/hooks/agents-overlay.js +1 -1
  131. package/dist/hooks/session.d.ts +26 -2
  132. package/dist/hooks/session.d.ts.map +1 -1
  133. package/dist/hooks/session.js +169 -13
  134. package/dist/hooks/session.js.map +1 -1
  135. package/dist/leader/__tests__/contract.test.js +57 -7
  136. package/dist/leader/__tests__/contract.test.js.map +1 -1
  137. package/dist/leader/contract.d.ts +16 -0
  138. package/dist/leader/contract.d.ts.map +1 -1
  139. package/dist/leader/contract.js +79 -4
  140. package/dist/leader/contract.js.map +1 -1
  141. package/dist/mcp/__tests__/code-intel-server.test.js +31 -3
  142. package/dist/mcp/__tests__/code-intel-server.test.js.map +1 -1
  143. package/dist/mcp/__tests__/wiki-server.test.js +18 -0
  144. package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
  145. package/dist/mcp/code-intel-server.d.ts.map +1 -1
  146. package/dist/mcp/code-intel-server.js +13 -3
  147. package/dist/mcp/code-intel-server.js.map +1 -1
  148. package/dist/mcp/wiki-server.d.ts +11 -0
  149. package/dist/mcp/wiki-server.d.ts.map +1 -1
  150. package/dist/mcp/wiki-server.js +9 -2
  151. package/dist/mcp/wiki-server.js.map +1 -1
  152. package/dist/modes/__tests__/base-tmux-pane.test.js +4 -4
  153. package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
  154. package/dist/notifications/temp-contract.d.ts.map +1 -1
  155. package/dist/notifications/temp-contract.js +4 -0
  156. package/dist/notifications/temp-contract.js.map +1 -1
  157. package/dist/pipeline/__tests__/orchestrator.test.js +4 -4
  158. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  159. package/dist/pipeline/__tests__/stages.test.js +8 -8
  160. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  161. package/dist/ralplan/__tests__/consensus-gate.test.js +134 -10
  162. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
  163. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts +2 -0
  164. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts.map +1 -0
  165. package/dist/ralplan/__tests__/documented-leader-preflight.test.js +46 -0
  166. package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -0
  167. package/dist/ralplan/__tests__/runtime.test.js +8 -4
  168. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  169. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  170. package/dist/ralplan/consensus-gate.js +58 -50
  171. package/dist/ralplan/consensus-gate.js.map +1 -1
  172. package/dist/ralplan/documented-leader-preflight.d.ts +34 -0
  173. package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -0
  174. package/dist/ralplan/documented-leader-preflight.js +58 -0
  175. package/dist/ralplan/documented-leader-preflight.js.map +1 -0
  176. package/dist/ralplan/runtime.d.ts +1 -0
  177. package/dist/ralplan/runtime.d.ts.map +1 -1
  178. package/dist/ralplan/runtime.js +11 -7
  179. package/dist/ralplan/runtime.js.map +1 -1
  180. package/dist/runtime/__tests__/bridge.test.js +42 -1
  181. package/dist/runtime/__tests__/bridge.test.js.map +1 -1
  182. package/dist/runtime/bridge.d.ts +12 -0
  183. package/dist/runtime/bridge.d.ts.map +1 -1
  184. package/dist/runtime/bridge.js +52 -1
  185. package/dist/runtime/bridge.js.map +1 -1
  186. package/dist/scripts/__tests__/codex-native-hook.test.js +2620 -487
  187. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  188. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts +2 -0
  189. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts.map +1 -0
  190. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +108 -0
  191. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -0
  192. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +46 -173
  193. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
  194. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +39 -159
  195. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
  196. package/dist/scripts/__tests__/smoke-packed-install.test.js +254 -2
  197. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  198. package/dist/scripts/codex-native-hook.d.ts +4 -1
  199. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  200. package/dist/scripts/codex-native-hook.js +11013 -1153
  201. package/dist/scripts/codex-native-hook.js.map +1 -1
  202. package/dist/scripts/notify-fallback-watcher.js +82 -108
  203. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  204. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  205. package/dist/scripts/notify-hook/auto-nudge.js +74 -3
  206. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  207. package/dist/scripts/notify-hook/team-dispatch.d.ts +9 -0
  208. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  209. package/dist/scripts/notify-hook/team-dispatch.js +234 -79
  210. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  211. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  212. package/dist/scripts/notify-hook/team-leader-nudge.js +39 -13
  213. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  214. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +12 -4
  215. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  216. package/dist/scripts/notify-hook/team-tmux-guard.js +365 -44
  217. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  218. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  219. package/dist/scripts/notify-hook/team-worker-stop.js +19 -18
  220. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  221. package/dist/scripts/notify-hook/team-worker.d.ts +3 -0
  222. package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
  223. package/dist/scripts/notify-hook/team-worker.js +37 -28
  224. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  225. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  226. package/dist/scripts/notify-hook/tmux-injection.js +28 -2
  227. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  228. package/dist/scripts/notify-hook.js +1 -0
  229. package/dist/scripts/notify-hook.js.map +1 -1
  230. package/dist/scripts/smoke-packed-install.d.ts +48 -3
  231. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  232. package/dist/scripts/smoke-packed-install.js +2796 -13
  233. package/dist/scripts/smoke-packed-install.js.map +1 -1
  234. package/dist/state/__tests__/mode-state-context.test.js +54 -0
  235. package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
  236. package/dist/state/__tests__/operations.test.js +2 -0
  237. package/dist/state/__tests__/operations.test.js.map +1 -1
  238. package/dist/state/mode-state-context.d.ts +5 -0
  239. package/dist/state/mode-state-context.d.ts.map +1 -1
  240. package/dist/state/mode-state-context.js +59 -0
  241. package/dist/state/mode-state-context.js.map +1 -1
  242. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +24 -245
  243. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
  244. package/dist/subagents/tracker.d.ts +1 -82
  245. package/dist/subagents/tracker.d.ts.map +1 -1
  246. package/dist/subagents/tracker.js +3 -275
  247. package/dist/subagents/tracker.js.map +1 -1
  248. package/dist/team/__tests__/api-interop.test.js +91 -1
  249. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  250. package/dist/team/__tests__/delivery-e2e-smoke.test.js +52 -10
  251. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  252. package/dist/team/__tests__/idle-nudge.test.js +45 -15
  253. package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
  254. package/dist/team/__tests__/mcp-comm.test.js +93 -27
  255. package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
  256. package/dist/team/__tests__/model-contract.test.js +110 -3
  257. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  258. package/dist/team/__tests__/runtime-cli.test.js +9 -0
  259. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  260. package/dist/team/__tests__/runtime.test.js +3204 -335
  261. package/dist/team/__tests__/runtime.test.js.map +1 -1
  262. package/dist/team/__tests__/scaling.test.js +2359 -142
  263. package/dist/team/__tests__/scaling.test.js.map +1 -1
  264. package/dist/team/__tests__/state.test.js +804 -36
  265. package/dist/team/__tests__/state.test.js.map +1 -1
  266. package/dist/team/__tests__/team-ops-contract.test.js +6 -0
  267. package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
  268. package/dist/team/__tests__/tmux-session.test.js +3049 -784
  269. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  270. package/dist/team/__tests__/worker-runtime-identity.test.js +38 -2
  271. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  272. package/dist/team/__tests__/worktree.test.js +6 -0
  273. package/dist/team/__tests__/worktree.test.js.map +1 -1
  274. package/dist/team/api-interop.d.ts.map +1 -1
  275. package/dist/team/api-interop.js +15 -9
  276. package/dist/team/api-interop.js.map +1 -1
  277. package/dist/team/exact-pane.d.ts +23 -0
  278. package/dist/team/exact-pane.d.ts.map +1 -0
  279. package/dist/team/exact-pane.js +133 -0
  280. package/dist/team/exact-pane.js.map +1 -0
  281. package/dist/team/idle-nudge.d.ts +12 -1
  282. package/dist/team/idle-nudge.d.ts.map +1 -1
  283. package/dist/team/idle-nudge.js +41 -14
  284. package/dist/team/idle-nudge.js.map +1 -1
  285. package/dist/team/model-contract.d.ts +2 -5
  286. package/dist/team/model-contract.d.ts.map +1 -1
  287. package/dist/team/model-contract.js +59 -17
  288. package/dist/team/model-contract.js.map +1 -1
  289. package/dist/team/runtime.d.ts +1 -0
  290. package/dist/team/runtime.d.ts.map +1 -1
  291. package/dist/team/runtime.js +1244 -180
  292. package/dist/team/runtime.js.map +1 -1
  293. package/dist/team/scaling.d.ts +13 -1
  294. package/dist/team/scaling.d.ts.map +1 -1
  295. package/dist/team/scaling.js +1430 -384
  296. package/dist/team/scaling.js.map +1 -1
  297. package/dist/team/state/dispatch.d.ts.map +1 -1
  298. package/dist/team/state/dispatch.js +26 -8
  299. package/dist/team/state/dispatch.js.map +1 -1
  300. package/dist/team/state/locks.d.ts.map +1 -1
  301. package/dist/team/state/locks.js +61 -13
  302. package/dist/team/state/locks.js.map +1 -1
  303. package/dist/team/state/tasks.d.ts.map +1 -1
  304. package/dist/team/state/tasks.js +4 -3
  305. package/dist/team/state/tasks.js.map +1 -1
  306. package/dist/team/state.d.ts +67 -1
  307. package/dist/team/state.d.ts.map +1 -1
  308. package/dist/team/state.js +636 -129
  309. package/dist/team/state.js.map +1 -1
  310. package/dist/team/team-ops.d.ts +5 -0
  311. package/dist/team/team-ops.d.ts.map +1 -1
  312. package/dist/team/team-ops.js +4 -0
  313. package/dist/team/team-ops.js.map +1 -1
  314. package/dist/team/tmux-session.d.ts +123 -23
  315. package/dist/team/tmux-session.d.ts.map +1 -1
  316. package/dist/team/tmux-session.js +1250 -291
  317. package/dist/team/tmux-session.js.map +1 -1
  318. package/dist/team/worktree.d.ts.map +1 -1
  319. package/dist/team/worktree.js +4 -0
  320. package/dist/team/worktree.js.map +1 -1
  321. package/dist/utils/__tests__/file-durability.test.d.ts +2 -0
  322. package/dist/utils/__tests__/file-durability.test.d.ts.map +1 -0
  323. package/dist/utils/__tests__/file-durability.test.js +57 -0
  324. package/dist/utils/__tests__/file-durability.test.js.map +1 -0
  325. package/dist/utils/file-durability.d.ts +15 -0
  326. package/dist/utils/file-durability.d.ts.map +1 -0
  327. package/dist/utils/file-durability.js +37 -0
  328. package/dist/utils/file-durability.js.map +1 -0
  329. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +3 -0
  330. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
  331. package/dist/wiki/lint.d.ts +4 -1
  332. package/dist/wiki/lint.d.ts.map +1 -1
  333. package/dist/wiki/lint.js +3 -2
  334. package/dist/wiki/lint.js.map +1 -1
  335. package/package.json +1 -1
  336. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  337. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +24 -0
  338. package/plugins/oh-my-codex/skills/ralph/SKILL.md +6 -4
  339. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +3 -2
  340. package/plugins/oh-my-codex/skills/team/SKILL.md +13 -9
  341. package/skills/ralph/SKILL.md +6 -4
  342. package/skills/ralplan/SKILL.md +3 -2
  343. package/skills/team/SKILL.md +13 -9
  344. package/src/scripts/__tests__/codex-native-hook.test.ts +2826 -497
  345. package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +129 -0
  346. package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +48 -185
  347. package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +44 -177
  348. package/src/scripts/__tests__/smoke-packed-install.test.ts +320 -2
  349. package/src/scripts/codex-native-hook.ts +10715 -1069
  350. package/src/scripts/notify-fallback-watcher.ts +84 -115
  351. package/src/scripts/notify-hook/auto-nudge.ts +71 -3
  352. package/src/scripts/notify-hook/team-dispatch.ts +242 -75
  353. package/src/scripts/notify-hook/team-leader-nudge.ts +40 -18
  354. package/src/scripts/notify-hook/team-tmux-guard.ts +366 -39
  355. package/src/scripts/notify-hook/team-worker-stop.ts +20 -16
  356. package/src/scripts/notify-hook/team-worker.ts +39 -26
  357. package/src/scripts/notify-hook/tmux-injection.ts +28 -2
  358. package/src/scripts/notify-hook.ts +1 -0
  359. package/src/scripts/smoke-packed-install.ts +3196 -14
  360. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +0 -2
  361. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +0 -1
  362. package/dist/subagents/__tests__/adapted-role-binding.test.js +0 -988
  363. package/dist/subagents/__tests__/adapted-role-binding.test.js.map +0 -1
  364. package/dist/subagents/adapted-role-binding.d.ts +0 -17
  365. package/dist/subagents/adapted-role-binding.d.ts.map +0 -1
  366. package/dist/subagents/adapted-role-binding.js +0 -75
  367. package/dist/subagents/adapted-role-binding.js.map +0 -1
@@ -74,6 +74,11 @@ if [[ "$cmd" == "display-message" ]]; then
74
74
  fi
75
75
  exit 0
76
76
  fi
77
+ if [[ "$cmd" == "show-option" && "$*" == *"@omx_team_pane_owner_id" ]]; then
78
+ echo "team:alpha"
79
+ exit 0
80
+ fi
81
+
77
82
  if [[ "$cmd" == "set-buffer" ]]; then
78
83
  printf '%s' "\${@: -1}" > "${tmuxLogPath}.buffer"
79
84
  exit 0
@@ -103,12 +108,49 @@ if [[ "$cmd" == "send-keys" ]]; then
103
108
  exit 0
104
109
  fi
105
110
  if [[ "$cmd" == "list-panes" ]]; then
111
+ if [[ "$*" == *"#{pane_dead}"* ]]; then
112
+ if [[ -n "\${OMX_TEST_EXACT_PANE_SEQUENCE_FILE:-}" && -f "\${OMX_TEST_EXACT_PANE_SEQUENCE_FILE}" ]]; then
113
+ counterFile="\${OMX_TEST_EXACT_PANE_COUNTER_FILE:-\${OMX_TEST_EXACT_PANE_SEQUENCE_FILE}.idx}"
114
+ idx=0
115
+ if [[ -f "$counterFile" ]]; then idx="$(cat "$counterFile")"; fi
116
+ lineNo=$((idx + 1))
117
+ line="$(sed -n "\${lineNo}p" "\${OMX_TEST_EXACT_PANE_SEQUENCE_FILE}" || true)"
118
+ if [[ -z "$line" ]]; then
119
+ line="$(tail -n 1 "\${OMX_TEST_EXACT_PANE_SEQUENCE_FILE}" || true)"
120
+ fi
121
+ echo "$lineNo" > "$counterFile"
122
+ if [[ "$line" != "__EMPTY__" ]]; then printf '%b\n' "$line"; fi
123
+ exit 0
124
+ fi
125
+ printf '%%42\t0\t4242\n%%99\t0\t9999\n'
126
+ exit 0
127
+ fi
106
128
  echo "%42 1"
107
129
  exit 0
108
130
  fi
109
131
  exit 0
110
132
  `;
111
133
  }
134
+ function assertFreshExactProofBeforePaneEffects(tmuxLog, paneId) {
135
+ const commands = tmuxLog.trim().split('\n').filter(Boolean);
136
+ const exactGlobalPaneProof = /^list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}$/;
137
+ const ownerProof = new RegExp(`^show-option -qv -p -t ${paneId} @omx_team_pane_owner_id$`);
138
+ const effects = commands
139
+ .map((command, index) => ({ command, index }))
140
+ .filter(({ command }) => (command.startsWith(`paste-buffer -t ${paneId} `)
141
+ || command.startsWith(`kill-pane -t ${paneId} `)
142
+ || (command.startsWith(`send-keys -t ${paneId} `) && !command.includes(' -l '))));
143
+ assert.ok(effects.length > 0, `expected explicit pane effects for ${paneId}:\n${commands.join('\n')}`);
144
+ for (const { command, index } of effects) {
145
+ assert.match(commands[index - 1] ?? '', exactGlobalPaneProof, `final PID proof must immediately precede ${command}`);
146
+ assert.match(commands[index - 2] ?? '', ownerProof, `owner proof must immediately precede the final PID proof for ${command}`);
147
+ assert.match(commands[index - 3] ?? '', exactGlobalPaneProof, `initial PID proof must precede owner proof for ${command}`);
148
+ }
149
+ }
150
+ function bindCanonicalTeamPaneAuthority(config, hudPaneId = '%88') {
151
+ config.tmux_pane_owner_id = 'team:alpha';
152
+ config.hud_pane_id = hudPaneId;
153
+ }
112
154
  async function readTeamDeliveryLog(cwd) {
113
155
  const path = join(cwd, '.omx', 'logs', `team-delivery-${new Date().toISOString().slice(0, 10)}.jsonl`);
114
156
  const raw = await readFile(path, 'utf-8').catch(() => '');
@@ -579,6 +621,8 @@ exit 0
579
621
  if (!cfg)
580
622
  throw new Error('missing team config');
581
623
  cfg.leader_pane_id = '%99';
624
+ cfg.leader_pane_pid = 9999;
625
+ bindCanonicalTeamPaneAuthority(cfg);
582
626
  await saveTeamConfig(cfg, cwd);
583
627
  const msg = await sendDirectMessage('alpha', 'worker-1', 'leader-fixed', 'hello leader', cwd);
584
628
  await enqueueDispatchRequest('alpha', {
@@ -592,6 +636,7 @@ exit 0
592
636
  const result = await mod.drainPendingTeamDispatch({ cwd, maxPerTick: 5 });
593
637
  assert.equal(result.processed, 1);
594
638
  const tmuxLog = await readFile(tmuxLogPath, 'utf8');
639
+ assertFreshExactProofBeforePaneEffects(tmuxLog, '%99');
595
640
  assert.match(tmuxLog, /send-keys -t %99/);
596
641
  assert.match(tmuxLog, /mailbox\/leader-fixed\.json; worker-1 sent a new message/);
597
642
  assert.doesNotMatch(tmuxLog, /send-keys -t .*devsess/);
@@ -604,7 +649,207 @@ exit 0
604
649
  await rm(cwd, { recursive: true, force: true });
605
650
  }
606
651
  });
607
- it('leader-fixed dispatch prefers the canonical codex pane over a stale HUD leader pane id', async () => {
652
+ it('uses the persisted worker pane as an exact identity without generic resolution', async () => {
653
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-hook-team-dispatch-'));
654
+ const fakeBinDir = join(cwd, 'fake-bin');
655
+ const tmuxLogPath = join(cwd, 'tmux.log');
656
+ const previousPath = process.env.PATH;
657
+ try {
658
+ await mkdir(fakeBinDir, { recursive: true });
659
+ await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
660
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
661
+ process.env.PATH = `${fakeBinDir}:${previousPath || ''}`;
662
+ await initTeamState('alpha', 'task', 'executor', 1, cwd);
663
+ const cfg = await readTeamConfig('alpha', cwd);
664
+ assert.ok(cfg);
665
+ if (!cfg)
666
+ throw new Error('missing team config');
667
+ cfg.workers[0].pane_id = '%99';
668
+ cfg.workers[0].pid = 9999;
669
+ bindCanonicalTeamPaneAuthority(cfg);
670
+ await saveTeamConfig(cfg, cwd);
671
+ await enqueueDispatchRequest('alpha', {
672
+ kind: 'inbox',
673
+ to_worker: 'worker-1',
674
+ worker_index: 1,
675
+ trigger_message: 'use the configured worker pane',
676
+ }, cwd);
677
+ const modulePath = new URL('../../../dist/scripts/notify-hook/team-dispatch.js', import.meta.url).pathname;
678
+ const mod = await import(pathToFileURL(modulePath).href);
679
+ const result = await mod.drainPendingTeamDispatch({ cwd, maxPerTick: 5 });
680
+ assert.equal(result.processed, 1);
681
+ const tmuxLog = await readFile(tmuxLogPath, 'utf8');
682
+ assertFreshExactProofBeforePaneEffects(tmuxLog, '%99');
683
+ assert.match(tmuxLog, /send-keys -t %99/);
684
+ assert.doesNotMatch(tmuxLog, /display-message -p -t %99 #\{pane_id\}/);
685
+ assert.doesNotMatch(tmuxLog, /list-panes -t /);
686
+ }
687
+ finally {
688
+ if (typeof previousPath === 'string')
689
+ process.env.PATH = previousPath;
690
+ else
691
+ delete process.env.PATH;
692
+ await rm(cwd, { recursive: true, force: true });
693
+ }
694
+ });
695
+ it('fails closed without a canonical PID for explicit worker and leader panes', async () => {
696
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-hook-missing-pane-pid-'));
697
+ try {
698
+ await initTeamState('alpha', 'task', 'executor', 1, cwd);
699
+ const config = await readTeamConfig('alpha', cwd);
700
+ assert.ok(config?.workers[0]);
701
+ if (!config?.workers[0])
702
+ throw new Error('missing worker');
703
+ config.workers[0].pane_id = '%42';
704
+ config.leader_pane_id = '%99';
705
+ await saveTeamConfig(config, cwd);
706
+ const worker = await enqueueDispatchRequest('alpha', {
707
+ kind: 'inbox', to_worker: 'worker-1', worker_index: 1, trigger_message: 'do not send',
708
+ }, cwd);
709
+ const leader = await enqueueDispatchRequest('alpha', {
710
+ kind: 'nudge', to_worker: 'leader-fixed', trigger_message: 'leader do not send',
711
+ }, cwd);
712
+ const modulePath = new URL('../../../dist/scripts/notify-hook/team-dispatch.js', import.meta.url).pathname;
713
+ const mod = await import(pathToFileURL(modulePath).href);
714
+ const result = await mod.drainPendingTeamDispatch({ cwd, maxPerTick: 5 });
715
+ assert.equal(result.failed, 2);
716
+ assert.equal((await readDispatchRequest('alpha', worker.request.request_id, cwd))?.last_reason, 'missing_exact_pane_pid');
717
+ assert.equal((await readDispatchRequest('alpha', leader.request.request_id, cwd))?.last_reason, 'missing_exact_pane_pid');
718
+ }
719
+ finally {
720
+ await rm(cwd, { recursive: true, force: true });
721
+ }
722
+ });
723
+ it('does not send when a guarded worker pane is reused before input effects', async () => {
724
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-hook-pane-pid-reuse-'));
725
+ const fakeBinDir = join(cwd, 'fake-bin');
726
+ const tmuxLogPath = join(cwd, 'tmux.log');
727
+ const proofSequencePath = join(cwd, 'exact-pane-sequence.txt');
728
+ const previousPath = process.env.PATH;
729
+ try {
730
+ await mkdir(fakeBinDir, { recursive: true });
731
+ await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
732
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
733
+ await writeFile(proofSequencePath, [
734
+ '%42\\t0\\t4242', '%42\\t0\\t4242', '%42\\t0\\t4242',
735
+ '%42\\t0\\t4242', '%42\\t0\\t4242', '%42\\t0\\t4242',
736
+ '%42\\t0\\t5252',
737
+ ].join('\n'));
738
+ process.env.PATH = `${fakeBinDir}:${previousPath || ''}`;
739
+ process.env.OMX_TEST_EXACT_PANE_SEQUENCE_FILE = proofSequencePath;
740
+ await initTeamState('alpha', 'task', 'executor', 1, cwd);
741
+ const config = await readTeamConfig('alpha', cwd);
742
+ assert.ok(config?.workers[0]);
743
+ if (!config?.workers[0])
744
+ throw new Error('missing worker');
745
+ config.workers[0].pane_id = '%42';
746
+ config.workers[0].pid = 4242;
747
+ bindCanonicalTeamPaneAuthority(config);
748
+ await saveTeamConfig(config, cwd);
749
+ const queued = await enqueueDispatchRequest('alpha', {
750
+ kind: 'inbox', to_worker: 'worker-1', worker_index: 1, trigger_message: 'do not send',
751
+ }, cwd);
752
+ const modulePath = new URL('../../../dist/scripts/notify-hook/team-dispatch.js', import.meta.url).pathname;
753
+ const mod = await import(pathToFileURL(modulePath).href);
754
+ const result = await mod.drainPendingTeamDispatch({ cwd, maxPerTick: 5 });
755
+ assert.equal(result.failed, 1);
756
+ assert.equal((await readDispatchRequest('alpha', queued.request.request_id, cwd))?.last_reason, 'exact_pane_unavailable');
757
+ const tmuxLog = await readFile(tmuxLogPath, 'utf8');
758
+ assert.doesNotMatch(tmuxLog, /(?:paste-buffer|send-keys) -t %42/, 'replacement pane must receive no input effect');
759
+ }
760
+ finally {
761
+ if (typeof previousPath === 'string')
762
+ process.env.PATH = previousPath;
763
+ else
764
+ delete process.env.PATH;
765
+ delete process.env.OMX_TEST_EXACT_PANE_SEQUENCE_FILE;
766
+ delete process.env.OMX_TEST_EXACT_PANE_COUNTER_FILE;
767
+ await rm(cwd, { recursive: true, force: true });
768
+ }
769
+ });
770
+ it('fails closed before tmux effects when request and configured panes differ', async () => {
771
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-hook-team-dispatch-'));
772
+ const fakeBinDir = join(cwd, 'fake-bin');
773
+ const tmuxLogPath = join(cwd, 'tmux.log');
774
+ const previousPath = process.env.PATH;
775
+ try {
776
+ await mkdir(fakeBinDir, { recursive: true });
777
+ await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
778
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
779
+ process.env.PATH = `${fakeBinDir}:${previousPath || ''}`;
780
+ await initTeamState('alpha', 'task', 'executor', 1, cwd);
781
+ const cfg = await readTeamConfig('alpha', cwd);
782
+ assert.ok(cfg);
783
+ if (!cfg)
784
+ throw new Error('missing team config');
785
+ cfg.workers[0].pane_id = '%99';
786
+ await saveTeamConfig(cfg, cwd);
787
+ const queued = await enqueueDispatchRequest('alpha', {
788
+ kind: 'inbox',
789
+ to_worker: 'worker-1',
790
+ worker_index: 1,
791
+ pane_id: '%42',
792
+ trigger_message: 'must not be redirected',
793
+ }, cwd);
794
+ const modulePath = new URL('../../../dist/scripts/notify-hook/team-dispatch.js', import.meta.url).pathname;
795
+ const mod = await import(pathToFileURL(modulePath).href);
796
+ const result = await mod.drainPendingTeamDispatch({ cwd, maxPerTick: 5 });
797
+ assert.equal(result.failed, 1);
798
+ const request = await readDispatchRequest('alpha', queued.request.request_id, cwd);
799
+ assert.equal(request?.status, 'failed');
800
+ assert.equal(request?.last_reason, 'exact_pane_mismatch');
801
+ assert.equal(await readFile(tmuxLogPath, 'utf8').catch(() => ''), '');
802
+ }
803
+ finally {
804
+ if (typeof previousPath === 'string')
805
+ process.env.PATH = previousPath;
806
+ else
807
+ delete process.env.PATH;
808
+ await rm(cwd, { recursive: true, force: true });
809
+ }
810
+ });
811
+ it('fails an invalid request pane without session or index fallback', async () => {
812
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-hook-team-dispatch-'));
813
+ const fakeBinDir = join(cwd, 'fake-bin');
814
+ const tmuxLogPath = join(cwd, 'tmux.log');
815
+ const previousPath = process.env.PATH;
816
+ try {
817
+ await mkdir(fakeBinDir, { recursive: true });
818
+ await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
819
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
820
+ process.env.PATH = `${fakeBinDir}:${previousPath || ''}`;
821
+ await initTeamState('alpha', 'task', 'executor', 1, cwd);
822
+ const cfg = await readTeamConfig('alpha', cwd);
823
+ assert.ok(cfg);
824
+ if (!cfg)
825
+ throw new Error('missing team config');
826
+ cfg.tmux_session = 'must-not-resolve';
827
+ delete cfg.workers[0].pane_id;
828
+ await saveTeamConfig(cfg, cwd);
829
+ const queued = await enqueueDispatchRequest('alpha', {
830
+ kind: 'inbox',
831
+ to_worker: 'worker-1',
832
+ worker_index: 1,
833
+ pane_id: 'invalid-pane-id',
834
+ trigger_message: 'must not be sent',
835
+ }, cwd);
836
+ const modulePath = new URL('../../../dist/scripts/notify-hook/team-dispatch.js', import.meta.url).pathname;
837
+ const mod = await import(pathToFileURL(modulePath).href);
838
+ const result = await mod.drainPendingTeamDispatch({ cwd, maxPerTick: 5 });
839
+ assert.equal(result.failed, 1);
840
+ const request = await readDispatchRequest('alpha', queued.request.request_id, cwd);
841
+ assert.equal(request?.last_reason, 'exact_pane_unavailable');
842
+ assert.equal(await readFile(tmuxLogPath, 'utf8').catch(() => ''), '');
843
+ }
844
+ finally {
845
+ if (typeof previousPath === 'string')
846
+ process.env.PATH = previousPath;
847
+ else
848
+ delete process.env.PATH;
849
+ await rm(cwd, { recursive: true, force: true });
850
+ }
851
+ });
852
+ it('uses the persisted leader pane as an exact identity without resolver healing', async () => {
608
853
  const cwd = await mkdtemp(join(tmpdir(), 'omx-hook-team-dispatch-'));
609
854
  const fakeBinDir = join(cwd, 'fake-bin');
610
855
  const tmuxLogPath = join(cwd, 'tmux.log');
@@ -657,7 +902,7 @@ if [[ "$cmd" == "display-message" ]]; then
657
902
  exit 0
658
903
  fi
659
904
  if [[ "$fmt" == "#{pane_start_command}" && "$target" == "%91" ]]; then
660
- echo "node dist/cli/omx.js hud --watch"
905
+ echo "codex"
661
906
  exit 0
662
907
  fi
663
908
  if [[ "$fmt" == "#{pane_start_command}" && "$target" == "%42" ]]; then
@@ -674,6 +919,10 @@ if [[ "$cmd" == "display-message" ]]; then
674
919
  fi
675
920
  exit 0
676
921
  fi
922
+ if [[ "$cmd" == "show-option" && "$*" == *"@omx_team_pane_owner_id" ]]; then
923
+ echo "team:alpha"
924
+ exit 0
925
+ fi
677
926
  if [[ "$cmd" == "set-buffer" ]]; then
678
927
  printf '%s' "\${@: -1}" > "${tmuxLogPath}.buffer"
679
928
  exit 0
@@ -703,7 +952,11 @@ if [[ "$cmd" == "send-keys" ]]; then
703
952
  exit 0
704
953
  fi
705
954
  if [[ "$cmd" == "list-panes" ]]; then
706
- printf "%%42\\t1\\tnode\\tcodex\\n%%91\\t0\\tnode\\tnode dist/cli/omx.js hud --watch\\n"
955
+ if [[ "$*" == *"#{pane_dead}"* ]]; then
956
+ printf "%%42\\t0\\t4242\\n%%91\\t0\\t9191\\n"
957
+ else
958
+ printf "%%42\\t1\\tnode\\tcodex\\n%%91\\t0\\tnode\\tnode dist/cli/omx.js hud --watch\\n"
959
+ fi
707
960
  exit 0
708
961
  fi
709
962
  exit 0
@@ -718,6 +971,8 @@ exit 0
718
971
  if (!cfg)
719
972
  throw new Error('missing team config');
720
973
  cfg.leader_pane_id = '%91';
974
+ cfg.leader_pane_pid = 9191;
975
+ bindCanonicalTeamPaneAuthority(cfg);
721
976
  await saveTeamConfig(cfg, cwd);
722
977
  const msg = await sendDirectMessage('alpha', 'worker-1', 'leader-fixed', 'hello leader', cwd);
723
978
  await enqueueDispatchRequest('alpha', {
@@ -731,8 +986,22 @@ exit 0
731
986
  const result = await mod.drainPendingTeamDispatch({ cwd, maxPerTick: 5 });
732
987
  assert.equal(result.processed, 1);
733
988
  const tmuxLog = await readFile(tmuxLogPath, 'utf8');
734
- assert.match(tmuxLog, /send-keys -t %42/);
735
- assert.doesNotMatch(tmuxLog, /send-keys -t %91/);
989
+ assert.match(tmuxLog, /send-keys -t %91/);
990
+ assert.doesNotMatch(tmuxLog, /send-keys -t %42/);
991
+ assert.doesNotMatch(tmuxLog, /display-message -p -t %91 #\{pane_id\}/, 'configured leader pane must bypass the generic resolver');
992
+ cfg.hud_pane_id = '%91';
993
+ await saveTeamConfig(cfg, cwd);
994
+ const hudMsg = await sendDirectMessage('alpha', 'worker-1', 'leader-fixed', 'second leader message', cwd);
995
+ const hudQueued = await enqueueDispatchRequest('alpha', {
996
+ kind: 'mailbox',
997
+ to_worker: 'leader-fixed',
998
+ message_id: hudMsg.message_id,
999
+ trigger_message: 'must not target the HUD pane',
1000
+ }, cwd);
1001
+ const hudResult = await mod.drainPendingTeamDispatch({ cwd, maxPerTick: 5 });
1002
+ assert.equal(hudResult.failed, 1);
1003
+ const hudRequest = await readDispatchRequest('alpha', hudQueued.request.request_id, cwd);
1004
+ assert.equal(hudRequest?.last_reason, 'hud_pane_target');
736
1005
  }
737
1006
  finally {
738
1007
  if (typeof prevPath === 'string')
@@ -795,6 +1064,10 @@ if [[ "$cmd" == "display-message" ]]; then
795
1064
  fi
796
1065
  exit 0
797
1066
  fi
1067
+ if [[ "$cmd" == "show-option" && "$*" == *"@omx_team_pane_owner_id" ]]; then
1068
+ echo "team:alpha"
1069
+ exit 0
1070
+ fi
798
1071
  if [[ "$cmd" == "set-buffer" ]]; then
799
1072
  printf '%s' "\${@: -1}" > "${tmuxLogPath}.buffer"
800
1073
  exit 0
@@ -824,7 +1097,11 @@ if [[ "$cmd" == "send-keys" ]]; then
824
1097
  exit 0
825
1098
  fi
826
1099
  if [[ "$cmd" == "list-panes" ]]; then
827
- printf "%%77\\t1\\tcodex\\tcodex\\n"
1100
+ if [[ "$*" == *"#{pane_dead}"* ]]; then
1101
+ printf "%%77\\t0\\t7777\\n"
1102
+ else
1103
+ printf "%%77\\t1\\tcodex\\tcodex\\n"
1104
+ fi
828
1105
  exit 0
829
1106
  fi
830
1107
  exit 0
@@ -837,6 +1114,8 @@ exit 0
837
1114
  if (!cfg)
838
1115
  throw new Error('missing team config');
839
1116
  cfg.leader_pane_id = '%77';
1117
+ cfg.leader_pane_pid = 7777;
1118
+ bindCanonicalTeamPaneAuthority(cfg);
840
1119
  await saveTeamConfig(cfg, cwd);
841
1120
  const msg = await sendDirectMessage('alpha', 'worker-1', 'leader-fixed', 'hello leader', cwd);
842
1121
  const queued = await enqueueDispatchRequest('alpha', {
@@ -1233,6 +1512,14 @@ exit 0
1233
1512
  process.env.PATH = `${fakeBinDir}:${previousPath || ''}`;
1234
1513
  process.env.OMX_TEST_CAPTURE_FILE = captureFile;
1235
1514
  await initTeamState('alpha', 'task', 'executor', 1, cwd);
1515
+ const config = await readTeamConfig('alpha', cwd);
1516
+ assert.ok(config?.workers[0]);
1517
+ if (!config?.workers[0])
1518
+ throw new Error('missing worker');
1519
+ config.workers[0].pane_id = '%42';
1520
+ config.workers[0].pid = 4242;
1521
+ bindCanonicalTeamPaneAuthority(config);
1522
+ await saveTeamConfig(config, cwd);
1236
1523
  const queued = await enqueueDispatchRequest('alpha', {
1237
1524
  kind: 'inbox',
1238
1525
  to_worker: 'worker-1',
@@ -1290,6 +1577,14 @@ exit 0
1290
1577
  process.env.OMX_TEST_CAPTURE_SEQUENCE_FILE = captureSeqFile;
1291
1578
  process.env.OMX_TEST_CAPTURE_COUNTER_FILE = captureCounterFile;
1292
1579
  await initTeamState('alpha', 'task', 'executor', 1, cwd);
1580
+ const config = await readTeamConfig('alpha', cwd);
1581
+ assert.ok(config?.workers[0]);
1582
+ if (!config?.workers[0])
1583
+ throw new Error('missing worker');
1584
+ config.workers[0].pane_id = '%42';
1585
+ config.workers[0].pid = 4242;
1586
+ bindCanonicalTeamPaneAuthority(config);
1587
+ await saveTeamConfig(config, cwd);
1293
1588
  const queued = await enqueueDispatchRequest('alpha', {
1294
1589
  kind: 'inbox',
1295
1590
  to_worker: 'worker-1',
@@ -1343,6 +1638,14 @@ exit 0
1343
1638
  process.env.OMX_TEST_CAPTURE_SEQUENCE_FILE = captureSeqFile;
1344
1639
  process.env.OMX_TEST_CAPTURE_COUNTER_FILE = captureCounterFile;
1345
1640
  await initTeamState('alpha', 'task', 'executor', 1, cwd);
1641
+ const config = await readTeamConfig('alpha', cwd);
1642
+ assert.ok(config?.workers[0]);
1643
+ if (!config?.workers[0])
1644
+ throw new Error('missing worker');
1645
+ config.workers[0].pane_id = '%42';
1646
+ config.workers[0].pid = 4242;
1647
+ bindCanonicalTeamPaneAuthority(config);
1648
+ await saveTeamConfig(config, cwd);
1346
1649
  const queued = await enqueueDispatchRequest('alpha', {
1347
1650
  kind: 'inbox',
1348
1651
  to_worker: 'worker-1',
@@ -1390,6 +1693,14 @@ exit 0
1390
1693
  process.env.OMX_TEST_CAPTURE_SEQUENCE_FILE = captureSeqFile;
1391
1694
  process.env.OMX_TEST_CAPTURE_COUNTER_FILE = captureCounterFile;
1392
1695
  await initTeamState('alpha', 'task', 'executor', 1, cwd);
1696
+ const config = await readTeamConfig('alpha', cwd);
1697
+ assert.ok(config?.workers[0]);
1698
+ if (!config?.workers[0])
1699
+ throw new Error('missing worker');
1700
+ config.workers[0].pane_id = '%42';
1701
+ config.workers[0].pid = 4242;
1702
+ bindCanonicalTeamPaneAuthority(config);
1703
+ await saveTeamConfig(config, cwd);
1393
1704
  const queued = await enqueueDispatchRequest('alpha', {
1394
1705
  kind: 'inbox',
1395
1706
  to_worker: 'worker-1',
@@ -1504,7 +1815,7 @@ exit 0
1504
1815
  await rm(cwd, { recursive: true, force: true });
1505
1816
  }
1506
1817
  });
1507
- it('resolves session-only dispatch targets without managed leader session context', async () => {
1818
+ it('rejects a session-only dispatch target without an exact Team pane binding', async () => {
1508
1819
  const cwd = await mkdtemp(join(tmpdir(), 'omx-dispatch-session-target-'));
1509
1820
  const stateDir = join(cwd, '.omx', 'state');
1510
1821
  const logsDir = join(cwd, '.omx', 'logs');
@@ -1543,13 +1854,221 @@ exit 0
1543
1854
  }
1544
1855
  const requests = JSON.parse(await readFile(join(stateDir, 'team', 'session-target-team', 'dispatch', 'requests.json'), 'utf-8'));
1545
1856
  const request = requests.find((entry) => entry.to_worker === 'worker-1');
1546
- assert.notEqual(request?.status, 'failed');
1547
- assert.doesNotMatch(JSON.stringify(request), /target_resolution_failed/);
1548
- const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
1549
- assert.match(tmuxLog, /list-panes -t omx-team-session-target/);
1550
- assert.match(tmuxLog, /send-keys -t %42 -l dispatch ping/);
1857
+ assert.equal(request?.status, 'failed');
1858
+ assert.equal(request?.last_reason, 'missing_tmux_target');
1859
+ assert.equal(await readFile(tmuxLogPath, 'utf-8').catch(() => ''), '');
1860
+ }
1861
+ finally {
1862
+ await rm(cwd, { recursive: true, force: true });
1863
+ }
1864
+ });
1865
+ it('fails closed for invalid explicit pane proofs without resolving or targeting another pane', async () => {
1866
+ const scenarios = [
1867
+ ['similar', '%420\\t0\\t4200'],
1868
+ ['dead', '%42\\t1\\t4242'],
1869
+ ['missing', '__EMPTY__'],
1870
+ ['unavailable', 'malformed snapshot'],
1871
+ ];
1872
+ for (const [name, snapshot] of scenarios) {
1873
+ const cwd = await mkdtemp(join(tmpdir(), `omx-hook-exact-pane-${name}-`));
1874
+ const fakeBinDir = join(cwd, 'fake-bin');
1875
+ const tmuxLogPath = join(cwd, 'tmux.log');
1876
+ const exactPaneSequencePath = join(cwd, 'exact-pane-sequence.txt');
1877
+ const previousPath = process.env.PATH;
1878
+ try {
1879
+ await mkdir(fakeBinDir, { recursive: true });
1880
+ await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
1881
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
1882
+ await writeFile(exactPaneSequencePath, `${snapshot}\n`);
1883
+ process.env.PATH = `${fakeBinDir}:${previousPath || ''}`;
1884
+ process.env.OMX_TEST_EXACT_PANE_SEQUENCE_FILE = exactPaneSequencePath;
1885
+ await initTeamState('alpha', 'task', 'executor', 1, cwd);
1886
+ const config = await readTeamConfig('alpha', cwd);
1887
+ assert.ok(config?.workers[0]);
1888
+ if (!config?.workers[0])
1889
+ throw new Error('missing worker');
1890
+ config.workers[0].pane_id = '%42';
1891
+ config.workers[0].pid = 4242;
1892
+ bindCanonicalTeamPaneAuthority(config);
1893
+ await saveTeamConfig(config, cwd);
1894
+ const queued = await enqueueDispatchRequest('alpha', {
1895
+ kind: 'inbox',
1896
+ to_worker: 'worker-1',
1897
+ worker_index: 1,
1898
+ pane_id: '%42',
1899
+ trigger_message: 'do not retarget this request',
1900
+ }, cwd);
1901
+ const modulePath = new URL('../../../dist/scripts/notify-hook/team-dispatch.js', import.meta.url).pathname;
1902
+ const mod = await import(pathToFileURL(modulePath).href);
1903
+ const result = await mod.drainPendingTeamDispatch({ cwd, maxPerTick: 5 });
1904
+ assert.equal(result.failed, 1, `${name} proof should fail the request`);
1905
+ const request = await readDispatchRequest('alpha', queued.request.request_id, cwd);
1906
+ assert.equal(request?.status, 'failed');
1907
+ assert.equal(request?.last_reason, 'exact_pane_unavailable');
1908
+ const tmuxLog = await readFile(tmuxLogPath, 'utf8');
1909
+ const commands = tmuxLog.trim().split('\n').filter(Boolean);
1910
+ assert.match(commands[0] || '', /^list-panes -a(?:\s|$)/, `${name} must prove the global pane first`);
1911
+ assert.doesNotMatch(tmuxLog, /display-message|capture-pane|send-keys|paste-buffer/);
1912
+ assert.doesNotMatch(tmuxLog, /list-panes -t /, `${name} must not use the session resolver fallback`);
1913
+ const dispatchLogPath = join(cwd, '.omx', 'logs', `team-dispatch-${new Date().toISOString().slice(0, 10)}.jsonl`);
1914
+ const dispatchLog = (await readFile(dispatchLogPath, 'utf8')).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
1915
+ const failure = dispatchLog.find((entry) => entry.request_id === queued.request.request_id && entry.type === 'dispatch_failed');
1916
+ assert.equal(failure?.exact_pane_proof?.status, name === 'unavailable' ? 'unavailable' : 'gone');
1917
+ }
1918
+ finally {
1919
+ if (typeof previousPath === 'string')
1920
+ process.env.PATH = previousPath;
1921
+ else
1922
+ delete process.env.PATH;
1923
+ delete process.env.OMX_TEST_EXACT_PANE_SEQUENCE_FILE;
1924
+ delete process.env.OMX_TEST_EXACT_PANE_COUNTER_FILE;
1925
+ await rm(cwd, { recursive: true, force: true });
1926
+ }
1927
+ }
1928
+ });
1929
+ it('rejects a recycled configured worker pane before dispatch effects', async () => {
1930
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-hook-recycled-worker-pane-'));
1931
+ const fakeBinDir = join(cwd, 'fake-bin');
1932
+ const tmuxLogPath = join(cwd, 'tmux.log');
1933
+ const exactPaneSequencePath = join(cwd, 'exact-pane-sequence.txt');
1934
+ const previousPath = process.env.PATH;
1935
+ try {
1936
+ await mkdir(fakeBinDir, { recursive: true });
1937
+ await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
1938
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
1939
+ await writeFile(exactPaneSequencePath, '%42\t0\t4242\n');
1940
+ process.env.PATH = `${fakeBinDir}:${previousPath || ''}`;
1941
+ process.env.OMX_TEST_EXACT_PANE_SEQUENCE_FILE = exactPaneSequencePath;
1942
+ await initTeamState('alpha', 'task', 'executor', 1, cwd);
1943
+ const config = await readTeamConfig('alpha', cwd);
1944
+ assert.ok(config?.workers[0]);
1945
+ if (!config?.workers[0])
1946
+ return;
1947
+ config.workers[0].pane_id = '%42';
1948
+ config.workers[0].pid = 1111;
1949
+ bindCanonicalTeamPaneAuthority(config);
1950
+ await saveTeamConfig(config, cwd);
1951
+ const queued = await enqueueDispatchRequest('alpha', {
1952
+ kind: 'inbox',
1953
+ to_worker: 'worker-1',
1954
+ worker_index: 1,
1955
+ pane_id: '%42',
1956
+ trigger_message: 'must not reach recycled pane',
1957
+ }, cwd);
1958
+ const modulePath = new URL('../../../dist/scripts/notify-hook/team-dispatch.js', import.meta.url).pathname;
1959
+ const mod = await import(pathToFileURL(modulePath).href);
1960
+ const result = await mod.drainPendingTeamDispatch({ cwd, maxPerTick: 5 });
1961
+ assert.equal(result.failed, 1);
1962
+ const request = await readDispatchRequest('alpha', queued.request.request_id, cwd);
1963
+ assert.equal(request?.status, 'failed');
1964
+ assert.equal(request?.last_reason, 'exact_pane_unavailable');
1965
+ const tmuxLog = await readFile(tmuxLogPath, 'utf8');
1966
+ assert.doesNotMatch(tmuxLog, /display-message|capture-pane|send-keys|paste-buffer|set-buffer/);
1967
+ const dispatchLogPath = join(cwd, '.omx', 'logs', `team-dispatch-${new Date().toISOString().slice(0, 10)}.jsonl`);
1968
+ const dispatchLog = (await readFile(dispatchLogPath, 'utf8')).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
1969
+ const failure = dispatchLog.find((entry) => entry.request_id === queued.request.request_id && entry.type === 'dispatch_failed');
1970
+ assert.equal(failure?.exact_pane_proof?.reason, 'pane_pid_changed');
1971
+ assert.equal(failure?.exact_pane_proof?.expectedPid, 1111);
1551
1972
  }
1552
1973
  finally {
1974
+ if (typeof previousPath === 'string')
1975
+ process.env.PATH = previousPath;
1976
+ else
1977
+ delete process.env.PATH;
1978
+ delete process.env.OMX_TEST_EXACT_PANE_SEQUENCE_FILE;
1979
+ delete process.env.OMX_TEST_EXACT_PANE_COUNTER_FILE;
1980
+ await rm(cwd, { recursive: true, force: true });
1981
+ }
1982
+ });
1983
+ it('stops explicit-pane dispatch after a later exact proof is lost', async () => {
1984
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-hook-exact-pane-loss-'));
1985
+ const fakeBinDir = join(cwd, 'fake-bin');
1986
+ const tmuxLogPath = join(cwd, 'tmux.log');
1987
+ const exactPaneSequencePath = join(cwd, 'exact-pane-sequence.txt');
1988
+ const previousPath = process.env.PATH;
1989
+ try {
1990
+ await mkdir(fakeBinDir, { recursive: true });
1991
+ await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
1992
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
1993
+ await writeFile(exactPaneSequencePath, [
1994
+ '%42\t0\t4242', // readiness mode: initial PID proof
1995
+ '%42\t0\t4242', // readiness mode: final PID proof after owner read
1996
+ '%42\t0\t4242', // readiness start-command: initial PID proof
1997
+ '%42\t0\t4242', // readiness start-command: final PID proof after owner read
1998
+ '%42\t0\t4242', // readiness current-command: initial PID proof
1999
+ '%42\t0\t4242', // readiness current-command: final PID proof after owner read
2000
+ '%42\t0\t4242', // readiness capture: initial PID proof
2001
+ '%42\t0\t4242', // readiness capture: final PID proof after owner read
2002
+ '%42\t0\t4242', // readiness capture: initial PID proof
2003
+ '%42\t0\t4242', // readiness capture: final PID proof after owner read
2004
+ '%42\t0\t4242', // readiness capture: initial PID proof
2005
+ '%42\t0\t4242', // readiness capture: final PID proof after owner read
2006
+ '%42\t0\t4242', // send: initial identity proof before HUD guard
2007
+ '%42\t0\t4242', // send: final identity proof before HUD guard
2008
+ '%42\t0\t4242', // send: final identity proof after HUD guard
2009
+ '%42\t0\t4242', // send: initial proof before buffer setup
2010
+ '%42\t0\t4242', // send: final proof after HUD guard
2011
+ '%42\t0\t4242', // clear composer: initial PID proof before owner read
2012
+ '%42\t0\t4242', // clear composer: final PID proof after owner read
2013
+ '%42\t0\t4242', // clear composer: final PID proof after owner read
2014
+ '%42\t0\t4242', // paste buffer: initial PID proof before owner read
2015
+ '%42\t0\t4242', // paste buffer: final PID proof after owner read
2016
+ '%42\t0\t4242', // paste buffer verification: initial PID proof
2017
+ '%42\t0\t4242', // paste buffer verification: final PID proof after owner read
2018
+ '%42\t1\t4242', // paste buffer: final PID proof must block
2019
+ ].join('\n'));
2020
+ process.env.PATH = `${fakeBinDir}:${previousPath || ''}`;
2021
+ process.env.OMX_TEST_EXACT_PANE_SEQUENCE_FILE = exactPaneSequencePath;
2022
+ await initTeamState('alpha', 'task', 'executor', 1, cwd);
2023
+ const config = await readTeamConfig('alpha', cwd);
2024
+ assert.ok(config?.workers[0]);
2025
+ if (!config?.workers[0])
2026
+ throw new Error('missing worker');
2027
+ config.workers[0].pane_id = '%42';
2028
+ config.workers[0].pid = 4242;
2029
+ bindCanonicalTeamPaneAuthority(config);
2030
+ await saveTeamConfig(config, cwd);
2031
+ const queued = await enqueueDispatchRequest('alpha', {
2032
+ kind: 'inbox',
2033
+ to_worker: 'worker-1',
2034
+ worker_index: 1,
2035
+ pane_id: '%42',
2036
+ trigger_message: 'stop after proof loss',
2037
+ }, cwd);
2038
+ const modulePath = new URL('../../../dist/scripts/notify-hook/team-dispatch.js', import.meta.url).pathname;
2039
+ const mod = await import(pathToFileURL(modulePath).href);
2040
+ const result = await mod.drainPendingTeamDispatch({ cwd, maxPerTick: 5 });
2041
+ assert.equal(result.failed, 1);
2042
+ const request = await readDispatchRequest('alpha', queued.request.request_id, cwd);
2043
+ assert.equal(request?.status, 'failed');
2044
+ assert.equal(request?.last_reason, 'exact_pane_unavailable');
2045
+ const tmuxLog = await readFile(tmuxLogPath, 'utf8');
2046
+ const commands = tmuxLog.trim().split('\n').filter(Boolean);
2047
+ const exactGlobalPaneProof = /^list-panes -a -F #\{pane_id\}\t#\{pane_dead\}\t#\{pane_pid\}$/;
2048
+ assert.match(commands[0] || '', exactGlobalPaneProof);
2049
+ assert.match(tmuxLog, /send-keys -t %42 C-u/, 'the live proof should permit the clear effect');
2050
+ assert.doesNotMatch(tmuxLog, /send-keys -t %42 C-m/);
2051
+ const ownerProof = 'show-option -qv -p -t %42 @omx_team_pane_owner_id';
2052
+ for (const [index, command] of commands.entries()) {
2053
+ if (!/^(send-keys|paste-buffer)\b/.test(command) || !command.includes('-t %42') || /^send-keys\b.*\s-l\s/.test(command))
2054
+ continue;
2055
+ assert.match(commands[index - 1] || '', exactGlobalPaneProof, `final PID proof must immediately precede ${command}`);
2056
+ assert.equal(commands[index - 2], ownerProof, `owner proof must precede final PID proof for ${command}`);
2057
+ assert.match(commands[index - 3] || '', exactGlobalPaneProof, `initial PID proof must precede owner proof for ${command}`);
2058
+ }
2059
+ const lastProof = commands.map((command, index) => exactGlobalPaneProof.test(command) ? index : -1).filter((index) => index >= 0).at(-1);
2060
+ assert.notEqual(lastProof, undefined);
2061
+ const commandsAfterLostProof = commands.slice((lastProof || 0) + 1);
2062
+ assert.equal(commandsAfterLostProof.length, 1, 'only non-pane buffer cleanup may follow proof loss');
2063
+ assert.match(commandsAfterLostProof[0] || '', /^delete-buffer -b /);
2064
+ }
2065
+ finally {
2066
+ if (typeof previousPath === 'string')
2067
+ process.env.PATH = previousPath;
2068
+ else
2069
+ delete process.env.PATH;
2070
+ delete process.env.OMX_TEST_EXACT_PANE_SEQUENCE_FILE;
2071
+ delete process.env.OMX_TEST_EXACT_PANE_COUNTER_FILE;
1553
2072
  await rm(cwd, { recursive: true, force: true });
1554
2073
  }
1555
2074
  });