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
@@ -0,0 +1,129 @@
1
+ import assert from 'node:assert/strict';
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { pathToFileURL } from 'node:url';
7
+ import { parse as parseToml } from '@iarna/toml';
8
+ import { test } from 'node:test';
9
+ import {
10
+ assertInstalledReasoningDeclarationContract,
11
+ assertInstalledRootReasoningHelp,
12
+ assertInstalledRootReasoningRejection,
13
+ assertInstalledTeamSkillContract,
14
+ } from '../smoke-packed-install.js';
15
+
16
+ const root = process.cwd();
17
+ const dist = join(root, 'dist');
18
+
19
+ function read(relativePath: string): string {
20
+ return readFileSync(join(root, relativePath), 'utf-8');
21
+ }
22
+
23
+ test('compiled reasoning artifacts preserve exact surface-specific max contracts', async () => {
24
+ const requiredArtifacts = [
25
+ 'dist/config/models.js',
26
+ 'dist/config/models.d.ts',
27
+ 'dist/agents/definitions.js',
28
+ 'dist/agents/definitions.d.ts',
29
+ 'dist/agents/native-config.js',
30
+ 'dist/agents/native-config.d.ts',
31
+ 'dist/team/model-contract.js',
32
+ 'dist/team/model-contract.d.ts',
33
+ 'dist/cli/index.js',
34
+ 'dist/cli/index.d.ts',
35
+ 'dist/cli/omx.js',
36
+ 'dist/cli/omx.d.ts',
37
+ ];
38
+ for (const relativePath of requiredArtifacts) {
39
+ assert.equal(existsSync(join(root, relativePath)), true, `missing compiled artifact: ${relativePath}`);
40
+ }
41
+
42
+ const modelsDeclaration = read('dist/config/models.d.ts');
43
+ const definitionsDeclaration = read('dist/agents/definitions.d.ts');
44
+ const nativeDeclaration = read('dist/agents/native-config.d.ts');
45
+ const teamDeclaration = read('dist/team/model-contract.d.ts');
46
+ assertInstalledReasoningDeclarationContract({
47
+ models: modelsDeclaration,
48
+ definitions: definitionsDeclaration,
49
+ nativeConfig: nativeDeclaration,
50
+ team: teamDeclaration,
51
+ });
52
+
53
+ const models = await import(pathToFileURL(join(dist, 'config/models.js')).href);
54
+ assert.deepEqual(models.CANONICAL_REASONING_EFFORTS, ['low', 'medium', 'high', 'xhigh']);
55
+ assert.deepEqual(models.AMBIGUOUS_UNSUPPORTED_REASONING_EFFORTS, ['max', 'ultra']);
56
+ assert.deepEqual(models.PER_AGENT_REASONING_EFFORTS, ['low', 'medium', 'high', 'xhigh', 'max']);
57
+ assert.deepEqual(models.ROOT_REASONING_EFFORTS, ['low', 'medium', 'high', 'xhigh']);
58
+ assert.deepEqual(models.ROOT_UNSUPPORTED_REASONING_EFFORTS, ['max', 'ultra']);
59
+ assert.equal(models.isAmbiguousUnsupportedReasoningEffort('MAX'), true);
60
+ assert.equal(models.isUnsupportedRootReasoningEffort('MAX'), true);
61
+ assert.equal(models.isUnsupportedRootReasoningEffort('ultra'), true);
62
+ assert.equal(models.isUnsupportedRootReasoningEffort('xhigh'), false);
63
+ assert.equal(models.normalizeUnsupportedRootReasoningEffort(' MAX '), 'max');
64
+ assert.equal(models.normalizeUnsupportedRootReasoningEffort('Ultra'), 'ultra');
65
+ assert.equal(models.normalizeUnsupportedRootReasoningEffort('xhigh'), undefined);
66
+
67
+ const nativeConfig = await import(pathToFileURL(join(dist, 'agents/native-config.js')).href);
68
+ const definitions = await import(pathToFileURL(join(dist, 'agents/definitions.js')).href);
69
+ const codexHome = mkdtempSync(join(tmpdir(), 'omx-artifact-max-native-'));
70
+ try {
71
+ writeFileSync(join(codexHome, '.omx-config.json'), JSON.stringify({
72
+ agentReasoning: { architect: ' MAX ', critic: 'ultra' },
73
+ }));
74
+ const maxToml = nativeConfig.generateAgentToml(
75
+ definitions.AGENT_DEFINITIONS.architect,
76
+ 'compiled native max contract',
77
+ { codexHomeOverride: codexHome },
78
+ );
79
+ assert.match(maxToml, /model_reasoning_effort = "max"/);
80
+ assert.doesNotMatch(maxToml, /model_reasoning_effort = "xhigh"/);
81
+ assert.equal((parseToml(maxToml) as { model_reasoning_effort?: unknown }).model_reasoning_effort, 'max');
82
+
83
+ const fallbackToml = nativeConfig.generateAgentToml(
84
+ definitions.AGENT_DEFINITIONS.critic,
85
+ 'compiled native ultra fallback contract',
86
+ { codexHomeOverride: codexHome },
87
+ );
88
+ assert.match(fallbackToml, /model_reasoning_effort = "high"/);
89
+ assert.doesNotMatch(fallbackToml, /model_reasoning_effort = "ultra"/);
90
+ } finally {
91
+ rmSync(codexHome, { recursive: true, force: true });
92
+ }
93
+
94
+ const team = await import(pathToFileURL(join(dist, 'team/model-contract.js')).href);
95
+ const maxArgs = team.resolveTeamWorkerLaunchArgs({ preferredReasoning: 'max' });
96
+ assert.deepEqual(maxArgs, ['-c', 'model_reasoning_effort="max"']);
97
+ assert.equal(maxArgs.includes('model_reasoning_effort="xhigh"'), false);
98
+ const opaqueUltra = team.resolveTeamWorkerLaunchDiagnostics({
99
+ existingRaw: '-c model_reasoning_effort="ultra"',
100
+ preferredReasoning: 'max',
101
+ });
102
+ assert.equal(opaqueUltra.reasoningSource, 'explicit');
103
+ assert.equal(opaqueUltra.actualReasoning, undefined);
104
+
105
+ const canonicalSkill = readFileSync(join(root, 'skills/team/SKILL.md'));
106
+ const pluginSkill = readFileSync(join(root, 'plugins/oh-my-codex/skills/team/SKILL.md'));
107
+ assertInstalledTeamSkillContract(canonicalSkill, pluginSkill);
108
+ });
109
+
110
+ test('compiled root reasoning keeps four-value help and rejects max/ultra without mutation', () => {
111
+ const omxPath = join(dist, 'cli/omx.js');
112
+ const codexHome = mkdtempSync(join(tmpdir(), 'omx-artifact-root-reasoning-'));
113
+ try {
114
+ const env = { ...process.env, CODEX_HOME: codexHome };
115
+ const configPath = join(codexHome, 'config.toml');
116
+ const help = spawnSync(process.execPath, [omxPath, 'reasoning'], { encoding: 'utf-8', env });
117
+ assert.equal(help.status, 0, help.stderr);
118
+ assertInstalledRootReasoningHelp(help.stdout);
119
+ const max = spawnSync(process.execPath, [omxPath, 'reasoning', 'max'], { encoding: 'utf-8', env });
120
+ assertInstalledRootReasoningRejection('max', max, undefined, existsSync(configPath) ? readFileSync(configPath, 'utf-8') : undefined);
121
+
122
+ const originalConfig = 'model = "preserve-root-config"\n';
123
+ writeFileSync(configPath, originalConfig);
124
+ const ultra = spawnSync(process.execPath, [omxPath, 'reasoning', 'ultra'], { encoding: 'utf-8', env });
125
+ assertInstalledRootReasoningRejection('ultra', ultra, originalConfig, readFileSync(configPath, 'utf-8'));
126
+ } finally {
127
+ rmSync(codexHome, { recursive: true, force: true });
128
+ }
129
+ });
@@ -1,214 +1,77 @@
1
1
  import assert from 'node:assert/strict';
