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
package/Cargo.lock CHANGED
@@ -32,7 +32,7 @@ checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
32
32
 
33
33
  [[package]]
34
34
  name = "omx-api"
35
- version = "0.20.2"
35
+ version = "0.20.3"
36
36
  dependencies = [
37
37
  "serde",
38
38
  "serde_json",
@@ -40,14 +40,14 @@ dependencies = [
40
40
 
41
41
  [[package]]
42
42
  name = "omx-explore-harness"
43
- version = "0.20.2"
43
+ version = "0.20.3"
44
44
  dependencies = [
45
45
  "libc",
46
46
  ]
47
47
 
48
48
  [[package]]
49
49
  name = "omx-mux"
50
- version = "0.20.2"
50
+ version = "0.20.3"
51
51
  dependencies = [
52
52
  "serde",
53
53
  "serde_json",
@@ -55,8 +55,9 @@ dependencies = [
55
55
 
56
56
  [[package]]
57
57
  name = "omx-runtime"
58
- version = "0.20.2"
58
+ version = "0.20.3"
59
59
  dependencies = [
60
+ "fs2",
60
61
  "omx-mux",
61
62
  "omx-runtime-core",
62
63
  "serde_json",
@@ -64,7 +65,7 @@ dependencies = [
64
65
 
65
66
  [[package]]
66
67
  name = "omx-runtime-core"
67
- version = "0.20.2"
68
+ version = "0.20.3"
68
69
  dependencies = [
69
70
  "fs2",
70
71
  "serde",
@@ -73,7 +74,7 @@ dependencies = [
73
74
 
74
75
  [[package]]
75
76
  name = "omx-sparkshell"
76
- version = "0.20.2"
77
+ version = "0.20.3"
77
78
  dependencies = [
78
79
  "omx-mux",
79
80
  ]
package/Cargo.toml CHANGED
@@ -11,7 +11,7 @@ resolver = "2"
11
11
 
12
12
  [workspace.package]
13
13
 
14
- version = "0.20.2"
14
+ version = "0.20.3"
15
15
 
16
16
  edition = "2021"
17
17
  rust-version = "1.73"
package/README.md CHANGED
@@ -165,6 +165,32 @@ created and recovered. `--worktree` also moves the launch into a separate git
165
165
  checkout, which is the safer default when using `--madmax`. Replace
166
166
  `feat/task` with a branch-like name for the task.
167
167
 
168
+ ### Concurrent standard conversations
169
+
170
+ A standard launch owns one writable session pointer under its selected `OMX_ROOT`. A second ordinary `omx` launch from the same checkout therefore fails closed instead of sharing or silently changing that root. Give each additional conversation an explicit, distinct root:
171
+
172
+ ```bash
173
+ omx
174
+ OMX_ROOT="$HOME/.omx/instances/second-conversation" omx
175
+ OMX_ROOT="$HOME/.omx/instances/third-conversation" omx
176
+ ```
177
+
178
+ PowerShell:
179
+
180
+ ```powershell
181
+ $env:OMX_ROOT = "$HOME/.omx/instances/second-conversation"
182
+ omx
183
+ ```
184
+
185
+ Command Prompt:
186
+
187
+ ```bat
188
+ set "OMX_ROOT=%USERPROFILE%\.omx\instances\second-conversation"
189
+ omx
190
+ ```
191
+
192
+ User-specified roots are literal: launching twice with the same explicit `OMX_ROOT` remains a fatal owner conflict. Separate checkouts have separate default roots, while `--worktree` and `--madmax` keep their existing isolation behavior.
193
+
168
194
  ### Madmax and worktree launch safety
169
195
 
170
196
  `--madmax` is OMX shorthand for Codex
@@ -9,5 +9,6 @@ repository.workspace = true
9
9
  [dependencies]
10
10
  omx-mux = { path = "../omx-mux" }
11
11
  omx-runtime-core = { path = "../omx-runtime-core" }
12
+ fs2 = "0.4"
12
13
  serde_json = "1"
13
14
 
@@ -1,3 +1,4 @@
1
+ use fs2::FileExt;
1
2
  use omx_mux::{canonical_contract_summary, MuxAdapter, MuxOperation, MuxTarget, TmuxAdapter};
2
3
  use omx_runtime_core::{runtime_contract_summary, RuntimeCommand, RuntimeEngine};
3
4
  use std::env;
@@ -71,9 +72,31 @@ fn run() -> Result<(), String> {
71
72
  let json_input = second.ok_or("exec requires a JSON command argument")?;
72
73
  let state_dir = args.iter().find_map(|a| a.strip_prefix("--state-dir="));
73
74
  let compact = args.iter().any(|a| a == "--compact");
75
+ let _mutation_lock = if let Some(dir) = state_dir {
76
+ std::fs::create_dir_all(dir).map_err(|e| e.to_string())?;
77
+ let lock =
78
+ std::fs::File::create(std::path::Path::new(dir).join("runtime-mutation.lock"))
79
+ .map_err(|e| e.to_string())?;
80
+ lock.lock_exclusive().map_err(|e| e.to_string())?;
81
+ Some(lock)
82
+ } else {
83
+ None
84
+ };
74
85
  let mut engine = match state_dir {
75
- Some(dir) => RuntimeEngine::load(dir)
76
- .unwrap_or_else(|_| RuntimeEngine::new().with_state_dir(dir)),
86
+ Some(dir) => match RuntimeEngine::load(dir) {
87
+ Ok(engine) => engine,
88
+ Err(error) => {
89
+ let has_persisted_state = ["events.json", "snapshot.json", "mailbox.json"]
90
+ .iter()
91
+ .any(|name| std::path::Path::new(dir).join(name).exists());
92
+ if has_persisted_state {
93
+ return Err(format!(
94
+ "failed to load authoritative runtime state: {error}"
95
+ ));
96
+ }
97
+ RuntimeEngine::new().with_state_dir(dir)
98
+ }
99
+ },
77
100
  None => RuntimeEngine::new(),
78
101
  };
79
102
 
@@ -178,6 +178,56 @@ fn exec_with_state_dir_persists() {
178
178
  let _ = std::fs::remove_dir_all(&dir);
179
179
  }
180
180
 
181
+ #[test]
182
+ fn exec_rejects_missing_seen_ledger_after_persisted_dispatch_history() {
183
+ let dir = std::env::temp_dir().join("omx-runtime-test-exec-missing-seen-ledger");
184
+ let _ = std::fs::remove_dir_all(&dir);
185
+ let state_arg = format!("--state-dir={}", dir.display());
186
+ let queued = Command::new(env!("CARGO_BIN_EXE_omx-runtime"))
187
+ .args([
188
+ "exec",
189
+ r#"{"command":"QueueDispatch","request_id":"request-1","target":"worker"}"#,
190
+ &state_arg,
191
+ ])
192
+ .output()
193
+ .expect("queued dispatch");
194
+ assert!(queued.status.success());
195
+ std::fs::remove_file(dir.join("dispatch-seen.json")).expect("removed seen ledger");
196
+
197
+ let reloaded = Command::new(env!("CARGO_BIN_EXE_omx-runtime"))
198
+ .args(["exec", r#"{"command":"CaptureSnapshot"}"#, &state_arg])
199
+ .output()
200
+ .expect("reloaded persisted state");
201
+ assert!(!reloaded.status.success());
202
+ assert!(String::from_utf8_lossy(&reloaded.stderr).contains("missing dispatch seen ledger"));
203
+ let _ = std::fs::remove_dir_all(&dir);
204
+ }
205
+
206
+ #[test]
207
+ fn exec_ignores_dispatch_compat_without_authoritative_runtime_state() {
208
+ let dir = std::env::temp_dir().join("omx-runtime-test-dispatch-compat-only");
209
+ let _ = std::fs::remove_dir_all(&dir);
210
+ std::fs::create_dir_all(&dir).unwrap();
211
+ std::fs::write(dir.join("dispatch.json"), "[]\n").unwrap();
212
+
213
+ let cmd_json = r#"{"command":"CaptureSnapshot"}"#;
214
+ let state_arg = format!("--state-dir={}", dir.display());
215
+ let output = Command::new(env!("CARGO_BIN_EXE_omx-runtime"))
216
+ .args(["exec", cmd_json, &state_arg])
217
+ .output()
218
+ .expect("ran omx-runtime");
219
+
220
+ assert!(
221
+ output.status.success(),
222
+ "dispatch compatibility output must not poison fresh authoritative state: {}",
223
+ String::from_utf8_lossy(&output.stderr)
224
+ );
225
+ assert!(dir.join("events.json").exists());
226
+ assert!(dir.join("snapshot.json").exists());
227
+
228
+ let _ = std::fs::remove_dir_all(&dir);
229
+ }
230
+
181
231
  #[test]
182
232
  fn snapshot_from_state_dir_reads_persisted_state() {
183
233
  let dir = std::env::temp_dir().join("omx-runtime-test-snapshot-statedir");
@@ -210,3 +260,172 @@ fn snapshot_from_state_dir_reads_persisted_state() {
210
260
 
211
261
  let _ = std::fs::remove_dir_all(&dir);
212
262
  }
263
+
264
+ #[test]
265
+ fn exec_compact_immediately_persists_only_nonterminal_dispatch_records() {
266
+ let dir = std::env::temp_dir().join("omx-runtime-test-exec-compact-dispatch");
267
+ let _ = std::fs::remove_dir_all(&dir);
268
+ let state_arg = format!("--state-dir={}", dir.display());
269
+ let run_exec = |json: &str, compact: bool| {
270
+ let mut command = Command::new(env!("CARGO_BIN_EXE_omx-runtime"));
271
+ command.args(["exec", json, &state_arg]);
272
+ if compact {
273
+ command.arg("--compact");
274
+ }
275
+ let output = command.output().expect("ran omx-runtime");
276
+ assert!(
277
+ output.status.success(),
278
+ "exec failed: {}",
279
+ String::from_utf8_lossy(&output.stderr)
280
+ );
281
+ };
282
+
283
+ run_exec(
284
+ r#"{"command":"QueueDispatch","request_id":"pending","target":"worker-pending"}"#,
285
+ false,
286
+ );
287
+ run_exec(
288
+ r#"{"command":"QueueDispatch","request_id":"notified","target":"worker-notified"}"#,
289
+ false,
290
+ );
291
+ run_exec(
292
+ r#"{"command":"MarkNotified","request_id":"notified","channel":"tmux"}"#,
293
+ false,
294
+ );
295
+ run_exec(
296
+ r#"{"command":"QueueDispatch","request_id":"delivered","target":"worker-delivered"}"#,
297
+ false,
298
+ );
299
+ run_exec(
300
+ r#"{"command":"MarkNotified","request_id":"delivered","channel":"tmux"}"#,
301
+ false,
302
+ );
303
+ run_exec(
304
+ r#"{"command":"MarkDelivered","request_id":"delivered"}"#,
305
+ false,
306
+ );
307
+ run_exec(r#"{"command":"CaptureSnapshot"}"#, false);
308
+ run_exec(
309
+ r#"{"command":"QueueDispatch","request_id":"failed","target":"worker-failed"}"#,
310
+ false,
311
+ );
312
+ run_exec(
313
+ r#"{"command":"MarkFailed","request_id":"failed","reason":"target unavailable"}"#,
314
+ true,
315
+ );
316
+
317
+ let duplicate = Command::new(env!("CARGO_BIN_EXE_omx-runtime"))
318
+ .args([
319
+ "exec",
320
+ r#"{"command":"QueueDispatch","request_id":"failed","target":"replacement"}"#,
321
+ &state_arg,
322
+ ])
323
+ .output()
324
+ .expect("ran duplicate dispatch exec");
325
+ assert!(!duplicate.status.success());
326
+ assert!(String::from_utf8_lossy(&duplicate.stderr)
327
+ .contains("duplicate dispatch request id: failed"));
328
+ assert!(dir.join("dispatch-seen.json").exists());
329
+
330
+ let dispatch: serde_json::Value = serde_json::from_str(
331
+ &std::fs::read_to_string(dir.join("dispatch.json")).expect("read immediate dispatch view"),
332
+ )
333
+ .expect("valid dispatch JSON");
334
+ let records = dispatch["records"]
335
+ .as_array()
336
+ .expect("dispatch records array");
337
+ assert_eq!(records.len(), 2);
338
+ assert_eq!(records[0]["request_id"], "pending");
339
+ assert_eq!(records[0]["status"], "pending");
340
+ assert_eq!(records[1]["request_id"], "notified");
341
+ assert_eq!(records[1]["status"], "notified");
342
+
343
+ run_exec(r#"{"command":"CaptureSnapshot"}"#, false);
344
+ let reloaded_dispatch: serde_json::Value = serde_json::from_str(
345
+ &std::fs::read_to_string(dir.join("dispatch.json"))
346
+ .expect("read reload-persisted dispatch view"),
347
+ )
348
+ .expect("valid reload-persisted dispatch JSON");
349
+ let reloaded_records = reloaded_dispatch["records"]
350
+ .as_array()
351
+ .expect("reload-persisted dispatch records array");
352
+ assert_eq!(reloaded_records.len(), records.len());
353
+ for (immediate, reloaded) in records.iter().zip(reloaded_records) {
354
+ for field in ["request_id", "target", "status", "metadata", "reason"] {
355
+ assert_eq!(reloaded[field], immediate[field], "semantic field {field}");
356
+ }
357
+ }
358
+
359
+ let snapshot: serde_json::Value = serde_json::from_str(
360
+ &std::fs::read_to_string(dir.join("snapshot.json"))
361
+ .expect("read reload-persisted snapshot"),
362
+ )
363
+ .expect("valid reload-persisted snapshot JSON");
364
+ assert_eq!(snapshot["backlog"]["pending"], 1);
365
+ assert_eq!(snapshot["backlog"]["notified"], 1);
366
+ assert_eq!(snapshot["backlog"]["delivered"], 0);
367
+ assert_eq!(snapshot["backlog"]["failed"], 0);
368
+
369
+ let events: serde_json::Value = serde_json::from_str(
370
+ &std::fs::read_to_string(dir.join("events.json")).expect("read compacted events"),
371
+ )
372
+ .expect("valid events JSON");
373
+ let events = events.as_array().expect("events array");
374
+ assert!(events
375
+ .iter()
376
+ .any(|event| event["event"] == "SnapshotCaptured"));
377
+ assert!(events
378
+ .iter()
379
+ .all(|event| { event["request_id"] != "delivered" && event["request_id"] != "failed" }));
380
+
381
+ let event_request_ids: Vec<&str> = events
382
+ .iter()
383
+ .filter_map(|event| event["request_id"].as_str())
384
+ .collect();
385
+ assert_eq!(event_request_ids, vec!["pending", "notified", "notified"]);
386
+ let _ = std::fs::remove_dir_all(&dir);
387
+ }
388
+
389
+ #[test]
390
+ fn concurrent_exec_queue_accepts_exactly_one_request_id() {
391
+ let dir = std::env::temp_dir().join("omx-runtime-test-concurrent-dispatch-id");
392
+ let _ = std::fs::remove_dir_all(&dir);
393
+ let state_arg = format!("--state-dir={}", dir.display());
394
+ let barrier = std::sync::Arc::new(std::sync::Barrier::new(3));
395
+ let mut workers = Vec::new();
396
+ for target in ["worker-a", "worker-b"] {
397
+ let barrier = std::sync::Arc::clone(&barrier);
398
+ let state_arg = state_arg.clone();
399
+ workers.push(std::thread::spawn(move || {
400
+ barrier.wait();
401
+ Command::new(env!("CARGO_BIN_EXE_omx-runtime"))
402
+ .args([
403
+ "exec",
404
+ &format!(
405
+ r#"{{"command":"QueueDispatch","request_id":"concurrent","target":"{target}"}}"#
406
+ ),
407
+ &state_arg,
408
+ ])
409
+ .output()
410
+ .expect("ran concurrent dispatch exec")
411
+ .status
412
+ .success()
413
+ }));
414
+ }
415
+ barrier.wait();
416
+ assert_eq!(
417
+ workers
418
+ .into_iter()
419
+ .map(|worker| worker.join().unwrap())
420
+ .filter(|accepted| *accepted)
421
+ .count(),
422
+ 1
423
+ );
424
+ let dispatch: serde_json::Value = serde_json::from_str(
425
+ &std::fs::read_to_string(dir.join("dispatch.json")).expect("read dispatch view"),
426
+ )
427
+ .expect("valid dispatch JSON");
428
+ assert_eq!(dispatch["records"].as_array().unwrap().len(), 1);
429
+ assert_eq!(dispatch["records"][0]["request_id"], "concurrent");
430
+ let _ = std::fs::remove_dir_all(&dir);
431
+ }
@@ -39,6 +39,12 @@ pub struct DispatchRecord {
39
39
 
40
40
  #[derive(Debug, Clone, PartialEq, Eq)]
41
41
  pub enum DispatchError {
42
+ DuplicateRequestId {
43
+ request_id: String,
44
+ },
45
+ InvalidRequestId {
46
+ request_id: String,
47
+ },
42
48
  NotFound {
43
49
  request_id: String,
44
50
  },
@@ -52,6 +58,12 @@ pub enum DispatchError {
52
58
  impl fmt::Display for DispatchError {
53
59
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54
60
  match self {
61
+ Self::DuplicateRequestId { request_id } => {
62
+ write!(f, "duplicate dispatch request id: {request_id}")
63
+ }
64
+ Self::InvalidRequestId { request_id } => {
65
+ write!(f, "invalid dispatch request id: {request_id:?}")
66
+ }
55
67
  Self::NotFound { request_id } => {
56
68
  write!(f, "dispatch record not found: {request_id}")
57
69
  }
@@ -85,9 +97,20 @@ impl DispatchLog {
85
97
  request_id: impl Into<String>,
86
98
  target: impl Into<String>,
87
99
  metadata: Option<serde_json::Value>,
88
- ) {
100
+ ) -> Result<(), DispatchError> {
101
+ let request_id = request_id.into();
102
+ if request_id.is_empty() {
103
+ return Err(DispatchError::InvalidRequestId { request_id });
104
+ }
105
+ if self
106
+ .records
107
+ .iter()
108
+ .any(|record| record.request_id == request_id)
109
+ {
110
+ return Err(DispatchError::DuplicateRequestId { request_id });
111
+ }
89
112
  self.records.push(DispatchRecord {
90
- request_id: request_id.into(),
113
+ request_id,
91
114
  target: target.into(),
92
115
  status: DispatchStatus::Pending,
93
116
  created_at: now_iso(),
@@ -97,6 +120,7 @@ impl DispatchLog {
97
120
  reason: None,
98
121
  metadata,
99
122
  });
123
+ Ok(())
100
124
  }
101
125
 
102
126
  pub fn mark_notified(
@@ -156,6 +180,23 @@ impl DispatchLog {
156
180
  &self.records
157
181
  }
158
182
 
183
+ pub fn request_ids(&self) -> impl Iterator<Item = &str> {
184
+ self.records.iter().map(|record| record.request_id.as_str())
185
+ }
186
+
187
+ pub fn contains_request_id(&self, request_id: &str) -> bool {
188
+ self.records
189
+ .iter()
190
+ .any(|record| record.request_id == request_id)
191
+ }
192
+
193
+ /// Remove records that have reached a terminal delivery state.
194
+ pub fn prune_terminal_records(&mut self) {
195
+ self.records.retain(|record| {
196
+ record.status != DispatchStatus::Delivered && record.status != DispatchStatus::Failed
197
+ });
198
+ }
199
+
159
200
  pub fn to_backlog_snapshot(&self) -> BacklogSnapshot {
160
201
  let mut snapshot = BacklogSnapshot::default();
161
202
  for record in &self.records {
@@ -248,7 +289,7 @@ mod tests {
248
289
  #[test]
249
290
  fn queue_and_transition_happy_path() {
250
291
  let mut log = DispatchLog::new();
251
- log.queue("req-1", "worker-1", None);
292
+ log.queue("req-1", "worker-1", None).unwrap();
252
293
  assert_eq!(log.records().len(), 1);
253
294
  assert_eq!(log.records()[0].status, DispatchStatus::Pending);
254
295
 
@@ -262,7 +303,7 @@ mod tests {
262
303
  #[test]
263
304
  fn mark_failed_from_notified() {
264
305
  let mut log = DispatchLog::new();
265
- log.queue("req-1", "worker-1", None);
306
+ log.queue("req-1", "worker-1", None).unwrap();
266
307
  log.mark_notified("req-1", "tmux").unwrap();
267
308
  log.mark_failed("req-1", "send_error").unwrap();
268
309
  assert_eq!(log.records()[0].status, DispatchStatus::Failed);
@@ -271,7 +312,7 @@ mod tests {
271
312
  #[test]
272
313
  fn invalid_transition_errors() {
273
314
  let mut log = DispatchLog::new();
274
- log.queue("req-1", "worker-1", None);
315
+ log.queue("req-1", "worker-1", None).unwrap();
275
316
 
276
317
  // Can't deliver from pending
277
318
  let err = log.mark_delivered("req-1").unwrap_err();
@@ -282,7 +323,7 @@ mod tests {
282
323
  fn mark_failed_from_pending() {
283
324
  // Matches TS behavior: pending -> failed allowed for target resolution failures
284
325
  let mut log = DispatchLog::new();
285
- log.queue("req-1", "worker-1", None);
326
+ log.queue("req-1", "worker-1", None).unwrap();
286
327
  log.mark_failed("req-1", "target_resolution_failed")
287
328
  .unwrap();
288
329
  assert_eq!(log.records()[0].status, DispatchStatus::Failed);
@@ -298,9 +339,9 @@ mod tests {
298
339
  #[test]
299
340
  fn backlog_snapshot_counts() {
300
341
  let mut log = DispatchLog::new();
301
- log.queue("req-1", "w1", None);
302
- log.queue("req-2", "w2", None);
303
- log.queue("req-3", "w3", None);
342
+ log.queue("req-1", "w1", None).unwrap();
343
+ log.queue("req-2", "w2", None).unwrap();
344
+ log.queue("req-3", "w3", None).unwrap();
304
345
  log.mark_notified("req-2", "tmux").unwrap();
305
346
  log.mark_notified("req-3", "tmux").unwrap();
306
347
  log.mark_delivered("req-2").unwrap();
@@ -313,11 +354,34 @@ mod tests {
313
354
  assert_eq!(snap.failed, 1);
314
355
  }
315
356
 
357
+ #[test]
358
+ fn prune_terminal_records_keeps_pending_and_notified_records() {
359
+ let mut log = DispatchLog::new();
360
+ log.queue("pending", "w1", None).unwrap();
361
+ log.queue("notified", "w2", None).unwrap();
362
+ log.queue("delivered", "w3", None).unwrap();
363
+ log.queue("failed", "w4", None).unwrap();
364
+ log.mark_notified("notified", "tmux").unwrap();
365
+ log.mark_notified("delivered", "tmux").unwrap();
366
+ log.mark_delivered("delivered").unwrap();
367
+ log.mark_failed("failed", "target resolution failed")
368
+ .unwrap();
369
+
370
+ log.prune_terminal_records();
371
+
372
+ assert_eq!(log.records().len(), 2);
373
+ assert_eq!(log.records()[0].request_id, "pending");
374
+ assert_eq!(log.records()[0].status, DispatchStatus::Pending);
375
+ assert_eq!(log.records()[1].request_id, "notified");
376
+ assert_eq!(log.records()[1].status, DispatchStatus::Notified);
377
+ }
378
+
316
379
  #[test]
317
380
  fn queue_with_metadata_round_trips() {
318
381
  let mut log = DispatchLog::new();
319
382
  let meta = serde_json::json!({"priority": "high", "tags": ["urgent"]});
320
- log.queue("req-meta", "worker-1", Some(meta.clone()));
383
+ log.queue("req-meta", "worker-1", Some(meta.clone()))
384
+ .unwrap();
321
385
 
322
386
  assert_eq!(log.records()[0].metadata, Some(meta.clone()));
323
387
 
@@ -327,4 +391,26 @@ mod tests {
327
391
  assert_eq!(loaded.records()[0].metadata, Some(meta));
328
392
  assert_eq!(loaded.records()[0].request_id, "req-meta");
329
393
  }
394
+
395
+ #[test]
396
+ fn queue_rejects_duplicate_request_id_after_terminal_transition() {
397
+ let mut log = DispatchLog::new();
398
+ log.queue("req-1", "worker-1", None).unwrap();
399
+ log.mark_failed("req-1", "target unavailable").unwrap();
400
+
401
+ let err = log.queue("req-1", "worker-2", None).unwrap_err();
402
+ assert!(
403
+ matches!(err, DispatchError::DuplicateRequestId { request_id } if request_id == "req-1")
404
+ );
405
+ assert_eq!(log.records().len(), 1);
406
+ }
407
+
408
+ #[test]
409
+ fn queue_rejects_empty_request_id() {
410
+ let mut log = DispatchLog::new();
411
+ assert!(matches!(
412
+ log.queue("", "worker", None),
413
+ Err(DispatchError::InvalidRequestId { request_id }) if request_id.is_empty()
414
+ ));
415
+ }
330
416
  }