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
@@ -1,17 +1,21 @@
1
1
  import { afterEach, beforeEach, describe, it } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { chmod, mkdtemp, rm, writeFile, readFile, mkdir, utimes } from 'fs/promises';
3
+ import { chmod, mkdtemp, rm, writeFile, readFile, mkdir, open, rename, utimes, symlink } from 'fs/promises';
4
4
  import { join } from 'path';
5
5
  import { tmpdir } from 'os';
6
6
  import { existsSync, readFileSync } from 'fs';
7
- import { ABSOLUTE_MAX_WORKERS, DEFAULT_MAX_WORKERS, cleanupTeamState, createTask, claimTask, computeTaskReadiness, getTeamSummary, initTeamState, listTasks, migrateV1ToV2, readTask, readTeamConfig, readTeamManifestV2, transitionTaskStatus, releaseTaskClaim, reclaimExpiredTaskClaim, sendDirectMessage, broadcastMessage, markMessageDelivered, markMessageNotified, listMailboxMessages, writeTaskApproval, readTaskApproval, readWorkerHeartbeat, readWorkerStatus, updateTask, updateWorkerHeartbeat, writeAtomic, setWriteAtomicRenameForTests, resetWriteAtomicRenameForTests, writeWorkerInbox, enqueueDispatchRequest, listDispatchRequests, markDispatchRequestNotified, markDispatchRequestDelivered, transitionDispatchRequest, readDispatchRequest, readMonitorSnapshot, resolveDispatchLockTimeoutMs, writeTeamManifestV2, } from '../state.js';
7
+ import { ABSOLUTE_MAX_WORKERS, DEFAULT_MAX_WORKERS, cleanupTeamState, createTask, claimTask, computeTaskReadiness, getTeamSummary, initTeamState, listTasks, migrateV1ToV2, readTask, readTeamConfig, saveTeamConfig, recoverTeamMembershipTaskTransaction, commitTeamMembershipTaskTransaction, withTeamTaskBarrier, readTeamManifestV2, transitionTaskStatus, releaseTaskClaim, reclaimExpiredTaskClaim, sendDirectMessage, broadcastMessage, markMessageDelivered, markMessageNotified, listMailboxMessages, writeTaskApproval, readTaskApproval, readWorkerHeartbeat, readWorkerStatus, updateTask, updateWorkerHeartbeat, writeAtomic, setWriteAtomicRenameForTests, resetWriteAtomicRenameForTests, setWriteAtomicOpenForTests, resetWriteAtomicOpenForTests, setWriteAtomicPlatformForTests, resetWriteAtomicPlatformForTests, writeWorkerInbox, enqueueDispatchRequest, listDispatchRequests, markDispatchRequestNotified, markDispatchRequestDelivered, transitionDispatchRequest, readDispatchRequest, readMonitorSnapshot, resolveDispatchLockTimeoutMs, writeTeamManifestV2, removeDispatchRequestsForWorkers, withScalingLock, } from '../state.js';
8
8
  import { normalizeDispatchRequest } from '../state/dispatch.js';
9
+ import { claimTask as claimTaskWithDeps } from '../state/tasks.js';
10
+ import { withScalingLock as withScalingLeaseLock, withTeamLock } from '../state/locks.js';
9
11
  const ORIGINAL_OMX_TEAM_STATE_ROOT = process.env.OMX_TEAM_STATE_ROOT;
10
12
  beforeEach(() => {
11
13
  delete process.env.OMX_TEAM_STATE_ROOT;
12
14
  });