2
- import { mkdtemp, rm } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
4
  import { tmpdir } from 'node:os';
4
5
  import { join } from 'node:path';
5
6
  import { describe, it } from 'node:test';
6
7
 
7
- import { ralplanCommand } from '../../cli/ralplan.js';
8
- import { readSubagentTrackingState, recordSubagentTurnForSession } from '../../subagents/tracker.js';
8
+ import { recordSubagentTurnForSession, readSubagentTrackingState } from '../../subagents/tracker.js';
9
9
  import { dispatchCodexNativeHook } from '../codex-native-hook.js';
10
10
 
11
- async function invokeRoleIntent(cwd: string, args: string[]) {
12
- const stdout: string[] = [];
13
- const stderr: string[] = [];
14
- const previous = process.exitCode;
15
- try {
16
- process.exitCode = undefined;
17
- await ralplanCommand(args, { cwd: () => cwd, stdout: (l) => stdout.push(l), stderr: (l) => stderr.push(l) });
18
- return { stdout, stderr, exitCode: process.exitCode };
19
- } finally {
20
- process.exitCode = previous;
21
- }
22
- }
23
-
24
- describe('#3181 end-to-end fresh App turn bootstrap', () => {
25
- it('SessionStart reconcile alone neither attests nor authorizes a role intent (fail-closed; positive provenance required)', async () => {
26
- const cwd = await mkdtemp(join(tmpdir(), 'omx-3181-e2e-'));
27
- const priorEnv = { OMX_SESSION_ID: process.env.OMX_SESSION_ID, CODEX_SESSION_ID: process.env.CODEX_SESSION_ID, SESSION_ID: process.env.SESSION_ID };
28
- try {
29
- delete process.env.OMX_SESSION_ID;
30
- delete process.env.CODEX_SESSION_ID;
31
- delete process.env.SESSION_ID;
32
- const nativeSessionId = 'codex-native-fresh-app';
33
-
34
- // 1. Fresh App/outside-tmux turn start: no session.json, no tracker yet.
35
- await dispatchCodexNativeHook(
36
- { hook_event_name: 'SessionStart', cwd, session_id: nativeSessionId },
37
- { cwd, sessionOwnerPid: process.pid },
38
- );
39
-
40
- // 2. SessionStart reconciles the canonical pointer but does NOT attest a leader
41
- // (a null transcript cannot positively classify a root vs a malformed child) and
42
- // writes no positively-provenanced tracker leader.
43
- const afterStart = await readSubagentTrackingState(cwd);
44
- assert.equal(afterStart.sessions[nativeSessionId]?.leader_attested_at, undefined, 'SessionStart must not attest a leader');
45
-
46
- // 3. A role-intent write on the reconciled-pointer-only session FAILS CLOSED: the bare
47
- // session.json native_session_id is not a trusted leader anchor (an ambiguous /
48
- // malformed-child SessionStart could set it). Authorization requires positive
49
- // provenance — a PreToolUse attestation (next test) or a recorded tracker leader.
50
- const res = await invokeRoleIntent(cwd, ['role-intent', 'write', '--role', 'architect', '--parent-thread', nativeSessionId, '--json']);
51
- assert.equal(res.exitCode, 1);
52
- assert.deepEqual(JSON.parse(res.stdout.join('\n')), { ok: false, reason: 'parent_not_active_leader' });
53
-
54
- const finalState = await readSubagentTrackingState(cwd);
55
- assert.deepEqual(finalState.pending_role_intents, []);
56
- } finally {
57
- if (priorEnv.OMX_SESSION_ID !== undefined) process.env.OMX_SESSION_ID = priorEnv.OMX_SESSION_ID;
58
- if (priorEnv.CODEX_SESSION_ID !== undefined) process.env.CODEX_SESSION_ID = priorEnv.CODEX_SESSION_ID;
59
- if (priorEnv.SESSION_ID !== undefined) process.env.SESSION_ID = priorEnv.SESSION_ID;
60
- await rm(cwd, { recursive: true, force: true });
61
- }
62
- });
63
-
64
- it('PreToolUse (leader turn) bootstraps the pointer + attestation when SessionStart did not, so the first role-intent write succeeds', async () => {
65
- const cwd = await mkdtemp(join(tmpdir(), 'omx-3181-e2e-pretool-'));
66
- const priorEnv = { OMX_SESSION_ID: process.env.OMX_SESSION_ID, CODEX_SESSION_ID: process.env.CODEX_SESSION_ID, SESSION_ID: process.env.SESSION_ID };
11
+ describe('#3194 role-intent native-hook e2e', () => {
12
+ it('does not treat legacy thread_id as leader authority and preserves a child record', async () => {
13
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-3194-'));
67
14
  try {
68
- delete process.env.OMX_SESSION_ID;
69
- delete process.env.CODEX_SESSION_ID;
70
- delete process.env.SESSION_ID;
71
- const nativeSessionId = 'codex-native-exec-leader';
72
-
73
- // Fresh exec turn where the first event reaching OMX is a leader PreToolUse (no
74
- // prior SessionStart pointer). The leader turn carries thread_id == session_id.
75
- await dispatchCodexNativeHook(
76
- {
77
- hook_event_name: 'PreToolUse',
78
- cwd,
79
- session_id: nativeSessionId,
80
- thread_id: nativeSessionId,
81
- tool_name: 'Bash',
82
- tool_use_id: 'tool-exec-first',
83
- tool_input: { command: 'omx ralplan role-intent write --role architect --parent-thread "$CODEX_THREAD_ID" --json' },
84
- },
85
- { cwd, sessionOwnerPid: process.pid },
86
- );
87
-
88
- const afterPreTool = await readSubagentTrackingState(cwd);
89
- const attested = afterPreTool.sessions[nativeSessionId];
90
- assert.equal(attested?.leader_thread_id, nativeSessionId);
91
- assert.equal(attested?.leader_attest_source, 'native-pretooluse');
92
-
93
- const res = await invokeRoleIntent(cwd, ['role-intent', 'write', '--role', 'architect', '--parent-thread', nativeSessionId, '--json']);
94
- assert.equal(res.exitCode, undefined);
95
- const receipt = JSON.parse(res.stdout.join('\n')) as { ok: boolean; intent: { role: string } };
96
- assert.equal(receipt.ok, true);
97
- assert.equal(receipt.intent.role, 'architect');
98
- assert.equal((await readSubagentTrackingState(cwd)).pending_role_intents.length, 1);
99
- } finally {
100
- if (priorEnv.OMX_SESSION_ID !== undefined) process.env.OMX_SESSION_ID = priorEnv.OMX_SESSION_ID;
101
- if (priorEnv.CODEX_SESSION_ID !== undefined) process.env.CODEX_SESSION_ID = priorEnv.CODEX_SESSION_ID;
102
- if (priorEnv.SESSION_ID !== undefined) process.env.SESSION_ID = priorEnv.SESSION_ID;
103
- await rm(cwd, { recursive: true, force: true });
104
- }
105
- });
106
-
107
- it('never attests a thread durably tracked as a subagent, even via a source-less leader-shaped PreToolUse', async () => {
108
- const cwd = await mkdtemp(join(tmpdir(), 'omx-3181-e2e-child-'));
109
- const priorEnv = { OMX_SESSION_ID: process.env.OMX_SESSION_ID, CODEX_SESSION_ID: process.env.CODEX_SESSION_ID, SESSION_ID: process.env.SESSION_ID };
110
- try {
111
- delete process.env.OMX_SESSION_ID;
112
- delete process.env.CODEX_SESSION_ID;
113
- delete process.env.SESSION_ID;
114
- const childThreadId = 'codex-native-child-thread';
115
-
116
- // A child is durably recorded as a subagent (e.g. at its own SessionStart).
117
15
  await recordSubagentTurnForSession(cwd, {
118
- sessionId: 'leader-session',
119
- threadId: childThreadId,
120
- kind: 'subagent',
121
- leaderThreadId: 'leader-session',
122
- timestamp: new Date().toISOString(),
16
+ sessionId: 'synthetic-leader-session', threadId: 'synthetic-child-thread', kind: 'subagent',
17
+ leaderThreadId: 'synthetic-leader-thread', timestamp: new Date(0).toISOString(),
123
18
  });
124
-
125
- // The child then emits a source-less, untyped, leader-shaped PreToolUse
126
- // (thread_id === session_id). It must NOT be promoted to leader.
127
- await dispatchCodexNativeHook(
128
- {
129
- hook_event_name: 'PreToolUse',
130
- cwd,
131
- session_id: childThreadId,
132
- thread_id: childThreadId,
133
- tool_name: 'Bash',
134
- tool_use_id: 'tool-child-selfpromote',
135
- tool_input: { command: 'omx ralplan role-intent write --role architect --parent-thread "$CODEX_THREAD_ID" --json' },
19
+ const before = await readSubagentTrackingState(cwd);
20
+ const result = await dispatchCodexNativeHook({
21
+ hook_event_name: 'PreToolUse', cwd, session_id: 'synthetic-child-thread', thread_id: 'synthetic-child-thread',
22
+ tool_name: 'Bash', tool_use_id: 'synthetic-tool-use', tool_input: { command: 'omx ralplan role-intent write --role architect --parent-thread "$CODEX_THREAD_ID" --json' },
23
+ }, { cwd, sessionOwnerPid: process.pid });
24
+ assert.deepEqual(result.outputJson, {
25
+ hookSpecificOutput: {
26
+ hookEventName: 'PreToolUse', permissionDecision: 'deny',
27
+ permissionDecisionReason: 'unsupported_documented_leader_proof: Codex 0.144.5 hooks do not expose documented root identity required for adapted Ralplan.',
136
28
  },
137
- { cwd, sessionOwnerPid: process.pid },
138
- );
139
-
140
- const state = await readSubagentTrackingState(cwd);
141
- assert.equal(state.sessions[childThreadId]?.leader_attested_at, undefined, 'child thread must not be attested as leader');
29
+ });
30
+ assert.deepEqual(await readSubagentTrackingState(cwd), before);
142
31
  } finally {
143
- if (priorEnv.OMX_SESSION_ID !== undefined) process.env.OMX_SESSION_ID = priorEnv.OMX_SESSION_ID;
144
- if (priorEnv.CODEX_SESSION_ID !== undefined) process.env.CODEX_SESSION_ID = priorEnv.CODEX_SESSION_ID;
145
- if (priorEnv.SESSION_ID !== undefined) process.env.SESSION_ID = priorEnv.SESSION_ID;
146
32
  await rm(cwd, { recursive: true, force: true });
147
33
  }
148
34
  });
149
35
 
150
- it('never bootstraps a leader from a PreToolUse carrying a malformed/blank thread_spawn carrier', async () => {
151
- const cwd = await mkdtemp(join(tmpdir(), 'omx-3181-e2e-malformed-spawn-'));
152
- const priorEnv = { OMX_SESSION_ID: process.env.OMX_SESSION_ID, CODEX_SESSION_ID: process.env.CODEX_SESSION_ID, SESSION_ID: process.env.SESSION_ID };
36
+ it('does not create session state for a fresh documented payload without thread_id', async () => {
37
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-3194-'));
153
38
  try {
154
- delete process.env.OMX_SESSION_ID;
155
- delete process.env.CODEX_SESSION_ID;
156
- delete process.env.SESSION_ID;
157
- const childThreadId = 'codex-native-malformed-spawn';
158
- // Present-but-malformed thread_spawn carrier (blank parent id) is still child
159
- // provenance and must veto leader bootstrap: no pointer, no attestation.
160
- await dispatchCodexNativeHook(
161
- {
162
- hook_event_name: 'PreToolUse',
163
- cwd,
164
- session_id: childThreadId,
165
- thread_id: childThreadId,
166
- tool_name: 'Bash',
167
- tool_use_id: 'tool-malformed-spawn',
168
- source: { subagent: { thread_spawn: { parent_thread_id: '' } } },
169
- tool_input: { command: 'omx ralplan role-intent write --role architect --parent-thread "$CODEX_THREAD_ID" --json' },
39
+ const result = await dispatchCodexNativeHook({
40
+ hook_event_name: 'PreToolUse', cwd, session_id: 'synthetic-session', tool_name: 'Bash', tool_use_id: 'synthetic-tool-use',
41
+ tool_input: { command: 'omx ralplan role-intent write --role architect --parent-thread "$CODEX_THREAD_ID" --json' },
42
+ }, { cwd, sessionOwnerPid: process.pid });
43
+ assert.deepEqual(result.outputJson, {
44
+ hookSpecificOutput: {
45
+ hookEventName: 'PreToolUse', permissionDecision: 'deny',
46
+ permissionDecisionReason: 'unsupported_documented_leader_proof: Codex 0.144.5 hooks do not expose documented root identity required for adapted Ralplan.',
170
47
  },
171
- { cwd, sessionOwnerPid: process.pid },
172
- );
173
- const state = await readSubagentTrackingState(cwd);
174
- assert.equal(state.sessions[childThreadId]?.leader_attested_at, undefined, 'malformed thread_spawn must not attest as leader');
48
+ });
49
+ assert.equal(existsSync(join(cwd, '.omx', 'state')), false);
175
50
  } finally {
176
- if (priorEnv.OMX_SESSION_ID !== undefined) process.env.OMX_SESSION_ID = priorEnv.OMX_SESSION_ID;
177
- if (priorEnv.CODEX_SESSION_ID !== undefined) process.env.CODEX_SESSION_ID = priorEnv.CODEX_SESSION_ID;
178
- if (priorEnv.SESSION_ID !== undefined) process.env.SESSION_ID = priorEnv.SESSION_ID;
179
51
  await rm(cwd, { recursive: true, force: true });
180
52
  }
181
53
  });
182
54
 
183
- it('never bootstraps a leader from a PreToolUse carrying an explicit non-installed agent_role', async () => {
184
- const cwd = await mkdtemp(join(tmpdir(), 'omx-3181-e2e-unknown-role-'));
185
- const priorEnv = { OMX_SESSION_ID: process.env.OMX_SESSION_ID, CODEX_SESSION_ID: process.env.CODEX_SESSION_ID, SESSION_ID: process.env.SESSION_ID };
55
+ it('does not mutate a foreign or stale pointer before denying the documented payload', async () => {
56
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-3194-'));
186
57
  try {
187
- delete process.env.OMX_SESSION_ID;
188
- delete process.env.CODEX_SESSION_ID;
189
- delete process.env.SESSION_ID;
190
- const childThreadId = 'codex-native-unknown-role';
191
- // An explicit but non-installed agent role is still role provenance and must veto
192
- // leader bootstrap even though it does not resolve to an installed OMX agent.
193
- await dispatchCodexNativeHook(
194
- {
195
- hook_event_name: 'PreToolUse',
196
- cwd,
197
- session_id: childThreadId,
198
- thread_id: childThreadId,
199
- agent_role: 'collaboration-child',
200
- tool_name: 'Bash',
201
- tool_use_id: 'tool-unknown-role',
202
- tool_input: { command: 'omx ralplan role-intent write --role architect --parent-thread "$CODEX_THREAD_ID" --json' },
58
+ const stateDir = join(cwd, '.omx', 'state');
59
+ const pointerPath = join(stateDir, 'session.json');
60
+ const pointer = '{"session_id":"synthetic-foreign","native_session_id":"synthetic-foreign","started_at":"1970-01-01T00:00:00.000Z"}\n';
61
+ await mkdir(stateDir, { recursive: true });
62
+ await writeFile(pointerPath, pointer);
63
+ const result = await dispatchCodexNativeHook({
64
+ hook_event_name: 'PreToolUse', cwd, session_id: 'synthetic-current', tool_name: 'Bash', tool_use_id: 'synthetic-tool-use',
65
+ tool_input: { command: 'omx ralplan role-intent write --role architect --parent-thread "$CODEX_THREAD_ID" --json' },
66
+ }, { cwd, sessionOwnerPid: process.pid });
67
+ assert.deepEqual(result.outputJson, {
68
+ hookSpecificOutput: {
69
+ hookEventName: 'PreToolUse', permissionDecision: 'deny',
70
+ permissionDecisionReason: 'unsupported_documented_leader_proof: Codex 0.144.5 hooks do not expose documented root identity required for adapted Ralplan.',
203
71
  },
204
- { cwd, sessionOwnerPid: process.pid },
205
- );
206
- const state = await readSubagentTrackingState(cwd);
207
- assert.equal(state.sessions[childThreadId]?.leader_attested_at, undefined, 'explicit non-installed agent_role must not attest as leader');
72
+ });
73
+ assert.equal(await readFile(pointerPath, 'utf8'), pointer);
208
74
  } finally {
209
- if (priorEnv.OMX_SESSION_ID !== undefined) process.env.OMX_SESSION_ID = priorEnv.OMX_SESSION_ID;
210
- if (priorEnv.CODEX_SESSION_ID !== undefined) process.env.CODEX_SESSION_ID = priorEnv.CODEX_SESSION_ID;
211
- if (priorEnv.SESSION_ID !== undefined) process.env.SESSION_ID = priorEnv.SESSION_ID;
212
75
  await rm(cwd, { recursive: true, force: true });
213
76
  }
214
77
  });
@@ -1,193 +1,60 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
4
+ import { dirname, join } from 'node:path';
5
5
  import { describe, it } from 'node:test';
6
6
 
7
- import { ralplanCommand } from '../../cli/ralplan.js';
8
- import { readSubagentTrackingState, subagentTrackingPath } from '../../subagents/tracker.js';
9
- import { dispatchCodexNativeHook } from '../codex-native-hook.js';
7
+ import {
8
+ bindPendingRoleIntentUnderLock,
9
+ readSubagentTrackingState,
10
+ readSubagentTrackingStateStrict,
11
+ recordPendingRoleIntent,
12
+ subagentTrackingPath,
13
+ } from '../../subagents/tracker.js';
10
14
 
11
- // #3181 durable bootstrap-order recovery: a crash/restart AFTER the authenticated adapted
12
- // Architect role intent is durably recorded but BEFORE the first worker spawn must, on the
13
- // SAME canonical session, recover the EXACT pending intent/receipt/spawn_task_name before
14
- // any leader action. This is durable-ordering behavior, not initial-run field presence.
15
+ const ARCHITECT_TOKEN = 'abcdef0123456789abcdef0123456789';
16
+ const CRITIC_TOKEN = '00112233445566778899aabbccddeeff';
15
17
 
16
- interface Receipt {
17
- ok: boolean;
18
- intent: { role: string; session_id: string; parent_thread_id: string; correlation_token: string };
19
- spawn_task_name: string;
18
+ async function withCwd(fn: (cwd: string) => Promise<void>): Promise<void> {
19
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-3194-'));
20
+ try { await fn(cwd); } finally { await rm(cwd, { recursive: true, force: true }); }
20
21
  }
21
22
 
22
- async function invoke(cwd: string, args: string[]): Promise<{ json: unknown; exitCode: number | undefined }> {
23
- const stdout: string[] = [];
24
- const stderr: string[] = [];
25
- const previous = process.exitCode;
26
- try {
27
- process.exitCode = undefined;
28
- await ralplanCommand(args, { cwd: () => cwd, stdout: (l) => stdout.push(l), stderr: (l) => stderr.push(l) });
29
- return { json: JSON.parse(stdout.join('\n')), exitCode: process.exitCode };
30
- } finally {
31
- process.exitCode = previous;
32
- }
33
- }
34
-
35
- function roleIntentArgs(role: string, parentThread: string): string[] {
36
- return ['role-intent', 'write', '--role', role, '--parent-thread', parentThread, '--json'];
37
- }
38
-
39
- async function withFreshEnv(fn: () => Promise<void>): Promise<void> {
40
- const prior = { OMX_SESSION_ID: process.env.OMX_SESSION_ID, CODEX_SESSION_ID: process.env.CODEX_SESSION_ID, SESSION_ID: process.env.SESSION_ID };
41
- delete process.env.OMX_SESSION_ID;
42
- delete process.env.CODEX_SESSION_ID;
43
- delete process.env.SESSION_ID;
44
- try {
45
- await fn();
46
- } finally {
47
- for (const key of ['OMX_SESSION_ID', 'CODEX_SESSION_ID', 'SESSION_ID'] as const) {
48
- if (prior[key] === undefined) delete process.env[key];
49
- else process.env[key] = prior[key];
50
- }
51
- }
52
- }
53
-
54
- // Both plugin and legacy delivery drive the same native hook + CLI + tracker recovery path.
55
- // Leader attestation is established only on the strictly-gated fresh leader PreToolUse
56
- // entry point (the role-intent command's own PreToolUse fires before it executes); both
57
- // deliveries route through it, so durable recovery is proven identically for each.
58
- const DELIVERIES: Array<{ label: string; event: 'SessionStart' | 'PreToolUse' }> = [
59
- { label: 'plugin (PreToolUse-seeded)', event: 'PreToolUse' },
60
- { label: 'legacy (PreToolUse-seeded)', event: 'PreToolUse' },
61
- ];
62
-
63
- async function seedAuthenticatedLeader(cwd: string, sessionId: string, event: 'SessionStart' | 'PreToolUse'): Promise<void> {
64
- if (event === 'SessionStart') {
65
- await dispatchCodexNativeHook({ hook_event_name: 'SessionStart', cwd, session_id: sessionId }, { cwd, sessionOwnerPid: process.pid });
66
- } else {
67
- await dispatchCodexNativeHook(
68
- {
69
- hook_event_name: 'PreToolUse',
70
- cwd,
71
- session_id: sessionId,
72
- thread_id: sessionId,
73
- tool_name: 'Bash',
74
- tool_use_id: 'tool-first',
75
- tool_input: { command: 'omx ralplan role-intent write --role architect --parent-thread "$CODEX_THREAD_ID" --json' },
76
- },
77
- { cwd, sessionOwnerPid: process.pid },
78
- );
79
- }
80
- }
81
-
82
- describe('#3181 durable bootstrap-order recovery', () => {
83
- for (const { label, event } of DELIVERIES) {
84
- it(`${label}: same-session resume recovers the exact adapted Architect intent/receipt/spawn_task_name before any spawn`, async () => {
85
- const cwd = await mkdtemp(join(tmpdir(), 'omx-3181-recover-'));
86
- await withFreshEnv(async () => {
87
- try {
88
- const sessionId = `codex-native-${event}`;
89
- await seedAuthenticatedLeader(cwd, sessionId, event);
90
-
91
- // Durably record the Architect intent (before any worker spawn).
92
- const first = (await invoke(cwd, roleIntentArgs('architect', sessionId))).json as Receipt;
93
- assert.equal(first.ok, true);
94
- assert.equal(first.intent.role, 'architect');
95
- const token = first.intent.correlation_token;
96
- const spawnTaskName = first.spawn_task_name;
97
-
98
- // Crash/restart-before-first-spawn: durable state persists on disk. On the SAME
99
- // canonical session, the resumed leader re-runs role-intent and MUST recover the
100
- // exact intent/receipt/spawn_task_name — never a fresh unrelated intent.
101
- const resumed = (await invoke(cwd, roleIntentArgs('architect', sessionId))).json as Receipt;
102
- assert.equal(resumed.intent.correlation_token, token, 'exact correlation token must be recovered');
103
- assert.equal(resumed.spawn_task_name, spawnTaskName, 'exact spawn_task_name must be recovered');
104
-
105
- const state = await readSubagentTrackingState(cwd);
106
- assert.equal(state.pending_role_intents.length, 1, 'no unrelated replacement intent may be created');
107
- assert.equal(state.pending_role_intents[0]?.role, 'architect');
108
-
109
- // Ordering invariant: while the Architect intent is still live (pre-spawn), a Critic
110
- // request for the same leader must NOT reorder/replace it — single_flight_conflict.
111
- const critic = (await invoke(cwd, roleIntentArgs('critic', sessionId))).json as { ok: boolean; reason?: string };
112
- assert.deepEqual(critic, { ok: false, reason: 'single_flight_conflict' });
113
- assert.equal((await readSubagentTrackingState(cwd)).pending_role_intents.length, 1);
114
- } finally {
115
- await rm(cwd, { recursive: true, force: true });
116
- }
23
+ describe('#3194 pending role-intent durability', () => {
24
+ it('keeps Architect before Critic as distinct single-flight intents and preserves binding integrity', async () => {
25
+ await withCwd(async (cwd) => {
26
+ const architect = recordPendingRoleIntent(cwd, {
27
+ role: 'architect', sessionId: 'synthetic-session', parentThreadId: 'synthetic-parent', correlationToken: ARCHITECT_TOKEN, nowMs: 0,
117
28
  });
118
- });
119
- }
120
-
121
- it('idempotent retry after crash-during-recovery converges to one intent/receipt', async () => {
122
- const cwd = await mkdtemp(join(tmpdir(), 'omx-3181-recover-retry-'));
123
- await withFreshEnv(async () => {
124
- try {
125
- const sessionId = 'codex-native-retry';
126
- await seedAuthenticatedLeader(cwd, sessionId, 'PreToolUse');
127
- const receipts: string[] = [];
128
- for (let i = 0; i < 3; i += 1) {
129
- const r = (await invoke(cwd, roleIntentArgs('architect', sessionId))).json as Receipt;
130
- receipts.push(r.intent.correlation_token);
131
- }
132
- assert.equal(new Set(receipts).size, 1, 'every retry recovers the same receipt');
133
- assert.equal((await readSubagentTrackingState(cwd)).pending_role_intents.length, 1);
134
- } finally {
135
- await rm(cwd, { recursive: true, force: true });
136
- }
137
- });
138
- });
139
-
140
- it('foreign-session resume fails closed and never adopts the foreign leader', async () => {
141
- const cwd = await mkdtemp(join(tmpdir(), 'omx-3181-recover-foreign-'));
142
- const prior = process.env.OMX_SESSION_ID;
143
- try {
144
- delete process.env.CODEX_SESSION_ID;
145
- delete process.env.SESSION_ID;
146
- // Real leader session A is authenticated and has a durable intent.
147
- await withFreshEnv(async () => {
148
- await seedAuthenticatedLeader(cwd, 'codex-native-A', 'PreToolUse');
149
- const r = (await invoke(cwd, roleIntentArgs('architect', 'codex-native-A'))).json as Receipt;
150
- assert.equal(r.ok, true);
29
+ assert.equal(architect.ok, true);
30
+ const critic = recordPendingRoleIntent(cwd, {
31
+ role: 'critic', sessionId: 'synthetic-session', parentThreadId: 'synthetic-parent', correlationToken: CRITIC_TOKEN, nowMs: 0,
151
32
  });
152
- // A foreign process selects session A by environment while the usable pointer is A's
153
- // session.json but resumes from a DIFFERENT workspace is simulated by pointing the
154
- // env at A while attempting to bind a foreign leader thread.
155
- process.env.OMX_SESSION_ID = 'codex-native-A';
156
- const foreign = (await invoke(cwd, roleIntentArgs('architect', 'attacker-thread'))).json as { ok: boolean; reason?: string };
157
- assert.equal(foreign.ok, false);
158
- assert.equal(foreign.reason, 'native_anchor_mismatch');
159
- } finally {
160
- if (prior === undefined) delete process.env.OMX_SESSION_ID;
161
- else process.env.OMX_SESSION_ID = prior;
162
- await rm(cwd, { recursive: true, force: true });
163
- }
33
+ assert.deepEqual(critic, { ok: false, reason: 'single_flight_conflict' });
34
+ const binding = bindPendingRoleIntentUnderLock(cwd, {
35
+ sessionId: 'synthetic-session', parentThreadId: 'synthetic-parent', correlationToken: ARCHITECT_TOKEN, nowMs: 1,
36
+ }, (state) => state);
37
+ assert.equal(binding?.role, 'architect');
38
+ assert.equal(binding?.alreadyBound, false);
39
+ const retry = bindPendingRoleIntentUnderLock(cwd, {
40
+ sessionId: 'synthetic-session', parentThreadId: 'synthetic-parent', correlationToken: ARCHITECT_TOKEN, nowMs: 2,
41
+ }, () => { throw new Error('an existing binding must not invoke the callback'); });
42
+ assert.equal(retry?.alreadyBound, true);
43
+ const state = await readSubagentTrackingState(cwd);
44
+ assert.equal(state.pending_role_intents.length, 1);
45
+ assert.equal(state.pending_role_intents[0]?.binding_state, 'bound');
46
+ assert.equal(state.pending_role_intents[0]?.correlation_token, ARCHITECT_TOKEN);
47
+ });
164
48
  });
165
49
 
166
- it('malformed/corrupt durable tracker on resume fails closed (native_anchor_unavailable) and never overwrites the evidence', async () => {
167
- const cwd = await mkdtemp(join(tmpdir(), 'omx-3181-recover-malformed-'));
168
- await withFreshEnv(async () => {
169
- try {
170
- const sessionId = 'codex-native-malformed';
171
- await seedAuthenticatedLeader(cwd, sessionId, 'PreToolUse');
172
- (await invoke(cwd, roleIntentArgs('architect', sessionId))).json as Receipt;
173
- // Corrupt the durable tracker. Per FR-20 the malformed evidence must not be trusted:
174
- // the CLI strict-reads it and fails closed rather than reading it as empty and
175
- // overwriting it via the legacy path (which would erase leader/subagent evidence).
176
- const corrupt = '{ corrupt tracker not valid json';
177
- await mkdir(join(cwd, '.omx', 'state'), { recursive: true });
178
- await writeFile(subagentTrackingPath(cwd), corrupt);
179
-
180
- // A spoofed parent AND the real native leader both fail closed on a corrupt tracker.
181
- const spoofed = (await invoke(cwd, roleIntentArgs('architect', 'attacker-thread'))).json as { ok: boolean; reason?: string };
182
- assert.deepEqual(spoofed, { ok: false, reason: 'native_anchor_unavailable' }, 'spoof on a corrupt tracker fails closed');
183
- const nativeLeader = (await invoke(cwd, roleIntentArgs('architect', sessionId))).json as { ok: boolean; reason?: string };
184
- assert.deepEqual(nativeLeader, { ok: false, reason: 'native_anchor_unavailable' }, 'even the native leader fails closed on a corrupt tracker');
185
-
186
- // The corrupt tracker is never overwritten (evidence preserved, not silently reset).
187
- assert.equal(await readFile(subagentTrackingPath(cwd), 'utf-8'), corrupt, 'corrupt tracker must not be overwritten');
188
- } finally {
189
- await rm(cwd, { recursive: true, force: true });
190
- }
50
+ it('strictly rejects corrupt durable tracker bytes without mutation', async () => {
51
+ await withCwd(async (cwd) => {
52
+ const path = subagentTrackingPath(cwd);
53
+ const corrupt = '{ synthetic corrupt tracker';
54
+ await mkdir(dirname(path), { recursive: true });
55
+ await writeFile(path, corrupt);
56
+ assert.deepEqual(await readSubagentTrackingStateStrict(cwd), { ok: false });
57
+ assert.equal(await readFile(path, 'utf8'), corrupt);
191
58
  });
192
59
  });
193
60
  });