13
15
  afterEach(() => {
14
16
  resetWriteAtomicRenameForTests();
17
+ resetWriteAtomicOpenForTests();
18
+ resetWriteAtomicPlatformForTests();
15
19
  if (typeof ORIGINAL_OMX_TEAM_STATE_ROOT === 'string')
16
20
  process.env.OMX_TEAM_STATE_ROOT = ORIGINAL_OMX_TEAM_STATE_ROOT;
17
21
  else
@@ -66,6 +70,8 @@ if (argv[0] === 'schema') {
66
70
  'mark-delivered',
67
71
  'mark-failed',
68
72
  'request-replay',
73
+ 'remove-dispatch-records',
74
+
69
75
  'capture-snapshot',
70
76
  ],
71
77
  events: [],
@@ -133,6 +139,21 @@ switch (command.command) {
133
139
  process.stdout.write(JSON.stringify({ event: 'DispatchFailed', request_id: command.request_id, reason: command.reason }) + '\\n');
134
140
  process.exit(0);
135
141
  }
142
+ case 'RemoveDispatchRecords': {
143
+ const ids = new Set(command.request_ids || []);
144
+ if (process.env.OMX_TEST_ACK_RETAIN_DISPATCH !== '1') {
145
+ dispatch.records = dispatch.records.filter((entry) => !ids.has(entry.request_id));
146
+ writeJson(dispatchPath, dispatch);
147
+ }
148
+ process.stdout.write(JSON.stringify({ event: 'DispatchRecordsRemoved', request_ids: command.request_ids || [] }) + '\\n');
149
+ process.exit(0);
150
+ }
151
+ case 'CaptureSnapshot': {
152
+ if (process.env.OMX_TEST_SKIP_SNAPSHOT_COMPAT_WRITE !== '1') writeJson(dispatchPath, dispatch);
153
+ process.stdout.write(JSON.stringify({ event: 'SnapshotCaptured' }) + '\\n');
154
+ process.exit(0);
155
+ }
156
+
136
157
  case 'CreateMailboxMessage': {
137
158
  mailbox.records.push({
138
159
  message_id: command.message_id,
@@ -198,6 +219,8 @@ describe('team state', () => {
198
219
  assert.equal(diskCfg.tmux_session, 'omx-team-team-1');
199
220
  assert.equal(diskCfg.lifecycle_profile, 'default');
200
221
  assert.equal(diskCfg.leader_pane_id, null);
222
+ assert.equal(diskCfg.leader_pane_pid, null);
223
+ assert.equal(diskCfg.hud_pane_pid, null);
201
224
  assert.equal(diskCfg.hud_pane_id, null);
202
225
  assert.equal(diskCfg.resize_hook_name, null);
203
226
  assert.equal(diskCfg.resize_hook_target, null);
@@ -209,6 +232,126 @@ describe('team state', () => {
209
232
  await rm(cwd, { recursive: true, force: true });
210
233
  }
211
234
  });
235
+ it('rejects a stale full config save without overwriting a newer canonical generation', async () => {
236
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-config-generation-'));
237
+ try {
238
+ await initTeamState('team-generation', 'generation test', 'executor', 1, cwd);
239
+ const first = await readTeamConfig('team-generation', cwd);
240
+ const stale = await readTeamConfig('team-generation', cwd);
241
+ assert.ok(first);
242
+ assert.ok(stale);
243
+ if (!first || !stale)
244
+ throw new Error('missing config');
245
+ first.task = 'newer canonical value';
246
+ await saveTeamConfig(first, cwd);
247
+ stale.task = 'stale overwrite';
248
+ await assert.rejects(() => saveTeamConfig(stale, cwd), /team_config_stale_generation:team-generation:0:1/);
249
+ assert.equal((await readTeamConfig('team-generation', cwd))?.task, 'newer canonical value');
250
+ }
251
+ finally {
252
+ await rm(cwd, { recursive: true, force: true });
253
+ }
254
+ });
255
+ it('recovers an interrupted config-first save to one old generation before exposing state', async () => {
256
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-config-pair-recovery-'));
257
+ try {
258
+ const teamName = 'config-pair-recovery';
259
+ await initTeamState(teamName, 'old task', 'executor', 1, cwd);
260
+ const teamRoot = join(cwd, '.omx', 'state', 'team', teamName);
261
+ const manifestPath = join(teamRoot, 'manifest.v2.json');
262
+ const config = await readTeamConfig(teamName, cwd);
263
+ assert.ok(config);
264
+ if (!config)
265
+ throw new Error('missing config');
266
+ config.task = 'new task';
267
+ setWriteAtomicRenameForTests(async (from, to) => {
268
+ if (to === manifestPath)
269
+ throw new Error('injected_manifest_publish_failure');
270
+ await rename(from, to);
271
+ });
272
+ await assert.rejects(() => saveTeamConfig(config, cwd), /injected_manifest_publish_failure/);
273
+ assert.equal(existsSync(join(teamRoot, '.membership-task-transaction.json')), true);
274
+ resetWriteAtomicRenameForTests();
275
+ const [recoveredConfig, recoveredManifest] = await Promise.all([
276
+ readTeamConfig(teamName, cwd),
277
+ readTeamManifestV2(teamName, cwd),
278
+ ]);
279
+ assert.equal(recoveredConfig?.task, 'old task');
280
+ assert.equal(recoveredConfig?.config_generation ?? 0, 0);
281
+ assert.equal(recoveredManifest?.task, 'old task');
282
+ assert.equal(recoveredManifest?.config_generation ?? 0, 0);
283
+ assert.equal(existsSync(join(teamRoot, '.membership-task-transaction.json')), false);
284
+ }
285
+ finally {
286
+ await rm(cwd, { recursive: true, force: true });
287
+ }
288
+ });
289
+ it('rejects a stale membership transaction without overwriting the accepted generation', async () => {
290
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-membership-generation-'));
291
+ try {
292
+ await initTeamState('membership-generation', 'generation test', 'executor', 1, cwd);
293
+ const teamRoot = join(cwd, '.omx', 'state', 'team', 'membership-generation');
294
+ const [oldConfig, oldManifest] = await Promise.all([
295
+ readFile(join(teamRoot, 'config.json'), 'utf8'),
296
+ readFile(join(teamRoot, 'manifest.v2.json'), 'utf8'),
297
+ ]);
298
+ const accepted = await readTeamConfig('membership-generation', cwd);
299
+ assert.ok(accepted);
300
+ if (!accepted)
301
+ throw new Error('missing config');
302
+ accepted.task = 'accepted update';
303
+ await saveTeamConfig(accepted, cwd);
304
+ const staleConfig = { ...JSON.parse(oldConfig), worker_count: 0, workers: [] };
305
+ const staleManifest = { ...JSON.parse(oldManifest), worker_count: 0, workers: [] };
306
+ await assert.rejects(withTeamTaskBarrier('membership-generation', cwd, () => commitTeamMembershipTaskTransaction('membership-generation', cwd, {
307
+ baseGeneration: 0,
308
+ tasks: [],
309
+ config: { oldBytes: oldConfig, newBytes: JSON.stringify(staleConfig, null, 2) },
310
+ manifest: { oldBytes: oldManifest, newBytes: JSON.stringify(staleManifest, null, 2) },
311
+ })), /team_membership_stale_generation:membership-generation:0:1/);
312
+ const [config, manifest] = await Promise.all([
313
+ readTeamConfig('membership-generation', cwd),
314
+ readTeamManifestV2('membership-generation', cwd),
315
+ ]);
316
+ assert.equal(config?.task, 'accepted update');
317
+ assert.equal(config?.config_generation, 1);
318
+ assert.equal(manifest?.config_generation, 1);
319
+ }
320
+ finally {
321
+ await rm(cwd, { recursive: true, force: true });
322
+ }
323
+ });
324
+ it('serializes concurrent scale and shutdown membership generations', async () => {
325
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-membership-concurrent-generation-'));
326
+ try {
327
+ await initTeamState('membership-concurrent', 'generation test', 'executor', 1, cwd);
328
+ const teamRoot = join(cwd, '.omx', 'state', 'team', 'membership-concurrent');
329
+ const [oldConfig, oldManifest] = await Promise.all([
330
+ readFile(join(teamRoot, 'config.json'), 'utf8'),
331
+ readFile(join(teamRoot, 'manifest.v2.json'), 'utf8'),
332
+ ]);
333
+ const commit = (task) => withTeamTaskBarrier('membership-concurrent', cwd, () => commitTeamMembershipTaskTransaction('membership-concurrent', cwd, {
334
+ baseGeneration: 0,
335
+ tasks: [],
336
+ config: { oldBytes: oldConfig, newBytes: JSON.stringify({ ...JSON.parse(oldConfig), task }, null, 2) },
337
+ manifest: { oldBytes: oldManifest, newBytes: JSON.stringify({ ...JSON.parse(oldManifest), task }, null, 2) },
338
+ }));
339
+ const results = await Promise.allSettled([commit('scale update'), commit('shutdown update')]);
340
+ assert.equal(results.filter((result) => result.status === 'fulfilled').length, 1);
341
+ assert.equal(results.filter((result) => result.status === 'rejected').length, 1);
342
+ const [config, manifest] = await Promise.all([
343
+ readTeamConfig('membership-concurrent', cwd),
344
+ readTeamManifestV2('membership-concurrent', cwd),
345
+ ]);
346
+ assert.equal(config?.config_generation, 1);
347
+ assert.equal(manifest?.config_generation, 1);
348
+ assert.ok(config?.task === 'scale update' || config?.task === 'shutdown update');
349
+ assert.equal(manifest?.task, config?.task);
350
+ }
351
+ finally {
352
+ await rm(cwd, { recursive: true, force: true });
353
+ }
354
+ });
212
355
  it('initTeamState persists the default lifecycle profile', async () => {
213
356
  const cwd = await mkdtemp(join(tmpdir(), 'omx-team-lifecycle-profile-'));
214
357
  try {
@@ -242,24 +385,26 @@ describe('team state', () => {
242
385
  await rm(cwd, { recursive: true, force: true });
243
386
  }
244
387
  });
245
- it('backfills missing or blank tmux pane owner ids in legacy manifests', async () => {
246
- const cwd = await mkdtemp(join(tmpdir(), 'omx-team-pane-owner-backfill-'));
388
+ it('preserves missing or blank tmux pane owner ids as unavailable in legacy manifests', async () => {
389
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-pane-owner-unavailable-'));
247
390
  try {
248
- await initTeamState('team-pane-owner-backfill', 't', 'executor', 1, cwd);
249
- const manifestPath = join(cwd, '.omx', 'state', 'team', 'team-pane-owner-backfill', 'manifest.v2.json');
391
+ await initTeamState('team-pane-owner-unavailable', 't', 'executor', 1, cwd);
392
+ const manifestPath = join(cwd, '.omx', 'state', 'team', 'team-pane-owner-unavailable', 'manifest.v2.json');
250
393
  const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
251
394
  delete manifest.tmux_pane_owner_id;
252
395
  await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
253
- const loadedManifest = await readTeamManifestV2('team-pane-owner-backfill', cwd);
254
- const loadedConfig = await readTeamConfig('team-pane-owner-backfill', cwd);
255
- assert.equal(loadedManifest?.tmux_pane_owner_id, 'team:team-pane-owner-backfill');
256
- assert.equal(loadedConfig?.tmux_pane_owner_id, 'team:team-pane-owner-backfill');
396
+ const loadedManifest = await readTeamManifestV2('team-pane-owner-unavailable', cwd);
397
+ const loadedConfig = await readTeamConfig('team-pane-owner-unavailable', cwd);
398
+ assert.equal(loadedManifest?.tmux_pane_owner_id, undefined);
399
+ assert.equal(loadedConfig?.tmux_pane_owner_id, undefined);
257
400
  await writeTeamManifestV2({
258
401
  ...loadedManifest,
259
402
  tmux_pane_owner_id: ' ',
260
403
  }, cwd);
261
- const blankNormalized = await readTeamManifestV2('team-pane-owner-backfill', cwd);
262
- assert.equal(blankNormalized?.tmux_pane_owner_id, 'team:team-pane-owner-backfill');
404
+ const blankNormalized = await readTeamManifestV2('team-pane-owner-unavailable', cwd);
405
+ assert.equal(blankNormalized?.tmux_pane_owner_id, undefined);
406
+ const persisted = JSON.parse(await readFile(manifestPath, 'utf8'));
407
+ assert.equal(persisted.tmux_pane_owner_id, undefined);
263
408
  }
264
409
  finally {
265
410
  await rm(cwd, { recursive: true, force: true });
@@ -319,15 +464,20 @@ describe('team state', () => {
319
464
  const fakeBinDir = join(cwd, 'fake-bin');
320
465
  const runtimeLogPath = join(cwd, 'runtime.log');
321
466
  await mkdir(fakeBinDir, { recursive: true });
467
+ await writeFile(join(cwd, '.omx', 'state', 'dispatch.json'), JSON.stringify({ records: [] }));
322
468
  await writeFile(join(fakeBinDir, 'omx-runtime'), `#!/usr/bin/env bash
323
469
  set -eu
324
470
  printf '%s\n' "$*" >> "${runtimeLogPath}"
325
471
  if [[ "\${1:-}" == "schema" ]]; then
326
- printf '{"schema_version":1,"commands":["acquire-authority","renew-authority","queue-dispatch","mark-notified","mark-delivered","mark-failed","request-replay","capture-snapshot"],"events":[],"transport":"tmux"}\n'
472
+ printf '{"schema_version":1,"commands":["acquire-authority","renew-authority","queue-dispatch","mark-notified","mark-delivered","mark-failed","remove-dispatch-records","request-replay","capture-snapshot"],"events":[],"transport":"tmux"}\n'
327
473
  exit 0
328
474
  fi
329
475
  if [[ "\${1:-}" == "exec" ]]; then
330
- printf '{"event":"DispatchQueued","request_id":"ok","target":"worker-1"}\n'
476
+ if [[ "$*" == *'"command":"CaptureSnapshot"'* ]]; then
477
+ printf '{"event":"SnapshotCaptured"}\n'
478
+ else
479
+ printf '{"event":"DispatchQueued","request_id":"ok","target":"worker-1"}\n'
480
+ fi
331
481
  exit 0
332
482
  fi
333
483
  exit 1
@@ -358,36 +508,43 @@ exit 1
358
508
  await rm(cwd, { recursive: true, force: true });
359
509
  }
360
510
  });
361
- it('dispatch request store enqueues, dedupes, and transitions idempotently', async () => {
511
+ it('coalesces mailbox wakes only for an exact target proof and releases on acknowledgement', async () => {
362
512
  const cwd = await mkdtemp(join(tmpdir(), 'omx-team-dispatch-store-'));
363
513
  try {
364
- await initTeamState('team-dispatch', 't', 'executor', 1, cwd);
365
- const first = await enqueueDispatchRequest('team-dispatch', {
366
- kind: 'mailbox',
367
- to_worker: 'worker-1',
368
- message_id: 'msg-1',
369
- trigger_message: 'check mailbox',
370
- intent: 'pending-mailbox-review',
371
- }, cwd);
372
- assert.equal(first.deduped, false);
373
- const dup = await enqueueDispatchRequest('team-dispatch', {
514
+ await initTeamState('team-dispatch', 't', 'executor', 2, cwd);
515
+ await initTeamState('team-dispatch-other', 't', 'executor', 1, cwd);
516
+ const input = (to_worker, message_id, pane_id = 'pane-1', intent = 'pending-mailbox-review') => ({
374
517
  kind: 'mailbox',
375
- to_worker: 'worker-1',
376
- message_id: 'msg-1',
518
+ to_worker,
519
+ pane_id,
520
+ message_id,
377
521
  trigger_message: 'check mailbox',
378
- }, cwd);
379
- assert.equal(dup.deduped, true);
380
- assert.equal(dup.request.request_id, first.request.request_id);
522
+ intent,
523
+ });
524
+ const first = await enqueueDispatchRequest('team-dispatch', input('worker-1', 'msg-1'), cwd);
525
+ const pendingSibling = await enqueueDispatchRequest('team-dispatch', input('worker-1', 'msg-2'), cwd);
526
+ assert.equal(pendingSibling.deduped, true);
527
+ assert.equal(pendingSibling.request.request_id, first.request.request_id);
381
528
  const notified = await markDispatchRequestNotified('team-dispatch', first.request.request_id, {}, cwd);
382
529
  assert.equal(notified?.status, 'notified');
383
- const notifiedAgain = await markDispatchRequestNotified('team-dispatch', first.request.request_id, {}, cwd);
384
- assert.equal(notifiedAgain?.status, 'notified');
530
+ const notifiedSibling = await enqueueDispatchRequest('team-dispatch', input('worker-1', 'msg-3'), cwd);
531
+ assert.equal(notifiedSibling.deduped, true);
532
+ assert.equal(notifiedSibling.request.request_id, first.request.request_id);
533
+ const differentWorker = await enqueueDispatchRequest('team-dispatch', input('worker-2', 'msg-4'), cwd);
534
+ const differentPane = await enqueueDispatchRequest('team-dispatch', input('worker-1', 'msg-5', 'pane-2'), cwd);
535
+ const differentIntent = await enqueueDispatchRequest('team-dispatch', input('worker-1', 'msg-6', 'pane-1', 'followup-relaunch'), cwd);
536
+ const differentTeam = await enqueueDispatchRequest('team-dispatch-other', input('worker-1', 'msg-7'), cwd);
537
+ assert.equal(differentWorker.deduped, false);
538
+ assert.equal(differentPane.deduped, false);
539
+ assert.equal(differentIntent.deduped, false);
540
+ assert.equal(differentTeam.deduped, false);
385
541
  const delivered = await markDispatchRequestDelivered('team-dispatch', first.request.request_id, {}, cwd);
386
542
  assert.equal(delivered?.status, 'delivered');
387
- const listed = await listDispatchRequests('team-dispatch', cwd);
388
- assert.equal(listed.length, 1);
389
- assert.equal(listed[0]?.message_id, 'msg-1');
390
- assert.equal(listed[0]?.intent, 'pending-mailbox-review');
543
+ const successor = await enqueueDispatchRequest('team-dispatch', input('worker-1', 'msg-8'), cwd);
544
+ assert.equal(successor.deduped, false);
545
+ assert.notEqual(successor.request.request_id, first.request.request_id);
546
+ const listed = await listDispatchRequests('team-dispatch', cwd, { kind: 'mailbox' });
547
+ assert.equal(listed.length, 5);
391
548
  }
392
549
  finally {
393
550
  await rm(cwd, { recursive: true, force: true });
@@ -431,9 +588,195 @@ exit 1
431
588
  await rm(cwd, { recursive: true, force: true });
432
589
  }
433
590
  });
591
+ it('preserves legacy rollback evidence when authoritative dispatch absence verification is unavailable', async () => {
592
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-dispatch-authority-unavailable-'));
593
+ const previousRuntimeBinary = process.env.OMX_RUNTIME_BINARY;
594
+ const previousRuntimeBridge = process.env.OMX_RUNTIME_BRIDGE;
595
+ const previousSkipSnapshotWrite = process.env.OMX_TEST_SKIP_SNAPSHOT_COMPAT_WRITE;
596
+ try {
597
+ const teamName = 'dispatch-unavailable';
598
+ await initTeamState(teamName, 't', 'executor', 1, cwd);
599
+ const fakeBinDir = join(cwd, 'fake-bin');
600
+ await mkdir(fakeBinDir, { recursive: true });
601
+ const runtimePath = join(fakeBinDir, 'omx-runtime');
602
+ await writeCompatRuntimeFixture(runtimePath, join(cwd, 'runtime.log'));
603
+ process.env.OMX_RUNTIME_BINARY = runtimePath;
604
+ process.env.OMX_RUNTIME_BRIDGE = '1';
605
+ process.env.OMX_TEST_SKIP_SNAPSHOT_COMPAT_WRITE = '1';
606
+ const legacyPath = join(cwd, '.omx', 'state', 'team', teamName, 'dispatch', 'requests.json');
607
+ const legacyBytes = JSON.stringify([{
608
+ request_id: 'legacy-preserved',
609
+ kind: 'inbox',
610
+ team_name: teamName,
611
+ to_worker: 'worker-1',
612
+ trigger_message: 'preserve rollback evidence',
613
+ transport_preference: 'hook_preferred_with_fallback',
614
+ fallback_allowed: true,
615
+ status: 'pending',
616
+ attempt_count: 0,
617
+ created_at: '2026-01-01T00:00:00.000Z',
618
+ updated_at: '2026-01-01T00:00:00.000Z',
619
+ }], null, 2);
620
+ await writeFile(legacyPath, legacyBytes);
621
+ const dispatchPath = join(cwd, '.omx', 'state', 'dispatch.json');
622
+ const unavailableFixtures = [
623
+ async () => { await rm(dispatchPath, { recursive: true, force: true }); },
624
+ async () => { await rm(dispatchPath, { recursive: true, force: true }); await writeFile(dispatchPath, '{'); },
625
+ async () => { await rm(dispatchPath, { recursive: true, force: true }); await writeFile(dispatchPath, JSON.stringify({ records: [{}] })); },
626
+ async () => { await rm(dispatchPath, { recursive: true, force: true }); await mkdir(dispatchPath); },
627
+ ];
628
+ for (const arrange of unavailableFixtures) {
629
+ await arrange();
630
+ await assert.rejects(() => removeDispatchRequestsForWorkers(teamName, ['worker-1'], cwd), /dispatch compatibility output/);
631
+ assert.equal(await readFile(legacyPath, 'utf8'), legacyBytes);
632
+ }
633
+ }
634
+ finally {
635
+ if (typeof previousRuntimeBinary === 'string')
636
+ process.env.OMX_RUNTIME_BINARY = previousRuntimeBinary;
637
+ else
638
+ delete process.env.OMX_RUNTIME_BINARY;
639
+ if (typeof previousRuntimeBridge === 'string')
640
+ process.env.OMX_RUNTIME_BRIDGE = previousRuntimeBridge;
641
+ else
642
+ delete process.env.OMX_RUNTIME_BRIDGE;
643
+ if (typeof previousSkipSnapshotWrite === 'string')
644
+ process.env.OMX_TEST_SKIP_SNAPSHOT_COMPAT_WRITE = previousSkipSnapshotWrite;
645
+ else
646
+ delete process.env.OMX_TEST_SKIP_SNAPSHOT_COMPAT_WRITE;
647
+ await rm(cwd, { recursive: true, force: true });
648
+ }
649
+ });
650
+ it('scopes bridge rollback IDs to the target team legacy file and preserves same-target foreign records', async () => {
651
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-dispatch-rollback-scope-'));
652
+ const previousRuntimeBinary = process.env.OMX_RUNTIME_BINARY;
653
+ const previousRuntimeBridge = process.env.OMX_RUNTIME_BRIDGE;
654
+ try {
655
+ await initTeamState('team-rollback-a', 't', 'executor', 1, cwd);
656
+ await initTeamState('team-rollback-b', 't', 'executor', 1, cwd);
657
+ const fakeBinDir = join(cwd, 'fake-bin');
658
+ const runtimeLogPath = join(cwd, 'runtime.log');
659
+ await mkdir(fakeBinDir, { recursive: true });
660
+ await writeCompatRuntimeFixture(join(fakeBinDir, 'omx-runtime'), runtimeLogPath);
661
+ process.env.OMX_RUNTIME_BINARY = join(fakeBinDir, 'omx-runtime');
662
+ process.env.OMX_RUNTIME_BRIDGE = '1';
663
+ process.env.OMX_RUNTIME_BRIDGE = '0';
664
+ const scopedRequest = await enqueueDispatchRequest('team-rollback-a', {
665
+ kind: 'inbox',
666
+ to_worker: 'worker-1',
667
+ trigger_message: 'rollback scoped request',
668
+ }, cwd);
669
+ process.env.OMX_RUNTIME_BRIDGE = '1';
670
+ const teamRoot = join(cwd, '.omx', 'state', 'team');
671
+ const legacyPath = join(teamRoot, 'team-rollback-a', 'dispatch', 'requests.json');
672
+ const rootDispatchPath = join(cwd, '.omx', 'state', 'dispatch.json');
673
+ const scopedRequestId = scopedRequest.request.request_id;
674
+ await writeFile(rootDispatchPath, JSON.stringify({ records: [
675
+ { request_id: scopedRequestId, target: 'worker-1', status: 'pending', created_at: new Date().toISOString(), notified_at: null, delivered_at: null, failed_at: null, reason: null, metadata: { team_name: 'team-rollback-a' } },
676
+ { request_id: 'team-b-worker-1', target: 'worker-1', status: 'pending', created_at: new Date().toISOString(), notified_at: null, delivered_at: null, failed_at: null, reason: null, metadata: { team_name: 'team-rollback-b' } },
677
+ { request_id: 'unscoped-worker-1', target: 'worker-1', status: 'pending', created_at: new Date().toISOString(), notified_at: null, delivered_at: null, failed_at: null, reason: null, metadata: null },
678
+ ] }, null, 2));
679
+ await removeDispatchRequestsForWorkers('team-rollback-a', ['worker-1'], cwd);
680
+ assert.deepEqual(JSON.parse(await readFile(legacyPath, 'utf8')), []);
681
+ const runtimeLog = await readFile(runtimeLogPath, 'utf8');
682
+ const removalLine = runtimeLog.split('\n').find((line) => line.startsWith('exec {"command":"RemoveDispatchRecords"'));
683
+ assert.ok(removalLine);
684
+ const removalPayload = JSON.parse(removalLine.slice(removalLine.indexOf('{'), removalLine.lastIndexOf(' --state-dir=')));
685
+ assert.deepEqual(removalPayload.request_ids, [scopedRequestId]);
686
+ assert.match(runtimeLog, /CaptureSnapshot/);
687
+ }
688
+ finally {
689
+ if (typeof previousRuntimeBinary === 'string')
690
+ process.env.OMX_RUNTIME_BINARY = previousRuntimeBinary;
691
+ else
692
+ delete process.env.OMX_RUNTIME_BINARY;
693
+ if (typeof previousRuntimeBridge === 'string')
694
+ process.env.OMX_RUNTIME_BRIDGE = previousRuntimeBridge;
695
+ else
696
+ delete process.env.OMX_RUNTIME_BRIDGE;
697
+ await rm(cwd, { recursive: true, force: true });
698
+ }
699
+ });
700
+ it('preserves malformed legacy rollback evidence byte-for-byte', async () => {
701
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-dispatch-legacy-malformed-'));
702
+ const previousRuntimeBridge = process.env.OMX_RUNTIME_BRIDGE;
703
+ try {
704
+ const teamName = 'dispatch-legacy-malformed';
705
+ process.env.OMX_RUNTIME_BRIDGE = '0';
706
+ await initTeamState(teamName, 't', 'executor', 1, cwd);
707
+ const legacyPath = join(cwd, '.omx', 'state', 'team', teamName, 'dispatch', 'requests.json');
708
+ const legacyBytes = '{\n "records": "not-an-array"\n}\n';
709
+ await writeFile(legacyPath, legacyBytes);
710
+ await assert.rejects(() => removeDispatchRequestsForWorkers(teamName, ['worker-1'], cwd), /legacy dispatch rollback evidence is malformed or unrecognized/);
711
+ assert.equal(await readFile(legacyPath, 'utf8'), legacyBytes);
712
+ }
713
+ finally {
714
+ if (typeof previousRuntimeBridge === 'string')
715
+ process.env.OMX_RUNTIME_BRIDGE = previousRuntimeBridge;
716
+ else
717
+ delete process.env.OMX_RUNTIME_BRIDGE;
718
+ await rm(cwd, { recursive: true, force: true });
719
+ }
720
+ });
721
+ it('rejects an acknowledged retained dispatch ID even when its scope fields changed', async () => {
722
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-dispatch-retained-id-'));
723
+ const previousRuntimeBinary = process.env.OMX_RUNTIME_BINARY;
724
+ const previousRuntimeBridge = process.env.OMX_RUNTIME_BRIDGE;
725
+ const previousAckRetain = process.env.OMX_TEST_ACK_RETAIN_DISPATCH;
726
+ try {
727
+ const teamName = 'dispatch-retained-id';
728
+ await initTeamState(teamName, 't', 'executor', 1, cwd);
729
+ const fakeBinDir = join(cwd, 'fake-bin');
730
+ await mkdir(fakeBinDir, { recursive: true });
731
+ const runtimePath = join(fakeBinDir, 'omx-runtime');
732
+ await writeCompatRuntimeFixture(runtimePath, join(cwd, 'runtime.log'));
733
+ process.env.OMX_RUNTIME_BINARY = runtimePath;
734
+ process.env.OMX_RUNTIME_BRIDGE = '0';
735
+ const queued = await enqueueDispatchRequest(teamName, {
736
+ kind: 'inbox',
737
+ to_worker: 'worker-1',
738
+ trigger_message: 'remove by exact ID',
739
+ }, cwd);
740
+ const legacyPath = join(cwd, '.omx', 'state', 'team', teamName, 'dispatch', 'requests.json');
741
+ const legacyBytes = await readFile(legacyPath, 'utf8');
742
+ const dispatchPath = join(cwd, '.omx', 'state', 'dispatch.json');
743
+ await writeFile(dispatchPath, JSON.stringify({ records: [{
744
+ request_id: queued.request.request_id,
745
+ target: 'worker-renamed',
746
+ status: 'pending',
747
+ created_at: '2026-01-01T00:00:00.000Z',
748
+ notified_at: null,
749
+ delivered_at: null,
750
+ failed_at: null,
751
+ reason: null,
752
+ metadata: { team_name: 'different-team' },
753
+ }] }, null, 2));
754
+ process.env.OMX_RUNTIME_BRIDGE = '1';
755
+ process.env.OMX_TEST_ACK_RETAIN_DISPATCH = '1';
756
+ await assert.rejects(() => removeDispatchRequestsForWorkers(teamName, ['worker-1'], cwd), /authoritative_dispatch_rollback_verification_failed/);
757
+ assert.equal(await readFile(legacyPath, 'utf8'), legacyBytes);
758
+ }
759
+ finally {
760
+ if (typeof previousRuntimeBinary === 'string')
761
+ process.env.OMX_RUNTIME_BINARY = previousRuntimeBinary;
762
+ else
763
+ delete process.env.OMX_RUNTIME_BINARY;
764
+ if (typeof previousRuntimeBridge === 'string')
765
+ process.env.OMX_RUNTIME_BRIDGE = previousRuntimeBridge;
766
+ else
767
+ delete process.env.OMX_RUNTIME_BRIDGE;
768
+ if (typeof previousAckRetain === 'string')
769
+ process.env.OMX_TEST_ACK_RETAIN_DISPATCH = previousAckRetain;
770
+ else
771
+ delete process.env.OMX_TEST_ACK_RETAIN_DISPATCH;
772
+ await rm(cwd, { recursive: true, force: true });
773
+ }
774
+ });
434
775
  it('dispatch request store keeps failed requests failed while allowing reason patches', async () => {
435
776
  const cwd = await mkdtemp(join(tmpdir(), 'omx-team-dispatch-store-failed-'));
777
+ const previousRuntimeBridge = process.env.OMX_RUNTIME_BRIDGE;
436
778
  try {
779
+ process.env.OMX_RUNTIME_BRIDGE = '0';
437
780
  await initTeamState('team-dispatch-failed', 't', 'executor', 1, cwd);
438
781
  const queued = await enqueueDispatchRequest('team-dispatch-failed', {
439
782
  kind: 'inbox',
@@ -452,6 +795,10 @@ exit 1
452
795
  assert.equal(reread?.last_reason, 'fallback_confirmed_after_failed_receipt:tmux_send_keys_sent');
453
796
  }
454
797
  finally {
798
+ if (typeof previousRuntimeBridge === 'string')
799
+ process.env.OMX_RUNTIME_BRIDGE = previousRuntimeBridge;
800
+ else
801
+ delete process.env.OMX_RUNTIME_BRIDGE;
455
802
  await rm(cwd, { recursive: true, force: true });
456
803
  }
457
804
  });
@@ -476,6 +823,8 @@ exit 1
476
823
  assert.deepEqual(manifest?.worktree_mode, { enabled: true, detached: false, name: 'feature/team-meta' });
477
824
  assert.equal(manifest?.lifecycle_profile, 'default');
478
825
  assert.equal(manifest?.leader_pane_id, null);
826
+ assert.equal(manifest?.leader_pane_pid, null);
827
+ assert.equal(manifest?.hud_pane_pid, null);
479
828
  assert.equal(manifest?.hud_pane_id, null);
480
829
  assert.equal(manifest?.resize_hook_name, null);
481
830
  assert.equal(manifest?.resize_hook_target, null);
@@ -605,6 +954,72 @@ exit 1
605
954
  await rm(cwd, { recursive: true, force: true });
606
955
  }
607
956
  });
957
+ it('claimTask revalidates a stale member under its claim lock when expectedVersion is null', async () => {
958
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-claim-member-boundary-'));
959
+ try {
960
+ const teamName = 'team-claim-member-boundary';
961
+ await initTeamState(teamName, 't', 'executor', 2, cwd);
962
+ const task = await createTask(teamName, { subject: 'a', description: 'd', status: 'pending' }, cwd);
963
+ const claim = await claimTaskWithDeps(task.id, 'worker-2', null, {
964
+ teamName,
965
+ cwd,
966
+ readTask,
967
+ readTeamConfig,
968
+ withTaskClaimLock: async (_teamName, _taskId, _cwd, fn) => {
969
+ const config = await readTeamConfig(teamName, cwd);
970
+ assert.ok(config);
971
+ config.workers = config.workers.filter((worker) => worker.name !== 'worker-2');
972
+ await saveTeamConfig(config, cwd);
973
+ return { ok: true, value: await fn() };
974
+ },
975
+ normalizeTask: (current) => current,
976
+ isTerminalTaskStatus: (status) => status === 'completed' || status === 'failed',
977
+ taskFilePath: (_teamName, taskId, _cwd) => join(cwd, '.omx', 'state', 'team', teamName, 'tasks', `task-${taskId}.json`),
978
+ writeAtomic,
979
+ });
980
+ assert.deepEqual(claim, { ok: false, error: 'worker_not_found' });
981
+ const persisted = await readTask(teamName, task.id, cwd);
982
+ assert.equal(persisted?.status, 'pending');
983
+ assert.equal(persisted?.owner, undefined);
984
+ }
985
+ finally {
986
+ await rm(cwd, { recursive: true, force: true });
987
+ }
988
+ });
989
+ it('claimTask rejects a removed worker for a task created after the scale-down snapshot', async () => {
990
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-claim-post-snapshot-'));
991
+ try {
992
+ const teamName = 'team-claim-post-snapshot';
993
+ await initTeamState(teamName, 't', 'executor', 2, cwd);
994
+ const scaleDownSnapshot = await readTeamConfig(teamName, cwd);
995
+ assert.ok(scaleDownSnapshot?.workers.some((worker) => worker.name === 'worker-2'));
996
+ const postSnapshotTask = await createTask(teamName, { subject: 'post snapshot', description: 'd', status: 'pending' }, cwd);
997
+ const claim = await claimTaskWithDeps(postSnapshotTask.id, 'worker-2', null, {
998
+ teamName,
999
+ cwd,
1000
+ readTask,
1001
+ readTeamConfig,
1002
+ withTaskClaimLock: async (_teamName, _taskId, _cwd, fn) => {
1003
+ const config = await readTeamConfig(teamName, cwd);
1004
+ assert.ok(config);
1005
+ config.workers = config.workers.filter((worker) => worker.name !== 'worker-2');
1006
+ await saveTeamConfig(config, cwd);
1007
+ return { ok: true, value: await fn() };
1008
+ },
1009
+ normalizeTask: (current) => current,
1010
+ isTerminalTaskStatus: (status) => status === 'completed' || status === 'failed',
1011
+ taskFilePath: (_teamName, taskId, _cwd) => join(cwd, '.omx', 'state', 'team', teamName, 'tasks', `task-${taskId}.json`),
1012
+ writeAtomic,
1013
+ });
1014
+ assert.deepEqual(claim, { ok: false, error: 'worker_not_found' });
1015
+ const persisted = await readTask(teamName, postSnapshotTask.id, cwd);
1016
+ assert.equal(persisted?.status, 'pending');
1017
+ assert.equal(persisted?.owner, undefined);
1018
+ }
1019
+ finally {
1020
+ await rm(cwd, { recursive: true, force: true });
1021
+ }
1022
+ });
608
1023
  it('claimTask recovers a stale task claim lock and proceeds', async () => {
609
1024
  const cwd = await mkdtemp(join(tmpdir(), 'omx-team-claim-stale-lock-'));
610
1025
  try {
@@ -1653,6 +2068,216 @@ exit 1
1653
2068
  await rm(cwd, { recursive: true, force: true });
1654
2069
  }
1655
2070
  });
2071
+ for (const code of ['EINVAL', 'ENOTSUP', 'EISDIR']) {
2072
+ it(`writeAtomic tolerates unsupported parent directory sync ${code} after fsyncing the file`, async () => {
2073
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-state-'));
2074
+ try {
2075
+ const p = join(cwd, `atomic-parent-sync-${code}.txt`);
2076
+ let fileSyncs = 0;
2077
+ setWriteAtomicOpenForTests(async (...args) => {
2078
+ const [path] = args;
2079
+ const handle = await open(...args);
2080
+ const sync = handle.sync.bind(handle);
2081
+ handle.sync = async () => {
2082
+ if (path === cwd) {
2083
+ const error = new Error(`unsupported parent sync: ${code}`);
2084
+ error.code = code;
2085
+ throw error;
2086
+ }
2087
+ fileSyncs += 1;
2088
+ await sync();
2089
+ };
2090
+ return handle;
2091
+ });
2092
+ await writeAtomic(p, 'durable data');
2093
+ assert.equal(fileSyncs, 1);
2094
+ assert.equal(readFileSync(p, 'utf8'), 'durable data');
2095
+ }
2096
+ finally {
2097
+ await rm(cwd, { recursive: true, force: true });
2098
+ }
2099
+ });
2100
+ }
2101
+ it('writeAtomic tolerates Windows EPERM opening the parent directory after fsyncing the file', async () => {
2102
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-state-'));
2103
+ try {
2104
+ const p = join(cwd, 'atomic-parent-open-eperm.txt');
2105
+ let fileSyncs = 0;
2106
+ setWriteAtomicPlatformForTests('win32');
2107
+ setWriteAtomicOpenForTests(async (...args) => {
2108
+ const [path] = args;
2109
+ if (path === cwd) {
2110
+ const error = new Error('Windows cannot open directory for sync');
2111
+ error.code = 'EPERM';
2112
+ throw error;
2113
+ }
2114
+ const handle = await open(...args);
2115
+ const sync = handle.sync.bind(handle);
2116
+ handle.sync = async () => {
2117
+ fileSyncs += 1;
2118
+ await sync();
2119
+ };
2120
+ return handle;
2121
+ });
2122
+ await writeAtomic(p, 'durable data');
2123
+ assert.equal(fileSyncs, 1);
2124
+ assert.equal(readFileSync(p, 'utf8'), 'durable data');
2125
+ }
2126
+ finally {
2127
+ await rm(cwd, { recursive: true, force: true });
2128
+ }
2129
+ });
2130
+ it('writeAtomic tolerates Windows EPERM syncing the parent directory after fsyncing the file', async () => {
2131
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-state-'));
2132
+ try {
2133
+ const p = join(cwd, 'atomic-parent-sync-eperm.txt');
2134
+ let fileSyncs = 0;
2135
+ setWriteAtomicPlatformForTests('win32');
2136
+ setWriteAtomicOpenForTests(async (...args) => {
2137
+ const [path] = args;
2138
+ const handle = await open(...args);
2139
+ const sync = handle.sync.bind(handle);
2140
+ handle.sync = async () => {
2141
+ if (path === cwd) {
2142
+ const error = new Error('Windows cannot sync directory');
2143
+ error.code = 'EPERM';
2144
+ throw error;
2145
+ }
2146
+ fileSyncs += 1;
2147
+ await sync();
2148
+ };
2149
+ return handle;
2150
+ });
2151
+ await writeAtomic(p, 'durable data');
2152
+ assert.equal(fileSyncs, 1);
2153
+ assert.equal(readFileSync(p, 'utf8'), 'durable data');
2154
+ }
2155
+ finally {
2156
+ await rm(cwd, { recursive: true, force: true });
2157
+ }
2158
+ });
2159
+ it('writeAtomic surfaces POSIX EPERM opening the parent directory after fsyncing the file', async () => {
2160
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-state-'));
2161
+ try {
2162
+ const p = join(cwd, 'atomic-parent-open-posix-eperm.txt');
2163
+ let fileSyncs = 0;
2164
+ setWriteAtomicPlatformForTests('linux');
2165
+ setWriteAtomicOpenForTests(async (...args) => {
2166
+ const [path] = args;
2167
+ if (path === cwd) {
2168
+ const error = new Error('POSIX parent open denied');
2169
+ error.code = 'EPERM';
2170
+ throw error;
2171
+ }
2172
+ const handle = await open(...args);
2173
+ const sync = handle.sync.bind(handle);
2174
+ handle.sync = async () => {
2175
+ fileSyncs += 1;
2176
+ await sync();
2177
+ };
2178
+ return handle;
2179
+ });
2180
+ await assert.rejects(() => writeAtomic(p, 'durable data'), (error) => {
2181
+ return error.code === 'EPERM';
2182
+ });
2183
+ assert.equal(fileSyncs, 1);
2184
+ assert.equal(readFileSync(p, 'utf8'), 'durable data');
2185
+ }
2186
+ finally {
2187
+ await rm(cwd, { recursive: true, force: true });
2188
+ }
2189
+ });
2190
+ it('writeAtomic surfaces POSIX EPERM syncing the parent directory after fsyncing the file', async () => {
2191
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-state-'));
2192
+ try {
2193
+ const p = join(cwd, 'atomic-parent-sync-posix-eperm.txt');
2194
+ let fileSyncs = 0;
2195
+ setWriteAtomicPlatformForTests('linux');
2196
+ setWriteAtomicOpenForTests(async (...args) => {
2197
+ const [path] = args;
2198
+ const handle = await open(...args);
2199
+ const sync = handle.sync.bind(handle);
2200
+ handle.sync = async () => {
2201
+ if (path === cwd) {
2202
+ const error = new Error('POSIX parent sync denied');
2203
+ error.code = 'EPERM';
2204
+ throw error;
2205
+ }
2206
+ fileSyncs += 1;
2207
+ await sync();
2208
+ };
2209
+ return handle;
2210
+ });
2211
+ await assert.rejects(() => writeAtomic(p, 'durable data'), (error) => {
2212
+ return error.code === 'EPERM';
2213
+ });
2214
+ assert.equal(fileSyncs, 1);
2215
+ assert.equal(readFileSync(p, 'utf8'), 'durable data');
2216
+ }
2217
+ finally {
2218
+ await rm(cwd, { recursive: true, force: true });
2219
+ }
2220
+ });
2221
+ it('writeAtomic surfaces unexpected parent directory open failures after fsyncing the file', async () => {
2222
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-state-'));
2223
+ try {
2224
+ const p = join(cwd, 'atomic-parent-open-unexpected.txt');
2225
+ let fileSyncs = 0;
2226
+ setWriteAtomicOpenForTests(async (...args) => {
2227
+ const [path] = args;
2228
+ if (path === cwd) {
2229
+ const error = new Error('parent open failed');
2230
+ error.code = 'EIO';
2231
+ throw error;
2232
+ }
2233
+ const handle = await open(...args);
2234
+ const sync = handle.sync.bind(handle);
2235
+ handle.sync = async () => {
2236
+ fileSyncs += 1;
2237
+ await sync();
2238
+ };
2239
+ return handle;
2240
+ });
2241
+ await assert.rejects(() => writeAtomic(p, 'durable data'), (error) => {
2242
+ return error.code === 'EIO';
2243
+ });
2244
+ assert.equal(fileSyncs, 1);
2245
+ assert.equal(readFileSync(p, 'utf8'), 'durable data');
2246
+ }
2247
+ finally {
2248
+ await rm(cwd, { recursive: true, force: true });
2249
+ }
2250
+ });
2251
+ it('writeAtomic surfaces unexpected parent directory sync failures after fsyncing the file', async () => {
2252
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-state-'));
2253
+ try {
2254
+ const p = join(cwd, 'atomic-parent-sync-unexpected.txt');
2255
+ let fileSyncs = 0;
2256
+ setWriteAtomicOpenForTests(async (...args) => {
2257
+ const [path] = args;
2258
+ const handle = await open(...args);
2259
+ const sync = handle.sync.bind(handle);
2260
+ handle.sync = async () => {
2261
+ if (path === cwd) {
2262
+ const error = new Error('parent sync failed');
2263
+ error.code = 'EIO';
2264
+ throw error;
2265
+ }
2266
+ fileSyncs += 1;
2267
+ await sync();
2268
+ };
2269
+ return handle;
2270
+ });
2271
+ await assert.rejects(() => writeAtomic(p, 'durable data'), (error) => {
2272
+ return error.code === 'EIO';
2273
+ });
2274
+ assert.equal(fileSyncs, 1);
2275
+ assert.equal(readFileSync(p, 'utf8'), 'durable data');
2276
+ }
2277
+ finally {
2278
+ await rm(cwd, { recursive: true, force: true });
2279
+ }
2280
+ });
1656
2281
  it('readWorkerStatus returns {state:\'unknown\'} on missing file', async () => {
1657
2282
  const cwd = await mkdtemp(join(tmpdir(), 'omx-team-state-'));
1658
2283
  try {
@@ -1961,5 +2586,148 @@ exit 1
1961
2586
  await rm(cwd, { recursive: true, force: true });
1962
2587
  }
1963
2588
  });
2589
+ it('heartbeats live scaling and membership leases while reclaiming stale crashed locks', async () => {
2590
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-lock-lease-'));
2591
+ const teamName = 'team-lock-lease';
2592
+ const staleMs = 90;
2593
+ const deps = {
2594
+ teamDir: (name, root) => join(root, '.omx', 'state', 'team', name),
2595
+ taskClaimLockDir: (name, taskId, root) => join(root, '.omx', 'state', 'team', name, 'claims', `task-${taskId}.lock`),
2596
+ mailboxLockDir: (name, worker, root) => join(root, '.omx', 'state', 'team', name, 'mailboxes', `${worker}.lock`),
2597
+ };
2598
+ const locks = [
2599
+ {
2600
+ label: 'scaling',
2601
+ lockDir: join(cwd, '.omx', 'state', '.team-locks', `${teamName}.scaling`),
2602
+ acquire: (fn) => withScalingLeaseLock(teamName, cwd, staleMs, deps, fn),
2603
+ },
2604
+ {
2605
+ label: 'membership',
2606
+ lockDir: join(cwd, '.omx', 'state', '.team-locks', `${teamName}.membership`),
2607
+ acquire: (fn) => withTeamLock(teamName, cwd, staleMs, deps, fn),
2608
+ },
2609
+ ];
2610
+ try {
2611
+ for (const lock of locks) {
2612
+ let releaseHeld;
2613
+ const heldRelease = new Promise((resolve) => { releaseHeld = resolve; });
2614
+ let heldEntered;
2615
+ const heldEnteredPromise = new Promise((resolve) => { heldEntered = resolve; });
2616
+ const held = lock.acquire(async () => {
2617
+ heldEntered();
2618
+ await heldRelease;
2619
+ });
2620
+ await heldEnteredPromise;
2621
+ let contenderEntered = false;
2622
+ const contender = lock.acquire(async () => { contenderEntered = true; });
2623
+ await new Promise((resolve) => setTimeout(resolve, staleMs * 3));
2624
+ assert.equal(contenderEntered, false, `live ${lock.label} lock must not be reclaimed after its directory mtime becomes stale`);
2625
+ releaseHeld();
2626
+ await Promise.all([held, contender]);
2627
+ await mkdir(lock.lockDir, { recursive: true });
2628
+ await writeFile(join(lock.lockDir, 'owner'), 'crashed-owner');
2629
+ await writeFile(join(lock.lockDir, 'lease'), 'crashed-owner');
2630
+ const staleTime = new Date(Date.now() - staleMs * 2);
2631
+ await utimes(lock.lockDir, staleTime, staleTime);
2632
+ await utimes(join(lock.lockDir, 'lease'), staleTime, staleTime);
2633
+ let reclaimed = false;
2634
+ await lock.acquire(async () => { reclaimed = true; });
2635
+ assert.equal(reclaimed, true, `stale ${lock.label} lock must be reclaimed after its lease expires`);
2636
+ }
2637
+ }
2638
+ finally {
2639
+ await rm(cwd, { recursive: true, force: true });
2640
+ }
2641
+ });
2642
+ it('serializes cleanup with concurrent scaling and claims without recreating team state', async () => {
2643
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-team-cleanup-lock-race-'));
2644
+ const teamName = 'team-cleanup-lock-race';
2645
+ const teamRoot = join(cwd, '.omx', 'state', 'team', teamName);
2646
+ let releaseScale;
2647
+ const scaleReleased = new Promise((resolve) => { releaseScale = resolve; });
2648
+ let scaleEntered;
2649
+ const scaleEnteredPromise = new Promise((resolve) => { scaleEntered = resolve; });
2650
+ try {
2651
+ await initTeamState(teamName, 't', 'executor', 1, cwd);
2652
+ const task = await createTask(teamName, { subject: 'racy', description: 'd', status: 'pending' }, cwd);
2653
+ const scale = withScalingLock(teamName, cwd, async () => {
2654
+ scaleEntered();
2655
+ await scaleReleased;
2656
+ });
2657
+ await scaleEnteredPromise;
2658
+ const cleanup = cleanupTeamState(teamName, cwd);
2659
+ const claim = await claimTask(teamName, task.id, 'worker-1', task.version, cwd);
2660
+ assert.equal(claim.ok, true, 'claim must finish before the queued cleanup deletes canonical state');
2661
+ releaseScale();
2662
+ await Promise.all([scale, cleanup]);
2663
+ assert.equal(existsSync(teamRoot), false);
2664
+ await assert.rejects(() => createTask(teamName, { subject: 'must not recreate', description: 'd', status: 'pending' }, cwd), /Team team-cleanup-lock-race not found/);
2665
+ const afterCleanupClaim = await claimTask(teamName, task.id, 'worker-1', null, cwd);
2666
+ assert.deepEqual(afterCleanupClaim, { ok: false, error: 'task_not_found' });
2667
+ assert.equal(existsSync(teamRoot), false, 'post-cleanup operations must not recreate a fake team root');
2668
+ }
2669
+ finally {
2670
+ releaseScale?.();
2671
+ await rm(cwd, { recursive: true, force: true });
2672
+ }
2673
+ });
2674
+ });
2675
+ describe('membership transaction recovery path authority', () => {
2676
+ it('rejects foreign absolute paths before mutating any victim or canonical file', async () => {
2677
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-membership-foreign-path-'));
2678
+ try {
2679
+ const teamName = 'foreign-path';
2680
+ await initTeamState(teamName, 'task', 'executor', 1, cwd);
2681
+ const teamDir = join(cwd, '.omx', 'state', 'team', teamName);
2682
+ const configPath = join(teamDir, 'config.json');
2683
+ const configBytes = await readFile(configPath, 'utf8');
2684
+ const victimPath = join(cwd, 'victim.txt');
2685
+ await writeFile(victimPath, 'ORIGINAL');
2686
+ await writeFile(join(teamDir, '.membership-task-transaction.json'), JSON.stringify({
2687
+ schemaVersion: 1,
2688
+ phase: 'committed',
2689
+ files: [
2690
+ { path: configPath, oldBytes: configBytes, newBytes: configBytes },
2691
+ { path: victimPath, oldBytes: 'ORIGINAL', newBytes: 'PWNED' },
2692
+ ],
2693
+ }));
2694
+ await assert.rejects(recoverTeamMembershipTaskTransaction(teamName, cwd), /escapes expected team files/);
2695
+ assert.equal(await readFile(victimPath, 'utf8'), 'ORIGINAL');
2696
+ assert.equal(await readFile(configPath, 'utf8'), configBytes);
2697
+ }
2698
+ finally {
2699
+ await rm(cwd, { recursive: true, force: true });
2700
+ }
2701
+ });
2702
+ it('rejects a symlinked tasks directory before writing through it', async () => {
2703
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-membership-symlink-path-'));
2704
+ try {
2705
+ const teamName = 'symlink-path';
2706
+ await initTeamState(teamName, 'task', 'executor', 1, cwd);
2707
+ const teamDir = join(cwd, '.omx', 'state', 'team', teamName);
2708
+ const configPath = join(teamDir, 'config.json');
2709
+ const configBytes = await readFile(configPath, 'utf8');
2710
+ const tasksDir = join(teamDir, 'tasks');
2711
+ const outsideTasksDir = join(cwd, 'outside-tasks');
2712
+ await rm(tasksDir, { recursive: true, force: true });
2713
+ await mkdir(outsideTasksDir);
2714
+ await writeFile(join(outsideTasksDir, 'task-1.json'), 'ORIGINAL');
2715
+ await symlink(outsideTasksDir, tasksDir, 'dir');
2716
+ await writeFile(join(teamDir, '.membership-task-transaction.json'), JSON.stringify({
2717
+ schemaVersion: 1,
2718
+ phase: 'committed',
2719
+ files: [
2720
+ { path: configPath, oldBytes: configBytes, newBytes: configBytes },
2721
+ { path: join(tasksDir, 'task-1.json'), oldBytes: 'ORIGINAL', newBytes: 'PWNED' },
2722
+ ],
2723
+ }));
2724
+ await assert.rejects(recoverTeamMembershipTaskTransaction(teamName, cwd), /tasks root escapes canonical team root/);
2725
+ assert.equal(await readFile(join(outsideTasksDir, 'task-1.json'), 'utf8'), 'ORIGINAL');
2726
+ assert.equal(await readFile(configPath, 'utf8'), configBytes);
2727
+ }
2728
+ finally {
2729
+ await rm(cwd, { recursive: true, force: true });
2730
+ }
2731
+ });
1964
2732
  });
1965
2733
  //# sourceMappingURL=state.test.js.map