oh-my-codex 0.15.2 → 0.16.0

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 (524) hide show
  1. package/Cargo.lock +10 -7
  2. package/Cargo.toml +1 -1
  3. package/README.md +3 -0
  4. package/crates/omx-explore/Cargo.toml +3 -0
  5. package/crates/omx-explore/src/main.rs +517 -16
  6. package/dist/agents/__tests__/native-config.test.js +33 -0
  7. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  8. package/dist/autoresearch/goal.d.ts +90 -0
  9. package/dist/autoresearch/goal.d.ts.map +1 -0
  10. package/dist/autoresearch/goal.js +237 -0
  11. package/dist/autoresearch/goal.js.map +1 -0
  12. package/dist/autoresearch/skill-validation.d.ts +1 -0
  13. package/dist/autoresearch/skill-validation.d.ts.map +1 -1
  14. package/dist/autoresearch/skill-validation.js +10 -3
  15. package/dist/autoresearch/skill-validation.js.map +1 -1
  16. package/dist/catalog/__tests__/generator.test.js +9 -4
  17. package/dist/catalog/__tests__/generator.test.js.map +1 -1
  18. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js +29 -2
  19. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js.map +1 -1
  20. package/dist/catalog/__tests__/schema.test.js +14 -3
  21. package/dist/catalog/__tests__/schema.test.js.map +1 -1
  22. package/dist/catalog/schema.js +1 -1
  23. package/dist/catalog/schema.js.map +1 -1
  24. package/dist/cli/__tests__/autoresearch-goal.test.d.ts +2 -0
  25. package/dist/cli/__tests__/autoresearch-goal.test.d.ts.map +1 -0
  26. package/dist/cli/__tests__/autoresearch-goal.test.js +194 -0
  27. package/dist/cli/__tests__/autoresearch-goal.test.js.map +1 -0
  28. package/dist/cli/__tests__/cleanup.test.js +82 -1
  29. package/dist/cli/__tests__/cleanup.test.js.map +1 -1
  30. package/dist/cli/__tests__/codex-plugin-layout.test.js +7 -4
  31. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  32. package/dist/cli/__tests__/doctor-context-window-warning.test.d.ts +2 -0
  33. package/dist/cli/__tests__/doctor-context-window-warning.test.d.ts.map +1 -0
  34. package/dist/cli/__tests__/doctor-context-window-warning.test.js +122 -0
  35. package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -0
  36. package/dist/cli/__tests__/doctor-warning-copy.test.js +25 -2
  37. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  38. package/dist/cli/__tests__/exec.test.js +1 -0
  39. package/dist/cli/__tests__/exec.test.js.map +1 -1
  40. package/dist/cli/__tests__/explore.test.js +48 -18
  41. package/dist/cli/__tests__/explore.test.js.map +1 -1
  42. package/dist/cli/__tests__/index.test.js +222 -10
  43. package/dist/cli/__tests__/index.test.js.map +1 -1
  44. package/dist/cli/__tests__/launch-fallback.test.js +58 -0
  45. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  46. package/dist/cli/__tests__/mcp-serve.test.js +27 -1
  47. package/dist/cli/__tests__/mcp-serve.test.js.map +1 -1
  48. package/dist/cli/__tests__/native-assets.test.js +26 -1
  49. package/dist/cli/__tests__/native-assets.test.js.map +1 -1
  50. package/dist/cli/__tests__/package-bin-contract.test.js +2 -2
  51. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  52. package/dist/cli/__tests__/performance-goal.test.d.ts +2 -0
  53. package/dist/cli/__tests__/performance-goal.test.d.ts.map +1 -0
  54. package/dist/cli/__tests__/performance-goal.test.js +144 -0
  55. package/dist/cli/__tests__/performance-goal.test.js.map +1 -0
  56. package/dist/cli/__tests__/question.test.js +8 -0
  57. package/dist/cli/__tests__/question.test.js.map +1 -1
  58. package/dist/cli/__tests__/ralph-goal-mode-contract.test.d.ts +2 -0
  59. package/dist/cli/__tests__/ralph-goal-mode-contract.test.d.ts.map +1 -0
  60. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +31 -0
  61. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -0
  62. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js +5 -4
  63. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -1
  64. package/dist/cli/__tests__/ralph-prd-smoke.test.js +7 -0
  65. package/dist/cli/__tests__/ralph-prd-smoke.test.js.map +1 -1
  66. package/dist/cli/__tests__/ralph.test.js +59 -1
  67. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  68. package/dist/cli/__tests__/setup-install-mode.test.js +57 -21
  69. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  70. package/dist/cli/__tests__/setup-refresh.test.js +27 -8
  71. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  72. package/dist/cli/__tests__/setup-scope.test.js +20 -10
  73. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  74. package/dist/cli/__tests__/setup-skill-validation.test.js +11 -11
  75. package/dist/cli/__tests__/setup-skill-validation.test.js.map +1 -1
  76. package/dist/cli/__tests__/setup-skills-overwrite.test.js +12 -12
  77. package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
  78. package/dist/cli/__tests__/team.test.js +242 -10
  79. package/dist/cli/__tests__/team.test.js.map +1 -1
  80. package/dist/cli/__tests__/ultragoal.test.d.ts +2 -0
  81. package/dist/cli/__tests__/ultragoal.test.d.ts.map +1 -0
  82. package/dist/cli/__tests__/ultragoal.test.js +106 -0
  83. package/dist/cli/__tests__/ultragoal.test.js.map +1 -0
  84. package/dist/cli/__tests__/uninstall.test.js +11 -0
  85. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  86. package/dist/cli/autoresearch-goal.d.ts +3 -0
  87. package/dist/cli/autoresearch-goal.d.ts.map +1 -0
  88. package/dist/cli/autoresearch-goal.js +175 -0
  89. package/dist/cli/autoresearch-goal.js.map +1 -0
  90. package/dist/cli/cleanup.d.ts +3 -1
  91. package/dist/cli/cleanup.d.ts.map +1 -1
  92. package/dist/cli/cleanup.js +42 -2
  93. package/dist/cli/cleanup.js.map +1 -1
  94. package/dist/cli/doctor.d.ts.map +1 -1
  95. package/dist/cli/doctor.js +95 -3
  96. package/dist/cli/doctor.js.map +1 -1
  97. package/dist/cli/explore.d.ts.map +1 -1
  98. package/dist/cli/explore.js +10 -2
  99. package/dist/cli/explore.js.map +1 -1
  100. package/dist/cli/index.d.ts +21 -2
  101. package/dist/cli/index.d.ts.map +1 -1
  102. package/dist/cli/index.js +268 -30
  103. package/dist/cli/index.js.map +1 -1
  104. package/dist/cli/mcp-serve.d.ts +1 -0
  105. package/dist/cli/mcp-serve.d.ts.map +1 -1
  106. package/dist/cli/mcp-serve.js +8 -0
  107. package/dist/cli/mcp-serve.js.map +1 -1
  108. package/dist/cli/native-assets.js +1 -1
  109. package/dist/cli/native-assets.js.map +1 -1
  110. package/dist/cli/performance-goal.d.ts +3 -0
  111. package/dist/cli/performance-goal.d.ts.map +1 -0
  112. package/dist/cli/performance-goal.js +186 -0
  113. package/dist/cli/performance-goal.js.map +1 -0
  114. package/dist/cli/ralph.d.ts +2 -0
  115. package/dist/cli/ralph.d.ts.map +1 -1
  116. package/dist/cli/ralph.js +25 -1
  117. package/dist/cli/ralph.js.map +1 -1
  118. package/dist/cli/setup.d.ts.map +1 -1
  119. package/dist/cli/setup.js +13 -6
  120. package/dist/cli/setup.js.map +1 -1
  121. package/dist/cli/team.d.ts +6 -0
  122. package/dist/cli/team.d.ts.map +1 -1
  123. package/dist/cli/team.js +113 -33
  124. package/dist/cli/team.js.map +1 -1
  125. package/dist/cli/tmux-hook.d.ts.map +1 -1
  126. package/dist/cli/tmux-hook.js +2 -1
  127. package/dist/cli/tmux-hook.js.map +1 -1
  128. package/dist/cli/ultragoal.d.ts +3 -0
  129. package/dist/cli/ultragoal.d.ts.map +1 -0
  130. package/dist/cli/ultragoal.js +191 -0
  131. package/dist/cli/ultragoal.js.map +1 -0
  132. package/dist/cli/uninstall.d.ts.map +1 -1
  133. package/dist/cli/uninstall.js +4 -2
  134. package/dist/cli/uninstall.js.map +1 -1
  135. package/dist/config/__tests__/generator-idempotent.test.js +39 -6
  136. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  137. package/dist/config/__tests__/generator-notify.test.js +5 -0
  138. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  139. package/dist/config/commit-lore-guard.d.ts +3 -0
  140. package/dist/config/commit-lore-guard.d.ts.map +1 -0
  141. package/dist/config/commit-lore-guard.js +9 -0
  142. package/dist/config/commit-lore-guard.js.map +1 -0
  143. package/dist/config/generator.d.ts +14 -4
  144. package/dist/config/generator.d.ts.map +1 -1
  145. package/dist/config/generator.js +166 -66
  146. package/dist/config/generator.js.map +1 -1
  147. package/dist/config/omx-first-party-mcp.d.ts +1 -0
  148. package/dist/config/omx-first-party-mcp.d.ts.map +1 -1
  149. package/dist/config/omx-first-party-mcp.js +4 -1
  150. package/dist/config/omx-first-party-mcp.js.map +1 -1
  151. package/dist/goal-workflows/__tests__/artifacts.test.d.ts +2 -0
  152. package/dist/goal-workflows/__tests__/artifacts.test.d.ts.map +1 -0
  153. package/dist/goal-workflows/__tests__/artifacts.test.js +96 -0
  154. package/dist/goal-workflows/__tests__/artifacts.test.js.map +1 -0
  155. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.d.ts +2 -0
  156. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.d.ts.map +1 -0
  157. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +54 -0
  158. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -0
  159. package/dist/goal-workflows/artifacts.d.ts +62 -0
  160. package/dist/goal-workflows/artifacts.d.ts.map +1 -0
  161. package/dist/goal-workflows/artifacts.js +132 -0
  162. package/dist/goal-workflows/artifacts.js.map +1 -0
  163. package/dist/goal-workflows/codex-goal-snapshot.d.ts +28 -0
  164. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -0
  165. package/dist/goal-workflows/codex-goal-snapshot.js +110 -0
  166. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -0
  167. package/dist/goal-workflows/handoff.d.ts +10 -0
  168. package/dist/goal-workflows/handoff.d.ts.map +1 -0
  169. package/dist/goal-workflows/handoff.js +31 -0
  170. package/dist/goal-workflows/handoff.js.map +1 -0
  171. package/dist/goal-workflows/validation.d.ts +13 -0
  172. package/dist/goal-workflows/validation.d.ts.map +1 -0
  173. package/dist/goal-workflows/validation.js +36 -0
  174. package/dist/goal-workflows/validation.js.map +1 -0
  175. package/dist/hooks/__tests__/agents-overlay.test.js +59 -0
  176. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  177. package/dist/hooks/__tests__/anti-slop-workflow.test.js +109 -18
  178. package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
  179. package/dist/hooks/__tests__/keyword-detector.test.js +45 -32
  180. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  181. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +3 -3
  182. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  183. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +2 -1
  184. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  185. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +17 -24
  186. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  187. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +3 -3
  188. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
  189. package/dist/hooks/__tests__/task-size-detector.test.js +1 -1
  190. package/dist/hooks/__tests__/task-size-detector.test.js.map +1 -1
  191. package/dist/hooks/__tests__/visual-ralph-skill.test.js +3 -3
  192. package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -1
  193. package/dist/hooks/__tests__/visual-verdict-loop.test.js +7 -11
  194. package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +1 -1
  195. package/dist/hooks/agents-overlay.d.ts.map +1 -1
  196. package/dist/hooks/agents-overlay.js +23 -2
  197. package/dist/hooks/agents-overlay.js.map +1 -1
  198. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  199. package/dist/hooks/keyword-detector.js +12 -13
  200. package/dist/hooks/keyword-detector.js.map +1 -1
  201. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  202. package/dist/hooks/keyword-registry.js +2 -10
  203. package/dist/hooks/keyword-registry.js.map +1 -1
  204. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  205. package/dist/hooks/prompt-guidance-contract.js +0 -4
  206. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  207. package/dist/hooks/session.js +2 -2
  208. package/dist/hooks/session.js.map +1 -1
  209. package/dist/hooks/task-size-detector.d.ts.map +1 -1
  210. package/dist/hooks/task-size-detector.js +1 -0
  211. package/dist/hooks/task-size-detector.js.map +1 -1
  212. package/dist/hud/__tests__/index.test.js +30 -14
  213. package/dist/hud/__tests__/index.test.js.map +1 -1
  214. package/dist/hud/__tests__/reconcile.test.js +29 -7
  215. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  216. package/dist/hud/reconcile.d.ts +2 -1
  217. package/dist/hud/reconcile.d.ts.map +1 -1
  218. package/dist/hud/reconcile.js +12 -0
  219. package/dist/hud/reconcile.js.map +1 -1
  220. package/dist/mcp/__tests__/bootstrap.test.js +15 -2
  221. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  222. package/dist/mcp/__tests__/state-paths.test.js +54 -0
  223. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  224. package/dist/mcp/__tests__/state-server.test.js +36 -0
  225. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  226. package/dist/mcp/bootstrap.d.ts +1 -1
  227. package/dist/mcp/bootstrap.d.ts.map +1 -1
  228. package/dist/mcp/bootstrap.js +9 -7
  229. package/dist/mcp/bootstrap.js.map +1 -1
  230. package/dist/mcp/state-paths.d.ts +17 -0
  231. package/dist/mcp/state-paths.d.ts.map +1 -1
  232. package/dist/mcp/state-paths.js +36 -2
  233. package/dist/mcp/state-paths.js.map +1 -1
  234. package/dist/modes/__tests__/base-session-scope.test.js +26 -0
  235. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  236. package/dist/modes/base.d.ts +1 -0
  237. package/dist/modes/base.d.ts.map +1 -1
  238. package/dist/modes/base.js +35 -5
  239. package/dist/modes/base.js.map +1 -1
  240. package/dist/notifications/__tests__/http-client.test.d.ts +2 -0
  241. package/dist/notifications/__tests__/http-client.test.d.ts.map +1 -0
  242. package/dist/notifications/__tests__/http-client.test.js +90 -0
  243. package/dist/notifications/__tests__/http-client.test.js.map +1 -0
  244. package/dist/notifications/__tests__/notifier.test.js +22 -60
  245. package/dist/notifications/__tests__/notifier.test.js.map +1 -1
  246. package/dist/notifications/dispatcher.d.ts.map +1 -1
  247. package/dist/notifications/dispatcher.js +35 -60
  248. package/dist/notifications/dispatcher.js.map +1 -1
  249. package/dist/notifications/http-client.d.ts +22 -0
  250. package/dist/notifications/http-client.d.ts.map +1 -0
  251. package/dist/notifications/http-client.js +298 -0
  252. package/dist/notifications/http-client.js.map +1 -0
  253. package/dist/notifications/notifier.d.ts +3 -2
  254. package/dist/notifications/notifier.d.ts.map +1 -1
  255. package/dist/notifications/notifier.js +17 -22
  256. package/dist/notifications/notifier.js.map +1 -1
  257. package/dist/openclaw/__tests__/dispatcher.test.js +63 -2
  258. package/dist/openclaw/__tests__/dispatcher.test.js.map +1 -1
  259. package/dist/openclaw/dispatcher.d.ts.map +1 -1
  260. package/dist/openclaw/dispatcher.js +3 -2
  261. package/dist/openclaw/dispatcher.js.map +1 -1
  262. package/dist/performance-goal/artifacts.d.ts +76 -0
  263. package/dist/performance-goal/artifacts.d.ts.map +1 -0
  264. package/dist/performance-goal/artifacts.js +221 -0
  265. package/dist/performance-goal/artifacts.js.map +1 -0
  266. package/dist/pipeline/__tests__/stages.test.js +423 -14
  267. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  268. package/dist/pipeline/stages/team-exec.d.ts +8 -4
  269. package/dist/pipeline/stages/team-exec.d.ts.map +1 -1
  270. package/dist/pipeline/stages/team-exec.js +181 -13
  271. package/dist/pipeline/stages/team-exec.js.map +1 -1
  272. package/dist/planning/__tests__/artifacts.test.js +261 -1
  273. package/dist/planning/__tests__/artifacts.test.js.map +1 -1
  274. package/dist/planning/artifact-names.d.ts +13 -0
  275. package/dist/planning/artifact-names.d.ts.map +1 -0
  276. package/dist/planning/artifact-names.js +108 -0
  277. package/dist/planning/artifact-names.js.map +1 -0
  278. package/dist/planning/artifacts.d.ts +23 -1
  279. package/dist/planning/artifacts.d.ts.map +1 -1
  280. package/dist/planning/artifacts.js +171 -59
  281. package/dist/planning/artifacts.js.map +1 -1
  282. package/dist/ralph/__tests__/persistence.test.js +21 -1
  283. package/dist/ralph/__tests__/persistence.test.js.map +1 -1
  284. package/dist/ralph/persistence.d.ts.map +1 -1
  285. package/dist/ralph/persistence.js +6 -4
  286. package/dist/ralph/persistence.js.map +1 -1
  287. package/dist/ralplan/__tests__/runtime.test.js +2 -0
  288. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  289. package/dist/ralplan/runtime.d.ts.map +1 -1
  290. package/dist/ralplan/runtime.js +6 -0
  291. package/dist/ralplan/runtime.js.map +1 -1
  292. package/dist/scripts/__tests__/codex-native-hook.test.js +1749 -88
  293. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  294. package/dist/scripts/__tests__/hook-derived-watcher.test.js +33 -1
  295. package/dist/scripts/__tests__/hook-derived-watcher.test.js.map +1 -1
  296. package/dist/scripts/__tests__/run-test-files.test.js +36 -0
  297. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
  298. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  299. package/dist/scripts/codex-native-hook.js +570 -45
  300. package/dist/scripts/codex-native-hook.js.map +1 -1
  301. package/dist/scripts/codex-native-pre-post.d.ts +7 -0
  302. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  303. package/dist/scripts/codex-native-pre-post.js +341 -15
  304. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  305. package/dist/scripts/hook-derived-watcher.js +2 -1
  306. package/dist/scripts/hook-derived-watcher.js.map +1 -1
  307. package/dist/scripts/notify-fallback-watcher.js +2 -1
  308. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  309. package/dist/scripts/notify-hook/orchestration-intent.d.ts +1 -2
  310. package/dist/scripts/notify-hook/orchestration-intent.d.ts.map +1 -1
  311. package/dist/scripts/notify-hook/orchestration-intent.js +2 -3
  312. package/dist/scripts/notify-hook/orchestration-intent.js.map +1 -1
  313. package/dist/scripts/notify-hook/team-leader-nudge.d.ts +0 -2
  314. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  315. package/dist/scripts/notify-hook/team-leader-nudge.js +8 -60
  316. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  317. package/dist/scripts/notify-hook/team-worker-posttooluse.js +1 -1
  318. package/dist/scripts/notify-hook/team-worker-posttooluse.js.map +1 -1
  319. package/dist/scripts/notify-hook/team-worker-stop.d.ts +15 -0
  320. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -0
  321. package/dist/scripts/notify-hook/team-worker-stop.js +224 -0
  322. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -0
  323. package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
  324. package/dist/scripts/notify-hook/team-worker.js +26 -18
  325. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  326. package/dist/scripts/notify-hook.js +1 -1
  327. package/dist/scripts/notify-hook.js.map +1 -1
  328. package/dist/scripts/run-test-files.js +17 -1
  329. package/dist/scripts/run-test-files.js.map +1 -1
  330. package/dist/scripts/sync-plugin-mirror.d.ts +1 -0
  331. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  332. package/dist/scripts/sync-plugin-mirror.js +10 -4
  333. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  334. package/dist/state/__tests__/operations.test.js +26 -0
  335. package/dist/state/__tests__/operations.test.js.map +1 -1
  336. package/dist/state/__tests__/skill-active.test.js +76 -0
  337. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  338. package/dist/state/operations.d.ts +3 -1
  339. package/dist/state/operations.d.ts.map +1 -1
  340. package/dist/state/operations.js +8 -4
  341. package/dist/state/operations.js.map +1 -1
  342. package/dist/state/skill-active.d.ts +1 -0
  343. package/dist/state/skill-active.d.ts.map +1 -1
  344. package/dist/state/skill-active.js +54 -13
  345. package/dist/state/skill-active.js.map +1 -1
  346. package/dist/team/__tests__/api-interop.test.js +279 -0
  347. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  348. package/dist/team/__tests__/approved-execution.test.d.ts +2 -0
  349. package/dist/team/__tests__/approved-execution.test.d.ts.map +1 -0
  350. package/dist/team/__tests__/approved-execution.test.js +124 -0
  351. package/dist/team/__tests__/approved-execution.test.js.map +1 -0
  352. package/dist/team/__tests__/delivery-e2e-smoke.test.js +2 -4
  353. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  354. package/dist/team/__tests__/delivery-log.test.d.ts +2 -0
  355. package/dist/team/__tests__/delivery-log.test.d.ts.map +1 -0
  356. package/dist/team/__tests__/delivery-log.test.js +44 -0
  357. package/dist/team/__tests__/delivery-log.test.js.map +1 -0
  358. package/dist/team/__tests__/model-contract.test.js +40 -9
  359. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  360. package/dist/team/__tests__/repo-aware-decomposition.test.js +41 -0
  361. package/dist/team/__tests__/repo-aware-decomposition.test.js.map +1 -1
  362. package/dist/team/__tests__/role-router.test.js +4 -4
  363. package/dist/team/__tests__/role-router.test.js.map +1 -1
  364. package/dist/team/__tests__/runtime-boxed-state.test.d.ts +2 -0
  365. package/dist/team/__tests__/runtime-boxed-state.test.d.ts.map +1 -0
  366. package/dist/team/__tests__/runtime-boxed-state.test.js +39 -0
  367. package/dist/team/__tests__/runtime-boxed-state.test.js.map +1 -0
  368. package/dist/team/__tests__/runtime-cli.test.js +24 -0
  369. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  370. package/dist/team/__tests__/runtime.test.js +563 -72
  371. package/dist/team/__tests__/runtime.test.js.map +1 -1
  372. package/dist/team/__tests__/state-root.test.js +13 -0
  373. package/dist/team/__tests__/state-root.test.js.map +1 -1
  374. package/dist/team/__tests__/state.test.js +13 -0
  375. package/dist/team/__tests__/state.test.js.map +1 -1
  376. package/dist/team/__tests__/team-identity.test.d.ts +2 -0
  377. package/dist/team/__tests__/team-identity.test.d.ts.map +1 -0
  378. package/dist/team/__tests__/team-identity.test.js +166 -0
  379. package/dist/team/__tests__/team-identity.test.js.map +1 -0
  380. package/dist/team/__tests__/tmux-session.test.js +58 -1
  381. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  382. package/dist/team/__tests__/worker-bootstrap.test.js +62 -0
  383. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  384. package/dist/team/api-interop.d.ts +1 -0
  385. package/dist/team/api-interop.d.ts.map +1 -1
  386. package/dist/team/api-interop.js +163 -132
  387. package/dist/team/api-interop.js.map +1 -1
  388. package/dist/team/approved-execution.d.ts +37 -0
  389. package/dist/team/approved-execution.d.ts.map +1 -0
  390. package/dist/team/approved-execution.js +136 -0
  391. package/dist/team/approved-execution.js.map +1 -0
  392. package/dist/team/delivery-log.d.ts +1 -1
  393. package/dist/team/delivery-log.d.ts.map +1 -1
  394. package/dist/team/delivery-log.js +2 -1
  395. package/dist/team/delivery-log.js.map +1 -1
  396. package/dist/team/followup-planner.js +2 -2
  397. package/dist/team/followup-planner.js.map +1 -1
  398. package/dist/team/goal-workflow.d.ts +20 -0
  399. package/dist/team/goal-workflow.d.ts.map +1 -0
  400. package/dist/team/goal-workflow.js +57 -0
  401. package/dist/team/goal-workflow.js.map +1 -0
  402. package/dist/team/orchestrator.js +2 -2
  403. package/dist/team/orchestrator.js.map +1 -1
  404. package/dist/team/repo-aware-decomposition.d.ts +3 -0
  405. package/dist/team/repo-aware-decomposition.d.ts.map +1 -1
  406. package/dist/team/repo-aware-decomposition.js +2 -0
  407. package/dist/team/repo-aware-decomposition.js.map +1 -1
  408. package/dist/team/role-router.js +5 -5
  409. package/dist/team/role-router.js.map +1 -1
  410. package/dist/team/runtime-cli.d.ts +32 -2
  411. package/dist/team/runtime-cli.d.ts.map +1 -1
  412. package/dist/team/runtime-cli.js +78 -26
  413. package/dist/team/runtime-cli.js.map +1 -1
  414. package/dist/team/runtime.d.ts +7 -1
  415. package/dist/team/runtime.d.ts.map +1 -1
  416. package/dist/team/runtime.js +383 -40
  417. package/dist/team/runtime.js.map +1 -1
  418. package/dist/team/scaling.d.ts.map +1 -1
  419. package/dist/team/scaling.js +2 -0
  420. package/dist/team/scaling.js.map +1 -1
  421. package/dist/team/state.d.ts +9 -0
  422. package/dist/team/state.d.ts.map +1 -1
  423. package/dist/team/state.js +21 -0
  424. package/dist/team/state.js.map +1 -1
  425. package/dist/team/team-identity.d.ts +26 -0
  426. package/dist/team/team-identity.d.ts.map +1 -0
  427. package/dist/team/team-identity.js +169 -0
  428. package/dist/team/team-identity.js.map +1 -0
  429. package/dist/team/tmux-session.d.ts +18 -0
  430. package/dist/team/tmux-session.d.ts.map +1 -1
  431. package/dist/team/tmux-session.js +65 -3
  432. package/dist/team/tmux-session.js.map +1 -1
  433. package/dist/team/worker-bootstrap.d.ts +4 -0
  434. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  435. package/dist/team/worker-bootstrap.js +28 -2
  436. package/dist/team/worker-bootstrap.js.map +1 -1
  437. package/dist/ultragoal/__tests__/artifacts.test.d.ts +2 -0
  438. package/dist/ultragoal/__tests__/artifacts.test.d.ts.map +1 -0
  439. package/dist/ultragoal/__tests__/artifacts.test.js +93 -0
  440. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -0
  441. package/dist/ultragoal/artifacts.d.ts +89 -0
  442. package/dist/ultragoal/artifacts.d.ts.map +1 -0
  443. package/dist/ultragoal/artifacts.js +233 -0
  444. package/dist/ultragoal/artifacts.js.map +1 -0
  445. package/dist/utils/__tests__/agents-model-table.test.js +3 -1
  446. package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
  447. package/dist/utils/__tests__/paths.test.js +31 -1
  448. package/dist/utils/__tests__/paths.test.js.map +1 -1
  449. package/dist/utils/agents-model-table.d.ts.map +1 -1
  450. package/dist/utils/agents-model-table.js +12 -1
  451. package/dist/utils/agents-model-table.js.map +1 -1
  452. package/dist/utils/paths.d.ts +2 -0
  453. package/dist/utils/paths.d.ts.map +1 -1
  454. package/dist/utils/paths.js +23 -7
  455. package/dist/utils/paths.js.map +1 -1
  456. package/dist/verification/__tests__/ci-rust-gates.test.js +30 -19
  457. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  458. package/package.json +5 -5
  459. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  460. package/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md +30 -5
  461. package/plugins/oh-my-codex/skills/ask/SKILL.md +58 -0
  462. package/plugins/oh-my-codex/skills/autoresearch-goal/SKILL.md +36 -0
  463. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +2 -2
  464. package/plugins/oh-my-codex/skills/performance-goal/SKILL.md +65 -0
  465. package/plugins/oh-my-codex/skills/plan/SKILL.md +1 -1
  466. package/plugins/oh-my-codex/skills/ralph/SKILL.md +22 -3
  467. package/plugins/oh-my-codex/skills/team/SKILL.md +6 -2
  468. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +49 -0
  469. package/plugins/oh-my-codex/skills/visual-ralph/SKILL.md +9 -9
  470. package/prompts/api-reviewer.md +1 -1
  471. package/prompts/code-reviewer.md +2 -0
  472. package/prompts/performance-reviewer.md +1 -1
  473. package/prompts/quality-reviewer.md +1 -1
  474. package/prompts/quality-strategist.md +2 -2
  475. package/prompts/style-reviewer.md +1 -1
  476. package/prompts/test-engineer.md +1 -1
  477. package/skills/ai-slop-cleaner/SKILL.md +30 -5
  478. package/skills/ask/SKILL.md +58 -0
  479. package/skills/ask-claude/SKILL.md +3 -54
  480. package/skills/ask-gemini/SKILL.md +3 -54
  481. package/skills/autoresearch-goal/SKILL.md +36 -0
  482. package/skills/build-fix/SKILL.md +4 -139
  483. package/skills/deepsearch/SKILL.md +4 -32
  484. package/skills/ecomode/SKILL.md +4 -108
  485. package/skills/help/SKILL.md +4 -196
  486. package/skills/note/SKILL.md +4 -56
  487. package/skills/omx-setup/SKILL.md +2 -2
  488. package/skills/performance-goal/SKILL.md +65 -0
  489. package/skills/plan/SKILL.md +1 -1
  490. package/skills/ralph/SKILL.md +22 -3
  491. package/skills/ralph-init/SKILL.md +4 -40
  492. package/skills/review/SKILL.md +4 -32
  493. package/skills/security-review/SKILL.md +4 -294
  494. package/skills/swarm/SKILL.md +4 -19
  495. package/skills/tdd/SKILL.md +4 -100
  496. package/skills/team/SKILL.md +6 -2
  497. package/skills/trace/SKILL.md +4 -27
  498. package/skills/ultragoal/SKILL.md +49 -0
  499. package/skills/visual-ralph/SKILL.md +9 -9
  500. package/skills/visual-verdict/SKILL.md +4 -70
  501. package/skills/web-clone/SKILL.md +4 -18
  502. package/src/scripts/__tests__/codex-native-hook.test.ts +2923 -1030
  503. package/src/scripts/__tests__/hook-derived-watcher.test.ts +45 -1
  504. package/src/scripts/__tests__/run-test-files.test.ts +46 -0
  505. package/src/scripts/codex-native-hook.ts +696 -46
  506. package/src/scripts/codex-native-pre-post.ts +369 -16
  507. package/src/scripts/hook-derived-watcher.ts +2 -1
  508. package/src/scripts/notify-fallback-watcher.ts +2 -1
  509. package/src/scripts/notify-hook/orchestration-intent.ts +1 -3
  510. package/src/scripts/notify-hook/team-leader-nudge.ts +7 -63
  511. package/src/scripts/notify-hook/team-worker-posttooluse.ts +1 -1
  512. package/src/scripts/notify-hook/team-worker-stop.ts +246 -0
  513. package/src/scripts/notify-hook/team-worker.ts +23 -14
  514. package/src/scripts/notify-hook.ts +1 -1
  515. package/src/scripts/run-test-files.ts +20 -1
  516. package/src/scripts/sync-plugin-mirror.ts +13 -4
  517. package/templates/catalog-manifest.json +45 -27
  518. package/plugins/oh-my-codex/skills/ask-claude/SKILL.md +0 -61
  519. package/plugins/oh-my-codex/skills/ask-gemini/SKILL.md +0 -61
  520. package/plugins/oh-my-codex/skills/help/SKILL.md +0 -202
  521. package/plugins/oh-my-codex/skills/note/SKILL.md +0 -62
  522. package/plugins/oh-my-codex/skills/security-review/SKILL.md +0 -300
  523. package/plugins/oh-my-codex/skills/trace/SKILL.md +0 -33
  524. package/plugins/oh-my-codex/skills/visual-verdict/SKILL.md +0 -76
@@ -13,6 +13,7 @@ import { dispatchCodexNativeHook, isCodexNativeHookMainModule, mapCodexHookEvent
13
13
  import { writeSessionStart } from "../../hooks/session.js";
14
14
  import { resetTriageConfigCache } from "../../hooks/triage-config.js";
15
15
  import { executeStateOperation } from "../../state/operations.js";
16
+ import { OMX_TMUX_HUD_OWNER_ENV } from "../../hud/reconcile.js";
16
17
  function nativeHookScriptPath() {
17
18
  return join(process.cwd(), "dist", "scripts", "codex-native-hook.js");
18
19
  }
@@ -35,6 +36,50 @@ async function writeJson(path, value) {
35
36
  await mkdir(dirname(path), { recursive: true }).catch(() => { });
36
37
  await writeFile(path, JSON.stringify(value, null, 2));
37
38
  }
39
+ function buildWorkerStopFakeTmux(tmuxLogPath, options = {}) {
40
+ return `#!/usr/bin/env bash
41
+ set -eu
42
+ echo "$@" >> "${tmuxLogPath}"
43
+ cmd="$1"
44
+ shift || true
45
+ if [[ "$cmd" == "display-message" ]]; then
46
+ fmt=""
47
+ while [[ "$#" -gt 0 ]]; do
48
+ case "$1" in
49
+ -p) ;;
50
+ -t) shift ;;
51
+ *) fmt="$1" ;;
52
+ esac
53
+ shift || true
54
+ done
55
+ case "$fmt" in
56
+ "#{pane_in_mode}") echo "0" ;;
57
+ "#{pane_id}") echo "%42" ;;
58
+ "#{pane_current_path}") pwd ;;
59
+ "#{pane_start_command}") echo "codex" ;;
60
+ "#{pane_current_command}") echo "codex" ;;
61
+ "#S") echo "omx-team-worker-stop" ;;
62
+ *) ;;
63
+ esac
64
+ exit 0
65
+ fi
66
+ if [[ "$cmd" == "capture-pane" ]]; then
67
+ echo "› ready"
68
+ exit 0
69
+ fi
70
+ if [[ "$cmd" == "send-keys" ]]; then
71
+ ${options.failSend ? "exit 1" : "exit 0"}
72
+ fi
73
+ exit 0
74
+ `;
75
+ }
76
+ async function initTempGitRepo(prefix) {
77
+ const cwd = await mkdtemp(join(tmpdir(), prefix));
78
+ execFileSync("git", ["init"], { cwd, stdio: "ignore" });
79
+ execFileSync("git", ["config", "user.email", "test@example.com"], { cwd, stdio: "ignore" });
80
+ execFileSync("git", ["config", "user.name", "Test User"], { cwd, stdio: "ignore" });
81
+ return cwd;
82
+ }
38
83
  async function writeActiveAutopilotSession(cwd, sessionId) {
39
84
  await writeJson(join(cwd, ".omx", "state", "session.json"), {
40
85
  session_id: sessionId,
@@ -96,10 +141,12 @@ const TEAM_ENV_KEYS = [
96
141
  "OMX_TEAM_STATE_ROOT",
97
142
  "OMX_TEAM_LEADER_CWD",
98
143
  "OMX_SESSION_ID",
144
+ "SESSION_ID",
99
145
  "OMX_QUESTION_RETURN_PANE",
100
146
  "OMX_LEADER_PANE_ID",
101
147
  "TMUX",
102
148
  "TMUX_PANE",
149
+ "OMX_TMUX_HUD_OWNER",
103
150
  ];
104
151
  const priorTeamEnv = new Map();
105
152
  beforeEach(() => {
@@ -763,6 +810,114 @@ describe("codex native hook dispatch", () => {
763
810
  await rm(cwd, { recursive: true, force: true });
764
811
  }
765
812
  });
813
+ it("warns completion-like prompts when active goal workflows need Codex snapshot reconciliation", async () => {
814
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-goal-warning-"));
815
+ try {
816
+ await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
817
+ version: 1,
818
+ activeGoalId: "G001-demo",
819
+ goals: [{ id: "G001-demo", status: "in_progress", objective: "Demo goal" }],
820
+ });
821
+ const result = await dispatchCodexNativeHook({
822
+ hook_event_name: "UserPromptSubmit",
823
+ cwd,
824
+ session_id: "sess-goal-warning",
825
+ thread_id: "thread-goal-warning",
826
+ prompt: "complete this goal now",
827
+ }, { cwd });
828
+ assert.match(JSON.stringify(result.outputJson), /requires Codex goal snapshot reconciliation/);
829
+ assert.match(JSON.stringify(result.outputJson), /get_goal/);
830
+ assert.match(JSON.stringify(result.outputJson), /--codex-goal-json/);
831
+ }
832
+ finally {
833
+ await rm(cwd, { recursive: true, force: true });
834
+ }
835
+ });
836
+ it("blocks Stop when a completion-like final answer skips active goal snapshot reconciliation", async () => {
837
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-goal-stop-"));
838
+ try {
839
+ await writeJson(join(cwd, ".omx", "goals", "performance", "latency", "state.json"), {
840
+ version: 1,
841
+ workflow: "performance-goal",
842
+ slug: "latency",
843
+ objective: "Reduce latency",
844
+ status: "validation_passed",
845
+ });
846
+ const result = await dispatchCodexNativeHook({
847
+ hook_event_name: "Stop",
848
+ cwd,
849
+ session_id: "sess-goal-stop",
850
+ thread_id: "thread-goal-stop",
851
+ last_assistant_message: "Performance goal complete; next call update_goal({status: \"complete\"}).",
852
+ }, { cwd });
853
+ assert.equal(result.outputJson?.decision, "block");
854
+ assert.match(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
855
+ assert.match(JSON.stringify(result.outputJson), /omx performance-goal complete --slug latency/);
856
+ assert.match(JSON.stringify(result.outputJson), /Hooks must not mutate Codex goal state/);
857
+ }
858
+ finally {
859
+ await rm(cwd, { recursive: true, force: true });
860
+ }
861
+ });
862
+ it("treats workflow keywords in native subagent prompt text as literal delegation text", async () => {
863
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-keyword-literal-"));
864
+ try {
865
+ const stateDir = join(cwd, ".omx", "state");
866
+ const canonicalSessionId = "sess-parent";
867
+ const leaderNativeSessionId = "native-parent-thread";
868
+ const childNativeSessionId = "native-child-thread";
869
+ const nowIso = new Date().toISOString();
870
+ await writeJson(join(stateDir, "session.json"), {
871
+ session_id: canonicalSessionId,
872
+ native_session_id: leaderNativeSessionId,
873
+ });
874
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
875
+ schemaVersion: 1,
876
+ sessions: {
877
+ [canonicalSessionId]: {
878
+ session_id: canonicalSessionId,
879
+ leader_thread_id: leaderNativeSessionId,
880
+ updated_at: nowIso,
881
+ threads: {
882
+ [leaderNativeSessionId]: {
883
+ thread_id: leaderNativeSessionId,
884
+ kind: "leader",
885
+ first_seen_at: nowIso,
886
+ last_seen_at: nowIso,
887
+ turn_count: 1,
888
+ },
889
+ [childNativeSessionId]: {
890
+ thread_id: childNativeSessionId,
891
+ kind: "subagent",
892
+ first_seen_at: nowIso,
893
+ last_seen_at: nowIso,
894
+ turn_count: 1,
895
+ mode: "architect",
896
+ },
897
+ },
898
+ },
899
+ },
900
+ });
901
+ const result = await dispatchCodexNativeHook({
902
+ hook_event_name: "UserPromptSubmit",
903
+ cwd,
904
+ session_id: childNativeSessionId,
905
+ thread_id: childNativeSessionId,
906
+ turn_id: "turn-child-1",
907
+ prompt: "$ralplan Architect review step. Review the draft plan and return APPROVE or ITERATE.",
908
+ }, { cwd });
909
+ assert.equal(result.omxEventName, "keyword-detector");
910
+ assert.equal(result.skillState, null);
911
+ assert.equal(result.outputJson, null);
912
+ assert.equal(existsSync(join(stateDir, "skill-active-state.json")), false);
913
+ assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "skill-active-state.json")), false);
914
+ assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "ralplan-state.json")), false);
915
+ assert.equal(existsSync(join(stateDir, "sessions", childNativeSessionId, "ralplan-state.json")), false);
916
+ }
917
+ finally {
918
+ await rm(cwd, { recursive: true, force: true });
919
+ }
920
+ });
766
921
  it("records plugin-prefixed keyword activation from UserPromptSubmit payloads", async () => {
767
922
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-plugin-prefixed-"));
768
923
  try {
@@ -786,6 +941,33 @@ describe("codex native hook dispatch", () => {
786
941
  await rm(cwd, { recursive: true, force: true });
787
942
  }
788
943
  });
944
+ it("records ultragoal prompt skill activation with goal-tool handoff guidance", async () => {
945
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-"));
946
+ try {
947
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
948
+ const result = await dispatchCodexNativeHook({
949
+ hook_event_name: "UserPromptSubmit",
950
+ cwd,
951
+ session_id: "sess-ultragoal-1",
952
+ thread_id: "thread-ultragoal-1",
953
+ turn_id: "turn-ultragoal-1",
954
+ prompt: "$ultragoal split this launch into durable goals",
955
+ }, { cwd });
956
+ assert.equal(result.omxEventName, "keyword-detector");
957
+ assert.equal(result.skillState?.skill, "ultragoal");
958
+ assert.equal(result.skillState?.initialized_mode, undefined);
959
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
960
+ assert.match(message, /"\$ultragoal" -> ultragoal/);
961
+ assert.match(message, /Ultragoal protocol:/);
962
+ assert.match(message, /get_goal/);
963
+ assert.match(message, /create_goal/);
964
+ assert.match(message, /update_goal/);
965
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ultragoal-1", "ultragoal-state.json")), false);
966
+ }
967
+ finally {
968
+ await rm(cwd, { recursive: true, force: true });
969
+ }
970
+ });
789
971
  it("normalizes the Korean keyboard typo for ulw during UserPromptSubmit activation", async () => {
790
972
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ulw-ko-"));
791
973
  try {
@@ -1505,10 +1687,12 @@ export async function onHookEvent(event) {
1505
1687
  const originalTmux = process.env.TMUX;
1506
1688
  const originalTmuxPane = process.env.TMUX_PANE;
1507
1689
  const originalPath = process.env.PATH;
1690
+ const originalHudOwner = process.env[OMX_TMUX_HUD_OWNER_ENV];
1508
1691
  const originalArgv = process.argv;
1509
1692
  try {
1510
1693
  process.env.TMUX = "1";
1511
1694
  process.env.TMUX_PANE = "%1";
1695
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = "1";
1512
1696
  await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1513
1697
  await writeFile(join(cwd, ".omx", "hud-config.json"), JSON.stringify({ preset: "focused", git: { display: "branch" } }, null, 2));
1514
1698
  const binDir = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-reconcile-bin-"));
@@ -1560,11 +1744,61 @@ esac
1560
1744
  else {
1561
1745
  process.env.TMUX_PANE = originalTmuxPane;
1562
1746
  }
1747
+ if (originalHudOwner === undefined) {
1748
+ delete process.env[OMX_TMUX_HUD_OWNER_ENV];
1749
+ }
1750
+ else {
1751
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = originalHudOwner;
1752
+ }
1563
1753
  process.env.PATH = originalPath;
1564
1754
  process.argv = originalArgv;
1565
1755
  await rm(cwd, { recursive: true, force: true });
1566
1756
  }
1567
1757
  });
1758
+ it("skips prompt-submit HUD reconciliation inside unowned tmux panes", async () => {
1759
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-unowned-"));
1760
+ const originalTmux = process.env.TMUX;
1761
+ const originalTmuxPane = process.env.TMUX_PANE;
1762
+ const originalPath = process.env.PATH;
1763
+ const originalHudOwner = process.env[OMX_TMUX_HUD_OWNER_ENV];
1764
+ try {
1765
+ process.env.TMUX = "1";
1766
+ process.env.TMUX_PANE = "%claude";
1767
+ delete process.env[OMX_TMUX_HUD_OWNER_ENV];
1768
+ const binDir = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-unowned-bin-"));
1769
+ const tmuxLog = join(cwd, "tmux.log");
1770
+ await writeFile(join(binDir, "tmux"), `#!/usr/bin/env bash
1771
+ printf '%s\n' "$*" >> ${JSON.stringify(tmuxLog)}
1772
+ exit 0
1773
+ `);
1774
+ await chmod(join(binDir, "tmux"), 0o755);
1775
+ process.env.PATH = `${binDir}:${originalPath}`;
1776
+ const result = await dispatchCodexNativeHook({
1777
+ hook_event_name: "UserPromptSubmit",
1778
+ cwd,
1779
+ session_id: "sess-hud-unowned",
1780
+ prompt: "$ralplan prepare plan",
1781
+ }, { cwd });
1782
+ assert.equal(result.omxEventName, "keyword-detector");
1783
+ assert.equal(existsSync(tmuxLog), false);
1784
+ }
1785
+ finally {
1786
+ if (originalTmux === undefined)
1787
+ delete process.env.TMUX;
1788
+ else
1789
+ process.env.TMUX = originalTmux;
1790
+ if (originalTmuxPane === undefined)
1791
+ delete process.env.TMUX_PANE;
1792
+ else
1793
+ process.env.TMUX_PANE = originalTmuxPane;
1794
+ if (originalHudOwner === undefined)
1795
+ delete process.env[OMX_TMUX_HUD_OWNER_ENV];
1796
+ else
1797
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = originalHudOwner;
1798
+ process.env.PATH = originalPath;
1799
+ await rm(cwd, { recursive: true, force: true });
1800
+ }
1801
+ });
1568
1802
  it("blocks Bash omx question when no leader-pane return hint is preserved", async () => {
1569
1803
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-enforce-"));
1570
1804
  try {
@@ -1933,48 +2167,45 @@ esac
1933
2167
  await rm(cwd, { recursive: true, force: true });
1934
2168
  }
1935
2169
  });
1936
- it("blocks PreToolUse git commit with supported response shape when the inline message is not Lore-compliant", async () => {
1937
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-invalid-"));
2170
+ it("warns on PreToolUse for vague sloppy fallback implementation framing", async () => {
2171
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-warn-"));
1938
2172
  try {
1939
2173
  const result = await dispatchCodexNativeHook({
1940
2174
  hook_event_name: "PreToolUse",
1941
2175
  cwd,
1942
2176
  tool_name: "Bash",
1943
- tool_use_id: "tool-git-commit-invalid",
1944
- tool_input: { command: 'git commit -m "fix tests"' },
2177
+ tool_use_id: "tool-slop-warn",
2178
+ tool_input: {
2179
+ command: [
2180
+ "cat > src/runtime.ts <<'EOF'",
2181
+ "export function loadRuntime() {",
2182
+ " // implement a quick hack fallback if it fails",
2183
+ " return process.env.RUNTIME || 'local';",
2184
+ "}",
2185
+ "EOF",
2186
+ ].join("\n"),
2187
+ },
1945
2188
  }, { cwd });
1946
2189
  assert.equal(result.omxEventName, "pre-tool-use");
1947
- assert.deepEqual(result.outputJson, {
1948
- decision: "block",
1949
- reason: "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
1950
- hookSpecificOutput: {
1951
- hookEventName: "PreToolUse",
1952
- },
1953
- systemMessage: [
1954
- "git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
1955
- "- Add a blank line after the subject before the narrative body.",
1956
- "- Add a narrative body paragraph explaining the decision context.",
1957
- "- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
1958
- "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
1959
- ].join("\n"),
1960
- });
1961
- const hookSpecificOutput = result.outputJson
1962
- .hookSpecificOutput ?? {};
1963
- assert.equal("additionalContext" in hookSpecificOutput, false);
2190
+ assert.equal(result.outputJson?.decision, undefined);
2191
+ assert.equal(result.outputJson?.hookSpecificOutput?.hookEventName, "PreToolUse");
2192
+ assert.match(JSON.stringify(result.outputJson), /don't make potential slop/);
2193
+ assert.match(JSON.stringify(result.outputJson), /architect/);
2194
+ assert.match(JSON.stringify(result.outputJson), /environment issue/);
1964
2195
  }
1965
2196
  finally {
1966
2197
  await rm(cwd, { recursive: true, force: true });
1967
2198
  }
1968
2199
  });
1969
- it("stays silent on PreToolUse for `git help commit`", async () => {
1970
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-help-commit-"));
2200
+ it("does not warn on PreToolUse for read-only fallback text inspection", async () => {
2201
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-readonly-"));
1971
2202
  try {
1972
2203
  const result = await dispatchCodexNativeHook({
1973
2204
  hook_event_name: "PreToolUse",
1974
2205
  cwd,
1975
2206
  tool_name: "Bash",
1976
- tool_use_id: "tool-git-help-commit",
1977
- tool_input: { command: "git help commit" },
2207
+ tool_use_id: "tool-slop-readonly",
2208
+ tool_input: { command: "rg \"quick hack fallback if it fails\" src docs" },
1978
2209
  }, { cwd });
1979
2210
  assert.equal(result.omxEventName, "pre-tool-use");
1980
2211
  assert.equal(result.outputJson, null);
@@ -1983,32 +2214,53 @@ esac
1983
2214
  await rm(cwd, { recursive: true, force: true });
1984
2215
  }
1985
2216
  });
1986
- it("stays silent on PreToolUse for `git config alias.ci commit`", async () => {
1987
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-config-alias-commit-"));
2217
+ it("warns when a read-only command is chained before sloppy fallback writes", async () => {
2218
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-chained-write-"));
1988
2219
  try {
1989
2220
  const result = await dispatchCodexNativeHook({
1990
2221
  hook_event_name: "PreToolUse",
1991
2222
  cwd,
1992
2223
  tool_name: "Bash",
1993
- tool_use_id: "tool-git-config-alias-commit",
1994
- tool_input: { command: "git config alias.ci commit" },
2224
+ tool_use_id: "tool-slop-chained-write",
2225
+ tool_input: {
2226
+ command: [
2227
+ "rg foo src && cat > src/runtime.ts <<EOF",
2228
+ "export function loadRuntime() {",
2229
+ " // implement quick hack fallback if it fails",
2230
+ " return 'local';",
2231
+ "}",
2232
+ "EOF",
2233
+ ].join("\n"),
2234
+ },
1995
2235
  }, { cwd });
1996
2236
  assert.equal(result.omxEventName, "pre-tool-use");
1997
- assert.equal(result.outputJson, null);
2237
+ assert.equal(result.outputJson?.decision, undefined);
2238
+ assert.equal(result.outputJson?.hookSpecificOutput?.hookEventName, "PreToolUse");
2239
+ assert.match(JSON.stringify(result.outputJson), /don't make potential slop/);
1998
2240
  }
1999
2241
  finally {
2000
2242
  await rm(cwd, { recursive: true, force: true });
2001
2243
  }
2002
2244
  });
2003
- it("stays silent on PreToolUse for `git tag commit`", async () => {
2004
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-tag-commit-"));
2245
+ it("does not warn on PreToolUse for grounded compatibility fallback code", async () => {
2246
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-grounded-"));
2005
2247
  try {
2006
2248
  const result = await dispatchCodexNativeHook({
2007
2249
  hook_event_name: "PreToolUse",
2008
2250
  cwd,
2009
2251
  tool_name: "Bash",
2010
- tool_use_id: "tool-git-tag-commit",
2011
- tool_input: { command: "git tag commit" },
2252
+ tool_use_id: "tool-slop-grounded",
2253
+ tool_input: {
2254
+ command: [
2255
+ "cat > src/compat.ts <<'EOF'",
2256
+ "export function resolveCompatMode() {",
2257
+ " // temporary fallback because legacy compatibility needs fail-safe startup behavior",
2258
+ " return 'legacy';",
2259
+ "}",
2260
+ "// Tested: npm test",
2261
+ "EOF",
2262
+ ].join("\n"),
2263
+ },
2012
2264
  }, { cwd });
2013
2265
  assert.equal(result.omxEventName, "pre-tool-use");
2014
2266
  assert.equal(result.outputJson, null);
@@ -2017,15 +2269,214 @@ esac
2017
2269
  await rm(cwd, { recursive: true, force: true });
2018
2270
  }
2019
2271
  });
2020
- it("blocks PreToolUse env-prefixed git commit when the inline message is not Lore-compliant", async () => {
2021
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-env-invalid-"));
2272
+ it("blocks Stop for untracked non-Bash-style sloppy fallback source edits", async () => {
2273
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-untracked-");
2274
+ try {
2275
+ await mkdir(join(cwd, "src"), { recursive: true });
2276
+ await writeFile(join(cwd, "src", "runtime.ts"), [
2277
+ "export function loadRuntime() {",
2278
+ " // implement a quick hack fallback if it fails",
2279
+ " return process.env.RUNTIME || 'local';",
2280
+ "}",
2281
+ ].join("\n"));
2282
+ const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-untracked" }, { cwd });
2283
+ assert.equal(result.omxEventName, "stop");
2284
+ assert.equal(result.outputJson?.decision, "block");
2285
+ assert.equal(result.outputJson?.stopReason, "sloppy_fallback_diff_audit");
2286
+ assert.match(JSON.stringify(result.outputJson), /src\/runtime\.ts/);
2287
+ assert.match(JSON.stringify(result.outputJson), /grounded design/);
2288
+ }
2289
+ finally {
2290
+ await rm(cwd, { recursive: true, force: true });
2291
+ }
2292
+ });
2293
+ it("keeps blocking repeated Stop while sloppy fallback diff remains", async () => {
2294
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-repeat-");
2295
+ try {
2296
+ await mkdir(join(cwd, "src"), { recursive: true });
2297
+ await writeFile(join(cwd, "src", "runtime.ts"), [
2298
+ "export function loadRuntime() {",
2299
+ " // implement a quick hack fallback if it fails",
2300
+ " return process.env.RUNTIME || 'local';",
2301
+ "}",
2302
+ ].join("\n"));
2303
+ const payload = { hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-repeat", turn_id: "turn-repeat" };
2304
+ const first = await dispatchCodexNativeHook(payload, { cwd });
2305
+ const repeated = await dispatchCodexNativeHook({ ...payload, stop_hook_active: true }, { cwd });
2306
+ assert.equal(first.outputJson?.decision, "block");
2307
+ assert.equal(repeated.outputJson?.decision, "block");
2308
+ assert.equal(repeated.outputJson?.stopReason, "sloppy_fallback_diff_audit");
2309
+ }
2310
+ finally {
2311
+ await rm(cwd, { recursive: true, force: true });
2312
+ }
2313
+ });
2314
+ it("blocks Stop for unstaged tracked sloppy fallback source edits", async () => {
2315
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-unstaged-");
2316
+ try {
2317
+ await mkdir(join(cwd, "src"), { recursive: true });
2318
+ await writeFile(join(cwd, "src", "runtime.ts"), "export const runtime = 'base';\n");
2319
+ execFileSync("git", ["add", "src/runtime.ts"], { cwd, stdio: "ignore" });
2320
+ execFileSync("git", ["commit", "-m", "initial"], { cwd, stdio: "ignore" });
2321
+ await writeFile(join(cwd, "src", "runtime.ts"), [
2322
+ "export function loadRuntime() {",
2323
+ " // just bypass fallback if it fails",
2324
+ " return process.env.RUNTIME || 'local';",
2325
+ "}",
2326
+ ].join("\n"));
2327
+ const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-unstaged" }, { cwd });
2328
+ assert.equal(result.omxEventName, "stop");
2329
+ assert.equal(result.outputJson?.decision, "block");
2330
+ assert.match(JSON.stringify(result.outputJson), /unstaged/);
2331
+ }
2332
+ finally {
2333
+ await rm(cwd, { recursive: true, force: true });
2334
+ }
2335
+ });
2336
+ it("blocks Stop from a subdirectory cwd for untracked sloppy source elsewhere", async () => {
2337
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-subdir-");
2338
+ try {
2339
+ await mkdir(join(cwd, "src", "nested"), { recursive: true });
2340
+ await writeFile(join(cwd, "src", "nested", "anchor.ts"), "export const anchor = true;\n");
2341
+ await writeFile(join(cwd, "src", "runtime.ts"), [
2342
+ "export function loadRuntime() {",
2343
+ " // implement a quick hack fallback if it fails",
2344
+ " return process.env.RUNTIME || 'local';",
2345
+ "}",
2346
+ ].join("\n"));
2347
+ const subdir = join(cwd, "src", "nested");
2348
+ const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd: subdir, session_id: "sess-stop-slop-subdir" }, { cwd: subdir });
2349
+ assert.equal(result.omxEventName, "stop");
2350
+ assert.equal(result.outputJson?.decision, "block");
2351
+ assert.match(JSON.stringify(result.outputJson), /src\/runtime\.ts/);
2352
+ }
2353
+ finally {
2354
+ await rm(cwd, { recursive: true, force: true });
2355
+ }
2356
+ });
2357
+ it("blocks Stop for staged sloppy fallback source edits", async () => {
2358
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-staged-");
2359
+ try {
2360
+ await mkdir(join(cwd, "src"), { recursive: true });
2361
+ await writeFile(join(cwd, "src", "runtime.ts"), "export const runtime = 'base';\n");
2362
+ execFileSync("git", ["add", "src/runtime.ts"], { cwd, stdio: "ignore" });
2363
+ execFileSync("git", ["commit", "-m", "initial"], { cwd, stdio: "ignore" });
2364
+ await writeFile(join(cwd, "src", "runtime.ts"), [
2365
+ "export function loadRuntime() {",
2366
+ " // temporary workaround fallback if it fails",
2367
+ " return process.env.RUNTIME || 'local';",
2368
+ "}",
2369
+ ].join("\n"));
2370
+ execFileSync("git", ["add", "src/runtime.ts"], { cwd, stdio: "ignore" });
2371
+ const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-staged" }, { cwd });
2372
+ assert.equal(result.omxEventName, "stop");
2373
+ assert.equal(result.outputJson?.decision, "block");
2374
+ assert.match(JSON.stringify(result.outputJson), /staged/);
2375
+ }
2376
+ finally {
2377
+ await rm(cwd, { recursive: true, force: true });
2378
+ }
2379
+ });
2380
+ it("does not block Stop for grounded compatibility fallback source edits", async () => {
2381
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-grounded-");
2382
+ try {
2383
+ await mkdir(join(cwd, "src"), { recursive: true });
2384
+ await writeFile(join(cwd, "src", "compat.ts"), [
2385
+ "export function resolveCompatMode() {",
2386
+ " // temporary fallback for legacy startup",
2387
+ " // compatibility fail-safe tested by regression coverage",
2388
+ " return 'legacy';",
2389
+ "}",
2390
+ ].join("\n"));
2391
+ const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-grounded" }, { cwd });
2392
+ assert.equal(result.omxEventName, "stop");
2393
+ assert.equal(result.outputJson, null);
2394
+ }
2395
+ finally {
2396
+ await rm(cwd, { recursive: true, force: true });
2397
+ }
2398
+ });
2399
+ it("does not block Stop when existing nearby source context grounds a new fallback line", async () => {
2400
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-existing-ground-");
2401
+ try {
2402
+ await mkdir(join(cwd, "src"), { recursive: true });
2403
+ await writeFile(join(cwd, "src", "compat.ts"), [
2404
+ "export function resolveCompatMode() {",
2405
+ " // compatibility fail-safe tested by regression coverage",
2406
+ " return 'legacy';",
2407
+ "}",
2408
+ ].join("\n"));
2409
+ execFileSync("git", ["add", "src/compat.ts"], { cwd, stdio: "ignore" });
2410
+ execFileSync("git", ["commit", "-m", "initial"], { cwd, stdio: "ignore" });
2411
+ await writeFile(join(cwd, "src", "compat.ts"), [
2412
+ "export function resolveCompatMode() {",
2413
+ " // compatibility fail-safe tested by regression coverage",
2414
+ " // temporary fallback if it fails",
2415
+ " return 'legacy';",
2416
+ "}",
2417
+ ].join("\n"));
2418
+ const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-existing-ground" }, { cwd });
2419
+ assert.equal(result.omxEventName, "stop");
2420
+ assert.equal(result.outputJson, null);
2421
+ }
2422
+ finally {
2423
+ await rm(cwd, { recursive: true, force: true });
2424
+ }
2425
+ });
2426
+ it("does not block Stop for source-adjacent test file fallback wording", async () => {
2427
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-test-file-");
2428
+ try {
2429
+ await mkdir(join(cwd, "src"), { recursive: true });
2430
+ await writeFile(join(cwd, "src", "runtime.test.ts"), "it('documents no quick hack fallback if it fails', () => {});\n");
2431
+ const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-test-file" }, { cwd });
2432
+ assert.equal(result.omxEventName, "stop");
2433
+ assert.equal(result.outputJson, null);
2434
+ }
2435
+ finally {
2436
+ await rm(cwd, { recursive: true, force: true });
2437
+ }
2438
+ });
2439
+ it("does not block Stop for docs-only fallback wording", async () => {
2440
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-docs-");
2441
+ try {
2442
+ await mkdir(join(cwd, "docs"), { recursive: true });
2443
+ await writeFile(join(cwd, "docs", "notes.md"), "Do not implement a quick hack fallback if it fails.\n");
2444
+ const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-docs" }, { cwd });
2445
+ assert.equal(result.omxEventName, "stop");
2446
+ assert.equal(result.outputJson, null);
2447
+ }
2448
+ finally {
2449
+ await rm(cwd, { recursive: true, force: true });
2450
+ }
2451
+ });
2452
+ it("keeps git commit Lore enforcement ahead of sloppy fallback advisory", async () => {
2453
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-git-priority-"));
2022
2454
  try {
2023
2455
  const result = await dispatchCodexNativeHook({
2024
2456
  hook_event_name: "PreToolUse",
2025
2457
  cwd,
2026
2458
  tool_name: "Bash",
2027
- tool_use_id: "tool-git-commit-env-invalid",
2028
- tool_input: { command: 'HUSKY=0 git commit -m "fix tests"' },
2459
+ tool_use_id: "tool-slop-git-priority",
2460
+ tool_input: { command: 'git commit -m "quick hack fallback if it fails"' },
2461
+ }, { cwd });
2462
+ assert.equal(result.omxEventName, "pre-tool-use");
2463
+ assert.equal(result.outputJson?.decision, "block");
2464
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
2465
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /don't make potential slop/);
2466
+ }
2467
+ finally {
2468
+ await rm(cwd, { recursive: true, force: true });
2469
+ }
2470
+ });
2471
+ it("blocks PreToolUse git commit with supported response shape when the inline message is not Lore-compliant", async () => {
2472
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-invalid-"));
2473
+ try {
2474
+ const result = await dispatchCodexNativeHook({
2475
+ hook_event_name: "PreToolUse",
2476
+ cwd,
2477
+ tool_name: "Bash",
2478
+ tool_use_id: "tool-git-commit-invalid",
2479
+ tool_input: { command: 'git commit -m "fix tests"' },
2029
2480
  }, { cwd });
2030
2481
  assert.equal(result.omxEventName, "pre-tool-use");
2031
2482
  assert.deepEqual(result.outputJson, {
@@ -2042,13 +2493,292 @@ esac
2042
2493
  "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
2043
2494
  ].join("\n"),
2044
2495
  });
2496
+ const hookSpecificOutput = result.outputJson
2497
+ .hookSpecificOutput ?? {};
2498
+ assert.equal("additionalContext" in hookSpecificOutput, false);
2045
2499
  }
2046
2500
  finally {
2047
2501
  await rm(cwd, { recursive: true, force: true });
2048
2502
  }
2049
2503
  });
2050
- it("blocks PreToolUse git commit when git options appear before the real commit subcommand", async () => {
2051
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-option-invalid-"));
2504
+ it("allows non-Lore git commit messages when the Lore commit guard is explicitly disabled", async () => {
2505
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-disabled-"));
2506
+ const original = process.env.OMX_LORE_COMMIT_GUARD;
2507
+ try {
2508
+ process.env.OMX_LORE_COMMIT_GUARD = "0";
2509
+ const result = await dispatchCodexNativeHook({
2510
+ hook_event_name: "PreToolUse",
2511
+ cwd,
2512
+ tool_name: "Bash",
2513
+ tool_use_id: "tool-git-commit-lore-disabled",
2514
+ tool_input: { command: 'git commit -m "fix: use conventional commit"' },
2515
+ }, { cwd });
2516
+ assert.equal(result.omxEventName, "pre-tool-use");
2517
+ assert.equal(result.outputJson, null);
2518
+ }
2519
+ finally {
2520
+ if (original === undefined)
2521
+ delete process.env.OMX_LORE_COMMIT_GUARD;
2522
+ else
2523
+ process.env.OMX_LORE_COMMIT_GUARD = original;
2524
+ await rm(cwd, { recursive: true, force: true });
2525
+ }
2526
+ });
2527
+ it("allows non-Lore git commit messages when the Lore commit guard is disabled inline", async () => {
2528
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-inline-disabled-"));
2529
+ try {
2530
+ const result = await dispatchCodexNativeHook({
2531
+ hook_event_name: "PreToolUse",
2532
+ cwd,
2533
+ tool_name: "Bash",
2534
+ tool_use_id: "tool-git-commit-lore-inline-disabled",
2535
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=0 git commit -m "fix: conventional"' },
2536
+ }, { cwd });
2537
+ assert.equal(result.omxEventName, "pre-tool-use");
2538
+ assert.equal(result.outputJson, null);
2539
+ }
2540
+ finally {
2541
+ await rm(cwd, { recursive: true, force: true });
2542
+ }
2543
+ });
2544
+ it("does not treat newline-separated Lore guard assignment as inline git commit env", async () => {
2545
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-newline-assignment-"));
2546
+ try {
2547
+ const result = await dispatchCodexNativeHook({
2548
+ hook_event_name: "PreToolUse",
2549
+ cwd,
2550
+ tool_name: "Bash",
2551
+ tool_use_id: "tool-git-commit-lore-newline-assignment",
2552
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=0\ngit commit -m "fix: conventional"' },
2553
+ }, { cwd });
2554
+ assert.equal(result.omxEventName, "pre-tool-use");
2555
+ assert.equal(result.outputJson?.decision, "block");
2556
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
2557
+ }
2558
+ finally {
2559
+ await rm(cwd, { recursive: true, force: true });
2560
+ }
2561
+ });
2562
+ it("restores default-on Lore guard when env -u unsets a disabled process env", async () => {
2563
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-env-unset-"));
2564
+ const original = process.env.OMX_LORE_COMMIT_GUARD;
2565
+ try {
2566
+ process.env.OMX_LORE_COMMIT_GUARD = "0";
2567
+ const result = await dispatchCodexNativeHook({
2568
+ hook_event_name: "PreToolUse",
2569
+ cwd,
2570
+ tool_name: "Bash",
2571
+ tool_use_id: "tool-git-commit-lore-env-unset",
2572
+ tool_input: { command: 'env -u OMX_LORE_COMMIT_GUARD git commit -m "fix: conventional"' },
2573
+ }, { cwd });
2574
+ assert.equal(result.omxEventName, "pre-tool-use");
2575
+ assert.equal(result.outputJson?.decision, "block");
2576
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
2577
+ }
2578
+ finally {
2579
+ if (original === undefined)
2580
+ delete process.env.OMX_LORE_COMMIT_GUARD;
2581
+ else
2582
+ process.env.OMX_LORE_COMMIT_GUARD = original;
2583
+ await rm(cwd, { recursive: true, force: true });
2584
+ }
2585
+ });
2586
+ it("restores default-on Lore guard when env -i clears a disabled process env", async () => {
2587
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-env-ignore-"));
2588
+ const original = process.env.OMX_LORE_COMMIT_GUARD;
2589
+ try {
2590
+ process.env.OMX_LORE_COMMIT_GUARD = "0";
2591
+ const result = await dispatchCodexNativeHook({
2592
+ hook_event_name: "PreToolUse",
2593
+ cwd,
2594
+ tool_name: "Bash",
2595
+ tool_use_id: "tool-git-commit-lore-env-ignore",
2596
+ tool_input: { command: 'env -i PATH=/usr/bin git commit -m "fix: conventional"' },
2597
+ }, { cwd });
2598
+ assert.equal(result.omxEventName, "pre-tool-use");
2599
+ assert.equal(result.outputJson?.decision, "block");
2600
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
2601
+ }
2602
+ finally {
2603
+ if (original === undefined)
2604
+ delete process.env.OMX_LORE_COMMIT_GUARD;
2605
+ else
2606
+ process.env.OMX_LORE_COMMIT_GUARD = original;
2607
+ await rm(cwd, { recursive: true, force: true });
2608
+ }
2609
+ });
2610
+ it("keeps Lore commit enforcement enabled for unknown inline guard values", async () => {
2611
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-inline-unknown-"));
2612
+ try {
2613
+ const result = await dispatchCodexNativeHook({
2614
+ hook_event_name: "PreToolUse",
2615
+ cwd,
2616
+ tool_name: "Bash",
2617
+ tool_use_id: "tool-git-commit-lore-inline-unknown",
2618
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=maybe git commit -m "fix: conventional"' },
2619
+ }, { cwd });
2620
+ assert.equal(result.omxEventName, "pre-tool-use");
2621
+ assert.equal(result.outputJson?.decision, "block");
2622
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
2623
+ }
2624
+ finally {
2625
+ await rm(cwd, { recursive: true, force: true });
2626
+ }
2627
+ });
2628
+ it("treats Lore commit guard disabled values as trim and case tolerant", async () => {
2629
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-off-"));
2630
+ const original = process.env.OMX_LORE_COMMIT_GUARD;
2631
+ try {
2632
+ process.env.OMX_LORE_COMMIT_GUARD = " OFF ";
2633
+ const result = await dispatchCodexNativeHook({
2634
+ hook_event_name: "PreToolUse",
2635
+ cwd,
2636
+ tool_name: "Bash",
2637
+ tool_use_id: "tool-git-commit-lore-off",
2638
+ tool_input: { command: 'git commit -m "chore: conventional commit"' },
2639
+ }, { cwd });
2640
+ assert.equal(result.omxEventName, "pre-tool-use");
2641
+ assert.equal(result.outputJson, null);
2642
+ }
2643
+ finally {
2644
+ if (original === undefined)
2645
+ delete process.env.OMX_LORE_COMMIT_GUARD;
2646
+ else
2647
+ process.env.OMX_LORE_COMMIT_GUARD = original;
2648
+ await rm(cwd, { recursive: true, force: true });
2649
+ }
2650
+ });
2651
+ it("keeps Lore commit enforcement enabled for unknown guard values", async () => {
2652
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-unknown-"));
2653
+ const original = process.env.OMX_LORE_COMMIT_GUARD;
2654
+ try {
2655
+ process.env.OMX_LORE_COMMIT_GUARD = "maybe";
2656
+ const result = await dispatchCodexNativeHook({
2657
+ hook_event_name: "PreToolUse",
2658
+ cwd,
2659
+ tool_name: "Bash",
2660
+ tool_use_id: "tool-git-commit-lore-unknown",
2661
+ tool_input: { command: 'git commit -m "fix tests"' },
2662
+ }, { cwd });
2663
+ assert.equal(result.omxEventName, "pre-tool-use");
2664
+ assert.equal(result.outputJson?.decision, "block");
2665
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
2666
+ }
2667
+ finally {
2668
+ if (original === undefined)
2669
+ delete process.env.OMX_LORE_COMMIT_GUARD;
2670
+ else
2671
+ process.env.OMX_LORE_COMMIT_GUARD = original;
2672
+ await rm(cwd, { recursive: true, force: true });
2673
+ }
2674
+ });
2675
+ it("continues to later PreToolUse checks when Lore commit guard is disabled", async () => {
2676
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-lore-disabled-destructive-"));
2677
+ const original = process.env.OMX_LORE_COMMIT_GUARD;
2678
+ try {
2679
+ process.env.OMX_LORE_COMMIT_GUARD = "false";
2680
+ const result = await dispatchCodexNativeHook({
2681
+ hook_event_name: "PreToolUse",
2682
+ cwd,
2683
+ tool_name: "Bash",
2684
+ tool_use_id: "tool-lore-disabled-destructive",
2685
+ tool_input: { command: "rm -rf dist" },
2686
+ }, { cwd });
2687
+ assert.equal(result.omxEventName, "pre-tool-use");
2688
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /Lore protocol/);
2689
+ assert.match(JSON.stringify(result.outputJson), /Destructive Bash command detected/);
2690
+ }
2691
+ finally {
2692
+ if (original === undefined)
2693
+ delete process.env.OMX_LORE_COMMIT_GUARD;
2694
+ else
2695
+ process.env.OMX_LORE_COMMIT_GUARD = original;
2696
+ await rm(cwd, { recursive: true, force: true });
2697
+ }
2698
+ });
2699
+ it("stays silent on PreToolUse for `git help commit`", async () => {
2700
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-help-commit-"));
2701
+ try {
2702
+ const result = await dispatchCodexNativeHook({
2703
+ hook_event_name: "PreToolUse",
2704
+ cwd,
2705
+ tool_name: "Bash",
2706
+ tool_use_id: "tool-git-help-commit",
2707
+ tool_input: { command: "git help commit" },
2708
+ }, { cwd });
2709
+ assert.equal(result.omxEventName, "pre-tool-use");
2710
+ assert.equal(result.outputJson, null);
2711
+ }
2712
+ finally {
2713
+ await rm(cwd, { recursive: true, force: true });
2714
+ }
2715
+ });
2716
+ it("stays silent on PreToolUse for `git config alias.ci commit`", async () => {
2717
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-config-alias-commit-"));
2718
+ try {
2719
+ const result = await dispatchCodexNativeHook({
2720
+ hook_event_name: "PreToolUse",
2721
+ cwd,
2722
+ tool_name: "Bash",
2723
+ tool_use_id: "tool-git-config-alias-commit",
2724
+ tool_input: { command: "git config alias.ci commit" },
2725
+ }, { cwd });
2726
+ assert.equal(result.omxEventName, "pre-tool-use");
2727
+ assert.equal(result.outputJson, null);
2728
+ }
2729
+ finally {
2730
+ await rm(cwd, { recursive: true, force: true });
2731
+ }
2732
+ });
2733
+ it("stays silent on PreToolUse for `git tag commit`", async () => {
2734
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-tag-commit-"));
2735
+ try {
2736
+ const result = await dispatchCodexNativeHook({
2737
+ hook_event_name: "PreToolUse",
2738
+ cwd,
2739
+ tool_name: "Bash",
2740
+ tool_use_id: "tool-git-tag-commit",
2741
+ tool_input: { command: "git tag commit" },
2742
+ }, { cwd });
2743
+ assert.equal(result.omxEventName, "pre-tool-use");
2744
+ assert.equal(result.outputJson, null);
2745
+ }
2746
+ finally {
2747
+ await rm(cwd, { recursive: true, force: true });
2748
+ }
2749
+ });
2750
+ it("blocks PreToolUse env-prefixed git commit when the inline message is not Lore-compliant", async () => {
2751
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-env-invalid-"));
2752
+ try {
2753
+ const result = await dispatchCodexNativeHook({
2754
+ hook_event_name: "PreToolUse",
2755
+ cwd,
2756
+ tool_name: "Bash",
2757
+ tool_use_id: "tool-git-commit-env-invalid",
2758
+ tool_input: { command: 'HUSKY=0 git commit -m "fix tests"' },
2759
+ }, { cwd });
2760
+ assert.equal(result.omxEventName, "pre-tool-use");
2761
+ assert.deepEqual(result.outputJson, {
2762
+ decision: "block",
2763
+ reason: "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
2764
+ hookSpecificOutput: {
2765
+ hookEventName: "PreToolUse",
2766
+ },
2767
+ systemMessage: [
2768
+ "git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
2769
+ "- Add a blank line after the subject before the narrative body.",
2770
+ "- Add a narrative body paragraph explaining the decision context.",
2771
+ "- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
2772
+ "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
2773
+ ].join("\n"),
2774
+ });
2775
+ }
2776
+ finally {
2777
+ await rm(cwd, { recursive: true, force: true });
2778
+ }
2779
+ });
2780
+ it("blocks PreToolUse git commit when git options appear before the real commit subcommand", async () => {
2781
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-option-invalid-"));
2052
2782
  try {
2053
2783
  const result = await dispatchCodexNativeHook({
2054
2784
  hook_event_name: "PreToolUse",
@@ -2956,12 +3686,44 @@ esac
2956
3686
  await rm(cwd, { recursive: true, force: true });
2957
3687
  }
2958
3688
  });
3689
+ for (const rootActiveCase of [
3690
+ { mode: "autopilot", phase: "execution" },
3691
+ { mode: "ultrawork", phase: "executing" },
3692
+ { mode: "ultraqa", phase: "diagnose" },
3693
+ ]) {
3694
+ it(`returns Stop continuation output from root ${rootActiveCase.mode} state when no session is active`, async () => {
3695
+ const cwd = await mkdtemp(join(tmpdir(), `omx-native-hook-stop-root-${rootActiveCase.mode}-`));
3696
+ try {
3697
+ const stateDir = join(cwd, ".omx", "state");
3698
+ await mkdir(stateDir, { recursive: true });
3699
+ await writeJson(join(stateDir, `${rootActiveCase.mode}-state.json`), {
3700
+ active: true,
3701
+ mode: rootActiveCase.mode,
3702
+ current_phase: rootActiveCase.phase,
3703
+ });
3704
+ const result = await dispatchCodexNativeHook({
3705
+ hook_event_name: "Stop",
3706
+ cwd,
3707
+ }, { cwd });
3708
+ assert.equal(result.omxEventName, "stop");
3709
+ assert.deepEqual(result.outputJson, {
3710
+ decision: "block",
3711
+ reason: `OMX ${rootActiveCase.mode} is still active (phase: ${rootActiveCase.phase}); continue the task and gather fresh verification evidence before stopping.`,
3712
+ stopReason: `${rootActiveCase.mode}_${rootActiveCase.phase}`,
3713
+ systemMessage: `OMX ${rootActiveCase.mode} is still active (phase: ${rootActiveCase.phase}).`,
3714
+ });
3715
+ }
3716
+ finally {
3717
+ await rm(cwd, { recursive: true, force: true });
3718
+ }
3719
+ });
3720
+ }
2959
3721
  it("returns Stop continuation output while Autopilot is active", async () => {
2960
3722
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-"));
2961
3723
  try {
2962
3724
  const stateDir = join(cwd, ".omx", "state");
2963
- await mkdir(stateDir, { recursive: true });
2964
- await writeJson(join(stateDir, "autopilot-state.json"), {
3725
+ await mkdir(join(stateDir, "sessions", "sess-stop-autopilot"), { recursive: true });
3726
+ await writeJson(join(stateDir, "sessions", "sess-stop-autopilot", "autopilot-state.json"), {
2965
3727
  active: true,
2966
3728
  current_phase: "execution",
2967
3729
  });
@@ -2986,8 +3748,8 @@ esac
2986
3748
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-planning-replay-"));
2987
3749
  try {
2988
3750
  const stateDir = join(cwd, ".omx", "state");
2989
- await mkdir(stateDir, { recursive: true });
2990
- await writeJson(join(stateDir, "autopilot-state.json"), {
3751
+ await mkdir(join(stateDir, "sessions", "sess-stop-autopilot-planning-replay"), { recursive: true });
3752
+ await writeJson(join(stateDir, "sessions", "sess-stop-autopilot-planning-replay", "autopilot-state.json"), {
2991
3753
  active: true,
2992
3754
  current_phase: "planning",
2993
3755
  });
@@ -3041,12 +3803,40 @@ esac
3041
3803
  await rm(cwd, { recursive: true, force: true });
3042
3804
  }
3043
3805
  });
3806
+ for (const staleRootCase of [
3807
+ { mode: "autopilot", phase: "execution" },
3808
+ { mode: "ultrawork", phase: "executing" },
3809
+ { mode: "ultraqa", phase: "diagnose" },
3810
+ ]) {
3811
+ it(`does not block Stop from stale root ${staleRootCase.mode} state when the explicit session directory is missing`, async () => {
3812
+ const cwd = await mkdtemp(join(tmpdir(), `omx-native-hook-stop-missing-session-${staleRootCase.mode}-`));
3813
+ try {
3814
+ const stateDir = join(cwd, ".omx", "state");
3815
+ await mkdir(stateDir, { recursive: true });
3816
+ await writeJson(join(stateDir, `${staleRootCase.mode}-state.json`), {
3817
+ active: true,
3818
+ mode: staleRootCase.mode,
3819
+ current_phase: staleRootCase.phase,
3820
+ });
3821
+ const result = await dispatchCodexNativeHook({
3822
+ hook_event_name: "Stop",
3823
+ cwd,
3824
+ session_id: "missing-session",
3825
+ }, { cwd });
3826
+ assert.equal(result.omxEventName, "stop");
3827
+ assert.equal(result.outputJson, null);
3828
+ }
3829
+ finally {
3830
+ await rm(cwd, { recursive: true, force: true });
3831
+ }
3832
+ });
3833
+ }
3044
3834
  it("does not block Stop when an explicit blocked_on_user run_outcome is present on a mode state", async () => {
3045
3835
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-blocked-outcome-"));
3046
3836
  try {
3047
3837
  const stateDir = join(cwd, ".omx", "state");
3048
- await mkdir(stateDir, { recursive: true });
3049
- await writeJson(join(stateDir, "autopilot-state.json"), {
3838
+ await mkdir(join(stateDir, "sessions", "sess-stop-autopilot-blocked-outcome"), { recursive: true });
3839
+ await writeJson(join(stateDir, "sessions", "sess-stop-autopilot-blocked-outcome", "autopilot-state.json"), {
3050
3840
  active: true,
3051
3841
  current_phase: "execution",
3052
3842
  run_outcome: "blocked_on_user",
@@ -3067,8 +3857,8 @@ esac
3067
3857
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultrawork-"));
3068
3858
  try {
3069
3859
  const stateDir = join(cwd, ".omx", "state");
3070
- await mkdir(stateDir, { recursive: true });
3071
- await writeJson(join(stateDir, "ultrawork-state.json"), {
3860
+ await mkdir(join(stateDir, "sessions", "sess-stop-ultrawork"), { recursive: true });
3861
+ await writeJson(join(stateDir, "sessions", "sess-stop-ultrawork", "ultrawork-state.json"), {
3072
3862
  active: true,
3073
3863
  current_phase: "executing",
3074
3864
  });
@@ -3088,8 +3878,8 @@ esac
3088
3878
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultraqa-"));
3089
3879
  try {
3090
3880
  const stateDir = join(cwd, ".omx", "state");
3091
- await mkdir(stateDir, { recursive: true });
3092
- await writeJson(join(stateDir, "ultraqa-state.json"), {
3881
+ await mkdir(join(stateDir, "sessions", "sess-stop-ultraqa"), { recursive: true });
3882
+ await writeJson(join(stateDir, "sessions", "sess-stop-ultraqa", "ultraqa-state.json"), {
3093
3883
  active: true,
3094
3884
  current_phase: "diagnose",
3095
3885
  });
@@ -3105,16 +3895,42 @@ esac
3105
3895
  await rm(cwd, { recursive: true, force: true });
3106
3896
  }
3107
3897
  });
3108
- it("returns Stop continuation output while team phase is non-terminal", async () => {
3109
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-"));
3898
+ it("marks leader-owned team attention during native Stop dispatch without a polling watcher", async () => {
3899
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-attention-"));
3110
3900
  try {
3111
- const stateDir = join(cwd, ".omx", "state");
3112
- await mkdir(stateDir, { recursive: true });
3113
- await writeJson(join(stateDir, "team-state.json"), {
3114
- active: true,
3115
- current_phase: "team-exec",
3116
- team_name: "review-team",
3117
- });
3901
+ await initTeamState("stop-attention-team", "native stop attention", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-attention" });
3902
+ const result = await dispatchCodexNativeHook({
3903
+ hook_event_name: "Stop",
3904
+ cwd,
3905
+ session_id: "sess-stop-team-attention",
3906
+ }, { cwd });
3907
+ const attention = await readTeamLeaderAttention("stop-attention-team", cwd);
3908
+ assert.equal(result.omxEventName, "stop");
3909
+ assert.equal(attention?.source, "native_stop");
3910
+ assert.equal(attention?.leader_session_active, false);
3911
+ assert.equal(attention?.leader_session_id, "sess-stop-team-attention");
3912
+ assert.match(attention?.leader_session_stopped_at ?? "", /^\d{4}-\d{2}-\d{2}T/);
3913
+ assert.deepEqual(result.outputJson, {
3914
+ decision: "block",
3915
+ reason: `OMX team pipeline is still active (stop-attention-team) at phase team-exec; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
3916
+ stopReason: "team_team-exec",
3917
+ systemMessage: "OMX team pipeline is still active at phase team-exec.",
3918
+ });
3919
+ }
3920
+ finally {
3921
+ await rm(cwd, { recursive: true, force: true });
3922
+ }
3923
+ });
3924
+ it("returns Stop continuation output while team phase is non-terminal", async () => {
3925
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-"));
3926
+ try {
3927
+ const stateDir = join(cwd, ".omx", "state");
3928
+ await mkdir(stateDir, { recursive: true });
3929
+ await writeJson(join(stateDir, "team-state.json"), {
3930
+ active: true,
3931
+ current_phase: "team-exec",
3932
+ team_name: "review-team",
3933
+ });
3118
3934
  await writeJson(join(stateDir, "team", "review-team", "phase.json"), {
3119
3935
  current_phase: "team-verify",
3120
3936
  max_fix_attempts: 3,
@@ -3158,7 +3974,7 @@ esac
3158
3974
  team_state_root: join(cwd, ".omx", "state"),
3159
3975
  });
3160
3976
  await writeJson(join(workerDir, "status.json"), {
3161
- state: "idle",
3977
+ state: "working",
3162
3978
  current_task_id: "1",
3163
3979
  updated_at: new Date().toISOString(),
3164
3980
  });
@@ -3201,7 +4017,69 @@ esac
3201
4017
  await rm(cwd, { recursive: true, force: true });
3202
4018
  }
3203
4019
  });
3204
- it("suppresses identical team worker Stop replays but re-blocks fresh turns and task state changes", async () => {
4020
+ it("blocks Stop as a team-worker task failure when worker status is terminal but task evidence is not completed", async () => {
4021
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-terminal-stale-"));
4022
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
4023
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
4024
+ const prevLeaderCwd = process.env.OMX_TEAM_LEADER_CWD;
4025
+ try {
4026
+ await initTeamState("worker-stale-team", "worker stale stop fallback", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-stale" });
4027
+ const stateDir = join(cwd, ".omx", "state");
4028
+ const workerCwd = join(cwd, ".omx", "team", "worker-stale-team", "worktrees", "worker-1");
4029
+ const workerDir = join(stateDir, "team", "worker-stale-team", "workers", "worker-1");
4030
+ await mkdir(workerCwd, { recursive: true });
4031
+ await writeJson(join(workerDir, "identity.json"), {
4032
+ name: "worker-1",
4033
+ index: 1,
4034
+ role: "executor",
4035
+ assigned_tasks: ["1"],
4036
+ worktree_path: workerCwd,
4037
+ team_state_root: stateDir,
4038
+ });
4039
+ await writeJson(join(workerDir, "status.json"), {
4040
+ state: "done",
4041
+ current_task_id: "1",
4042
+ updated_at: new Date().toISOString(),
4043
+ });
4044
+ await writeJson(join(stateDir, "team", "worker-stale-team", "tasks", "task-1.json"), {
4045
+ id: "1",
4046
+ subject: "stale hook task",
4047
+ description: "non-completed task should still block terminal worker Stop",
4048
+ status: "in_progress",
4049
+ owner: "worker-1",
4050
+ created_at: new Date().toISOString(),
4051
+ });
4052
+ process.env.OMX_TEAM_WORKER = "worker-stale-team/worker-1";
4053
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
4054
+ process.env.OMX_TEAM_LEADER_CWD = cwd;
4055
+ const payload = {
4056
+ hook_event_name: "Stop",
4057
+ cwd: workerCwd,
4058
+ session_id: "sess-stop-team-worker-stale",
4059
+ thread_id: "thread-stop-team-worker-stale",
4060
+ };
4061
+ const result = await dispatchCodexNativeHook(payload, { cwd: workerCwd });
4062
+ const replay = await dispatchCodexNativeHook({ ...payload, stop_hook_active: true }, { cwd: workerCwd });
4063
+ assert.equal(result.outputJson?.stopReason, "team_worker_worker-1_1_in_progress");
4064
+ assert.equal(replay.outputJson, null);
4065
+ }
4066
+ finally {
4067
+ if (typeof prevTeamWorker === "string")
4068
+ process.env.OMX_TEAM_WORKER = prevTeamWorker;
4069
+ else
4070
+ delete process.env.OMX_TEAM_WORKER;
4071
+ if (typeof prevTeamStateRoot === "string")
4072
+ process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
4073
+ else
4074
+ delete process.env.OMX_TEAM_STATE_ROOT;
4075
+ if (typeof prevLeaderCwd === "string")
4076
+ process.env.OMX_TEAM_LEADER_CWD = prevLeaderCwd;
4077
+ else
4078
+ delete process.env.OMX_TEAM_LEADER_CWD;
4079
+ await rm(cwd, { recursive: true, force: true });
4080
+ }
4081
+ });
4082
+ it("re-blocks live team worker Stop replays but suppresses stale terminal worker repeats", async () => {
3205
4083
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-repeat-"));
3206
4084
  try {
3207
4085
  await initTeamState("worker-repeat-team", "worker stop repeat guard", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-repeat" });
@@ -3219,7 +4097,7 @@ esac
3219
4097
  team_state_root: stateDir,
3220
4098
  });
3221
4099
  await writeJson(join(workerDir, "status.json"), {
3222
- state: "idle",
4100
+ state: "working",
3223
4101
  current_task_id: "1",
3224
4102
  updated_at: new Date().toISOString(),
3225
4103
  });
@@ -3259,9 +4137,9 @@ esac
3259
4137
  owner: "worker-1",
3260
4138
  created_at: new Date().toISOString(),
3261
4139
  });
3262
- const stateChanged = await dispatchCodexNativeHook({ ...basePayload, turn_id: "turn-stop-team-worker-repeat-2", stop_hook_active: true }, { cwd: workerCwd });
4140
+ const stateChanged = await dispatchCodexNativeHook({ ...basePayload, turn_id: "turn-stop-team-worker-repeat-3", stop_hook_active: true }, { cwd: workerCwd });
3263
4141
  assert.deepEqual(first.outputJson, expectedInProgress);
3264
- assert.deepEqual(replay.outputJson, null);
4142
+ assert.deepEqual(replay.outputJson, expectedInProgress);
3265
4143
  assert.deepEqual(freshTurn.outputJson, expectedInProgress);
3266
4144
  assert.deepEqual(stateChanged.outputJson, {
3267
4145
  decision: "block",
@@ -3274,13 +4152,39 @@ esac
3274
4152
  await rm(cwd, { recursive: true, force: true });
3275
4153
  }
3276
4154
  });
3277
- it("does not block Stop for a team worker when assigned task is terminal", async () => {
4155
+ it("allows Stop for a team worker when assigned task is terminal and bypasses generic team blocking", async () => {
3278
4156
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-terminal-"));
3279
4157
  const prevTeamWorker = process.env.OMX_TEAM_WORKER;
3280
4158
  const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
4159
+ const prevPath = process.env.PATH;
3281
4160
  try {
3282
4161
  await initTeamState("worker-stop-team-terminal", "worker stop terminal fallback", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-terminal" });
4162
+ const fakeBinDir = join(cwd, "fake-bin");
4163
+ const tmuxLogPath = join(cwd, "tmux.log");
4164
+ await mkdir(fakeBinDir, { recursive: true });
4165
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
4166
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
3283
4167
  const workerDir = join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "workers", "worker-1");
4168
+ await writeJson(join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "config.json"), {
4169
+ name: "worker-stop-team-terminal",
4170
+ tmux_session: "omx-team-worker-stop",
4171
+ leader_pane_id: "%42",
4172
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
4173
+ });
4174
+ await writeJson(join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "manifest.v2.json"), {
4175
+ name: "worker-stop-team-terminal",
4176
+ tmux_session: "omx-team-worker-stop",
4177
+ leader_pane_id: "%42",
4178
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
4179
+ });
4180
+ await writeJson(join(workerDir, "identity.json"), {
4181
+ name: "worker-1",
4182
+ index: 1,
4183
+ role: "executor",
4184
+ assigned_tasks: ["1"],
4185
+ worktree_path: cwd,
4186
+ team_state_root: join(cwd, ".omx", "state"),
4187
+ });
3284
4188
  await writeJson(join(workerDir, "status.json"), {
3285
4189
  state: "done",
3286
4190
  current_task_id: "1",
@@ -3296,27 +4200,424 @@ esac
3296
4200
  });
3297
4201
  process.env.OMX_TEAM_WORKER = "worker-stop-team-terminal/worker-1";
3298
4202
  process.env.OMX_TEAM_STATE_ROOT = join(cwd, ".omx", "state");
4203
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
3299
4204
  const result = await dispatchCodexNativeHook({
3300
4205
  hook_event_name: "Stop",
3301
4206
  cwd,
3302
4207
  session_id: "sess-stop-team-worker-terminal",
3303
4208
  }, { cwd });
3304
- assert.deepEqual(result.outputJson, {
3305
- decision: "block",
3306
- reason: `OMX team pipeline is still active (worker-stop-team-terminal) at phase team-exec; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
3307
- stopReason: "team_team-exec",
3308
- systemMessage: "OMX team pipeline is still active at phase team-exec.",
4209
+ const replay = await dispatchCodexNativeHook({
4210
+ hook_event_name: "Stop",
4211
+ cwd,
4212
+ session_id: "sess-stop-team-worker-terminal",
4213
+ turn_id: "turn-worker-stop-terminal-replay",
4214
+ }, { cwd });
4215
+ assert.equal(result.outputJson, null);
4216
+ assert.equal(replay.outputJson, null);
4217
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
4218
+ const stopNudges = tmuxLog.match(/send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/g) || [];
4219
+ assert.equal(stopNudges.length, 1, "allowed worker Stop should nudge leader exactly once inside cooldown");
4220
+ const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
4221
+ assert.equal(nudgeState.delivery, "sent");
4222
+ }
4223
+ finally {
4224
+ if (typeof prevTeamWorker === "string")
4225
+ process.env.OMX_TEAM_WORKER = prevTeamWorker;
4226
+ else
4227
+ delete process.env.OMX_TEAM_WORKER;
4228
+ if (typeof prevTeamStateRoot === "string")
4229
+ process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
4230
+ else
4231
+ delete process.env.OMX_TEAM_STATE_ROOT;
4232
+ if (typeof prevPath === "string")
4233
+ process.env.PATH = prevPath;
4234
+ else
4235
+ delete process.env.PATH;
4236
+ await rm(cwd, { recursive: true, force: true });
4237
+ }
4238
+ });
4239
+ it("allows worker Stop when the Stop nudge helper cannot deliver", async () => {
4240
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-helper-fail-"));
4241
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
4242
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
4243
+ const prevPath = process.env.PATH;
4244
+ try {
4245
+ await initTeamState("worker-stop-helper-fail", "worker stop helper failure", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-helper-fail" });
4246
+ const fakeBinDir = join(cwd, "fake-bin");
4247
+ await mkdir(fakeBinDir, { recursive: true });
4248
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(join(cwd, "tmux.log"), { failSend: true }));
4249
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
4250
+ const stateDir = join(cwd, ".omx", "state");
4251
+ const workerDir = join(stateDir, "team", "worker-stop-helper-fail", "workers", "worker-1");
4252
+ await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "config.json"), {
4253
+ name: "worker-stop-helper-fail",
4254
+ tmux_session: "omx-team-worker-stop",
4255
+ leader_pane_id: "%42",
4256
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
4257
+ });
4258
+ await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "manifest.v2.json"), {
4259
+ name: "worker-stop-helper-fail",
4260
+ tmux_session: "omx-team-worker-stop",
4261
+ leader_pane_id: "%42",
4262
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
3309
4263
  });
4264
+ await writeJson(join(workerDir, "identity.json"), {
4265
+ name: "worker-1",
4266
+ assigned_tasks: ["1"],
4267
+ team_state_root: stateDir,
4268
+ });
4269
+ await writeJson(join(workerDir, "status.json"), {
4270
+ state: "done",
4271
+ current_task_id: "1",
4272
+ updated_at: new Date().toISOString(),
4273
+ });
4274
+ await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "tasks", "task-1.json"), {
4275
+ id: "1",
4276
+ status: "completed",
4277
+ owner: "worker-1",
4278
+ });
4279
+ process.env.OMX_TEAM_WORKER = "worker-stop-helper-fail/worker-1";
4280
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
4281
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
4282
+ const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-team-worker-helper-fail" }, { cwd });
4283
+ assert.equal(result.outputJson, null);
4284
+ const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
4285
+ assert.equal(nudgeState.delivery, "deferred");
4286
+ }
4287
+ finally {
4288
+ if (typeof prevTeamWorker === "string")
4289
+ process.env.OMX_TEAM_WORKER = prevTeamWorker;
4290
+ else
4291
+ delete process.env.OMX_TEAM_WORKER;
4292
+ if (typeof prevTeamStateRoot === "string")
4293
+ process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
4294
+ else
4295
+ delete process.env.OMX_TEAM_STATE_ROOT;
4296
+ if (typeof prevPath === "string")
4297
+ process.env.PATH = prevPath;
4298
+ else
4299
+ delete process.env.PATH;
4300
+ await rm(cwd, { recursive: true, force: true });
4301
+ }
4302
+ });
4303
+ it("does not treat failed or ambiguous worker task state as completed Stop evidence", async () => {
4304
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-failed-"));
4305
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
4306
+ const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
4307
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
4308
+ const prevPath = process.env.PATH;
4309
+ try {
4310
+ await initTeamState("worker-stop-failed-task", "worker stop failed task", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-failed" });
4311
+ const fakeBinDir = join(cwd, "fake-bin");
4312
+ const tmuxLogPath = join(cwd, "tmux.log");
4313
+ await mkdir(fakeBinDir, { recursive: true });
4314
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
4315
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
4316
+ const stateDir = join(cwd, ".omx", "state");
4317
+ const workerDir = join(stateDir, "team", "worker-stop-failed-task", "workers", "worker-1");
4318
+ await writeJson(join(stateDir, "team", "worker-stop-failed-task", "config.json"), {
4319
+ name: "worker-stop-failed-task",
4320
+ tmux_session: "omx-team-worker-stop",
4321
+ leader_pane_id: "%42",
4322
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
4323
+ });
4324
+ await writeJson(join(workerDir, "identity.json"), {
4325
+ name: "worker-1",
4326
+ assigned_tasks: ["1"],
4327
+ team_state_root: stateDir,
4328
+ });
4329
+ await writeJson(join(workerDir, "status.json"), {
4330
+ state: "failed",
4331
+ current_task_id: "1",
4332
+ updated_at: new Date().toISOString(),
4333
+ });
4334
+ await writeJson(join(stateDir, "team", "worker-stop-failed-task", "tasks", "task-1.json"), {
4335
+ id: "1",
4336
+ status: "failed",
4337
+ owner: "worker-1",
4338
+ });
4339
+ process.env.OMX_TEAM_WORKER = "worker-stop-failed-task/worker-1";
4340
+ delete process.env.OMX_TEAM_INTERNAL_WORKER;
4341
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
4342
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
4343
+ const result = await dispatchCodexNativeHook({
4344
+ hook_event_name: "Stop",
4345
+ cwd,
4346
+ session_id: "sess-stop-team-worker-failed",
4347
+ thread_id: "thread-stop-team-worker-failed",
4348
+ turn_id: "turn-stop-team-worker-failed",
4349
+ }, { cwd });
4350
+ assert.equal(result.outputJson?.decision, "block");
4351
+ assert.match(String(result.outputJson?.stopReason || ""), /non_completed_task_1_failed/);
4352
+ assert.match(JSON.stringify(result.outputJson), /team/i);
4353
+ assert.equal(existsSync(join(workerDir, "worker-stop-nudge.json")), false);
4354
+ const tmuxLog = existsSync(tmuxLogPath) ? await readFile(tmuxLogPath, "utf-8") : "";
4355
+ assert.doesNotMatch(tmuxLog, /native Stop allowed/);
4356
+ }
4357
+ finally {
4358
+ if (typeof prevTeamWorker === "string")
4359
+ process.env.OMX_TEAM_WORKER = prevTeamWorker;
4360
+ else
4361
+ delete process.env.OMX_TEAM_WORKER;
4362
+ if (typeof prevInternalTeamWorker === "string")
4363
+ process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
4364
+ else
4365
+ delete process.env.OMX_TEAM_INTERNAL_WORKER;
4366
+ if (typeof prevTeamStateRoot === "string")
4367
+ process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
4368
+ else
4369
+ delete process.env.OMX_TEAM_STATE_ROOT;
4370
+ if (typeof prevPath === "string")
4371
+ process.env.PATH = prevPath;
4372
+ else
4373
+ delete process.env.PATH;
4374
+ await rm(cwd, { recursive: true, force: true });
4375
+ }
4376
+ });
4377
+ it("blocks worker Stop on missing task assignment without relying on generic team state", async () => {
4378
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-missing-assignment-"));
4379
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
4380
+ const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
4381
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
4382
+ try {
4383
+ const stateDir = join(cwd, ".omx", "state");
4384
+ const workerDir = join(stateDir, "team", "worker-missing-assignment", "workers", "worker-1");
4385
+ await mkdir(workerDir, { recursive: true });
4386
+ await writeJson(join(workerDir, "identity.json"), {
4387
+ name: "worker-1",
4388
+ assigned_tasks: [],
4389
+ team_state_root: stateDir,
4390
+ });
4391
+ await writeJson(join(workerDir, "status.json"), {
4392
+ state: "idle",
4393
+ updated_at: new Date().toISOString(),
4394
+ });
4395
+ process.env.OMX_TEAM_WORKER = "worker-missing-assignment/worker-1";
4396
+ delete process.env.OMX_TEAM_INTERNAL_WORKER;
4397
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
4398
+ const result = await dispatchCodexNativeHook({
4399
+ hook_event_name: "Stop",
4400
+ cwd,
4401
+ session_id: "sess-stop-team-worker-missing-assignment",
4402
+ thread_id: "thread-stop-team-worker-missing-assignment",
4403
+ turn_id: "turn-stop-team-worker-missing-assignment",
4404
+ }, { cwd });
4405
+ assert.equal(result.outputJson?.decision, "block");
4406
+ assert.equal(result.outputJson?.stopReason, "team_worker_worker-1_missing_task_assignment");
4407
+ assert.equal(existsSync(join(workerDir, "worker-stop-nudge.json")), false);
4408
+ }
4409
+ finally {
4410
+ if (typeof prevTeamWorker === "string")
4411
+ process.env.OMX_TEAM_WORKER = prevTeamWorker;
4412
+ else
4413
+ delete process.env.OMX_TEAM_WORKER;
4414
+ if (typeof prevInternalTeamWorker === "string")
4415
+ process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
4416
+ else
4417
+ delete process.env.OMX_TEAM_INTERNAL_WORKER;
4418
+ if (typeof prevTeamStateRoot === "string")
4419
+ process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
4420
+ else
4421
+ delete process.env.OMX_TEAM_STATE_ROOT;
4422
+ await rm(cwd, { recursive: true, force: true });
4423
+ }
4424
+ });
4425
+ it("blocks unresolved worker Stop before generic auto-nudge can bypass it", async () => {
4426
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-missing-state-"));
4427
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
4428
+ const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
4429
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
4430
+ const prevPath = process.env.PATH;
4431
+ try {
4432
+ const stateDir = join(cwd, ".omx", "state");
4433
+ const fakeBinDir = join(cwd, "fake-bin");
4434
+ const tmuxLogPath = join(cwd, "tmux.log");
4435
+ await mkdir(fakeBinDir, { recursive: true });
4436
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
4437
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
4438
+ process.env.OMX_TEAM_WORKER = "worker-missing-state/worker-1";
4439
+ delete process.env.OMX_TEAM_INTERNAL_WORKER;
4440
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
4441
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
4442
+ const result = await dispatchCodexNativeHook({
4443
+ hook_event_name: "Stop",
4444
+ cwd,
4445
+ session_id: "sess-stop-team-worker-missing-state",
4446
+ thread_id: "thread-stop-team-worker-missing-state",
4447
+ turn_id: "turn-stop-team-worker-missing-state",
4448
+ last_assistant_message: "Should I proceed?",
4449
+ }, { cwd });
4450
+ assert.equal(result.outputJson?.decision, "block");
4451
+ assert.equal(result.outputJson?.stopReason, "team_worker_worker-1_missing_worker_state");
4452
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /auto_nudge/);
4453
+ const tmuxLog = existsSync(tmuxLogPath) ? await readFile(tmuxLogPath, "utf-8") : "";
4454
+ assert.doesNotMatch(tmuxLog, /native Stop allowed/);
4455
+ }
4456
+ finally {
4457
+ if (typeof prevTeamWorker === "string")
4458
+ process.env.OMX_TEAM_WORKER = prevTeamWorker;
4459
+ else
4460
+ delete process.env.OMX_TEAM_WORKER;
4461
+ if (typeof prevInternalTeamWorker === "string")
4462
+ process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
4463
+ else
4464
+ delete process.env.OMX_TEAM_INTERNAL_WORKER;
4465
+ if (typeof prevTeamStateRoot === "string")
4466
+ process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
4467
+ else
4468
+ delete process.env.OMX_TEAM_STATE_ROOT;
4469
+ if (typeof prevPath === "string")
4470
+ process.env.PATH = prevPath;
4471
+ else
4472
+ delete process.env.PATH;
4473
+ await rm(cwd, { recursive: true, force: true });
4474
+ }
4475
+ });
4476
+ it("prefers canonical internal worker identity over public worker identity for Stop nudges", async () => {
4477
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-internal-env-"));
4478
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
4479
+ const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
4480
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
4481
+ const prevPath = process.env.PATH;
4482
+ try {
4483
+ const stateDir = join(cwd, ".omx", "state");
4484
+ const fakeBinDir = join(cwd, "fake-bin");
4485
+ const tmuxLogPath = join(cwd, "tmux.log");
4486
+ await mkdir(fakeBinDir, { recursive: true });
4487
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
4488
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
4489
+ const workerDir = join(stateDir, "team", "internal-stop-team", "workers", "worker-1");
4490
+ await writeJson(join(stateDir, "team", "internal-stop-team", "config.json"), {
4491
+ name: "internal-stop-team",
4492
+ tmux_session: "omx-team-worker-stop",
4493
+ leader_pane_id: "%42",
4494
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
4495
+ });
4496
+ await writeJson(join(workerDir, "identity.json"), {
4497
+ name: "worker-1",
4498
+ assigned_tasks: ["1"],
4499
+ team_state_root: stateDir,
4500
+ });
4501
+ await writeJson(join(workerDir, "status.json"), {
4502
+ state: "done",
4503
+ current_task_id: "1",
4504
+ updated_at: new Date().toISOString(),
4505
+ });
4506
+ await writeJson(join(stateDir, "team", "internal-stop-team", "tasks", "task-1.json"), {
4507
+ id: "1",
4508
+ status: "completed",
4509
+ owner: "worker-1",
4510
+ });
4511
+ process.env.OMX_TEAM_WORKER = "public-stop-team/worker-1";
4512
+ process.env.OMX_TEAM_INTERNAL_WORKER = "internal-stop-team/worker-1";
4513
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
4514
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
4515
+ const result = await dispatchCodexNativeHook({
4516
+ hook_event_name: "Stop",
4517
+ cwd,
4518
+ session_id: "sess-stop-team-worker-internal-env",
4519
+ thread_id: "thread-stop-team-worker-internal-env",
4520
+ turn_id: "turn-stop-team-worker-internal-env",
4521
+ }, { cwd });
4522
+ assert.equal(result.outputJson, null);
4523
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
4524
+ assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
4525
+ assert.equal(existsSync(join(workerDir, "worker-stop-nudge.json")), true);
4526
+ }
4527
+ finally {
4528
+ if (typeof prevTeamWorker === "string")
4529
+ process.env.OMX_TEAM_WORKER = prevTeamWorker;
4530
+ else
4531
+ delete process.env.OMX_TEAM_WORKER;
4532
+ if (typeof prevInternalTeamWorker === "string")
4533
+ process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
4534
+ else
4535
+ delete process.env.OMX_TEAM_INTERNAL_WORKER;
4536
+ if (typeof prevTeamStateRoot === "string")
4537
+ process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
4538
+ else
4539
+ delete process.env.OMX_TEAM_STATE_ROOT;
4540
+ if (typeof prevPath === "string")
4541
+ process.env.PATH = prevPath;
4542
+ else
4543
+ delete process.env.PATH;
4544
+ await rm(cwd, { recursive: true, force: true });
4545
+ }
4546
+ });
4547
+ it("blocks worker Stop when canonical task ownership has a newer non-terminal task", async () => {
4548
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-owned-task-"));
4549
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
4550
+ const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
4551
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
4552
+ const prevPath = process.env.PATH;
4553
+ try {
4554
+ const stateDir = join(cwd, ".omx", "state");
4555
+ const fakeBinDir = join(cwd, "fake-bin");
4556
+ const tmuxLogPath = join(cwd, "tmux.log");
4557
+ await mkdir(fakeBinDir, { recursive: true });
4558
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
4559
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
4560
+ const workerDir = join(stateDir, "team", "worker-owned-task", "workers", "worker-1");
4561
+ await writeJson(join(stateDir, "team", "worker-owned-task", "config.json"), {
4562
+ name: "worker-owned-task",
4563
+ tmux_session: "omx-team-worker-stop",
4564
+ leader_pane_id: "%42",
4565
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
4566
+ });
4567
+ await writeJson(join(workerDir, "identity.json"), {
4568
+ name: "worker-1",
4569
+ assigned_tasks: ["1"],
4570
+ team_state_root: stateDir,
4571
+ });
4572
+ await writeJson(join(workerDir, "status.json"), {
4573
+ state: "done",
4574
+ current_task_id: "1",
4575
+ updated_at: new Date().toISOString(),
4576
+ });
4577
+ await writeJson(join(stateDir, "team", "worker-owned-task", "tasks", "task-1.json"), {
4578
+ id: "1",
4579
+ status: "completed",
4580
+ owner: "worker-1",
4581
+ });
4582
+ await writeJson(join(stateDir, "team", "worker-owned-task", "tasks", "task-2.json"), {
4583
+ id: "2",
4584
+ status: "in_progress",
4585
+ owner: "worker-1",
4586
+ });
4587
+ process.env.OMX_TEAM_WORKER = "worker-owned-task/worker-1";
4588
+ delete process.env.OMX_TEAM_INTERNAL_WORKER;
4589
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
4590
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
4591
+ const result = await dispatchCodexNativeHook({
4592
+ hook_event_name: "Stop",
4593
+ cwd,
4594
+ session_id: "sess-stop-team-worker-owned-task",
4595
+ thread_id: "thread-stop-team-worker-owned-task",
4596
+ turn_id: "turn-stop-team-worker-owned-task",
4597
+ }, { cwd });
4598
+ assert.equal(result.outputJson?.decision, "block");
4599
+ assert.equal(result.outputJson?.stopReason, "team_worker_worker-1_2_in_progress");
4600
+ assert.equal(existsSync(join(workerDir, "worker-stop-nudge.json")), false);
4601
+ const tmuxLog = existsSync(tmuxLogPath) ? await readFile(tmuxLogPath, "utf-8") : "";
4602
+ assert.doesNotMatch(tmuxLog, /native Stop allowed/);
3310
4603
  }
3311
4604
  finally {
3312
4605
  if (typeof prevTeamWorker === "string")
3313
4606
  process.env.OMX_TEAM_WORKER = prevTeamWorker;
3314
4607
  else
3315
4608
  delete process.env.OMX_TEAM_WORKER;
4609
+ if (typeof prevInternalTeamWorker === "string")
4610
+ process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
4611
+ else
4612
+ delete process.env.OMX_TEAM_INTERNAL_WORKER;
3316
4613
  if (typeof prevTeamStateRoot === "string")
3317
4614
  process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
3318
4615
  else
3319
4616
  delete process.env.OMX_TEAM_STATE_ROOT;
4617
+ if (typeof prevPath === "string")
4618
+ process.env.PATH = prevPath;
4619
+ else
4620
+ delete process.env.PATH;
3320
4621
  await rm(cwd, { recursive: true, force: true });
3321
4622
  }
3322
4623
  });
@@ -3626,12 +4927,12 @@ esac
3626
4927
  session_id: "sess-stop-skill",
3627
4928
  }, { cwd });
3628
4929
  assert.equal(result.omxEventName, "stop");
3629
- assert.deepEqual(result.outputJson, {
3630
- decision: "block",
3631
- reason: "OMX skill ralplan is still active (phase: planning); continue until the current ralplan workflow reaches a terminal state.",
3632
- stopReason: "skill_ralplan_planning",
3633
- systemMessage: "OMX skill ralplan is still active (phase: planning).",
3634
- });
4930
+ assert.equal(result.outputJson?.decision, "block");
4931
+ assert.match(String(result.outputJson?.reason ?? ""), /Status: continue_from_artifact/);
4932
+ assert.match(String(result.outputJson?.reason ?? ""), /ralplan is still active \(phase: planning\)/);
4933
+ assert.match(String(result.outputJson?.reason ?? ""), /continue from the current ralplan artifact/i);
4934
+ assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
4935
+ assert.match(String(result.outputJson?.systemMessage ?? ""), /complete, paused for review, waiting for input, or still continuing/);
3635
4936
  }
3636
4937
  finally {
3637
4938
  await rm(cwd, { recursive: true, force: true });
@@ -3667,7 +4968,91 @@ esac
3667
4968
  await rm(cwd, { recursive: true, force: true });
3668
4969
  }
3669
4970
  });
3670
- it("does not block on active ralplan skill when subagents are still active", async () => {
4971
+ it("does not block on stale ralplan skill-active when canonical run-state is terminal", async () => {
4972
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-terminal-ralplan-run-"));
4973
+ try {
4974
+ const stateDir = join(cwd, ".omx", "state");
4975
+ const sessionId = "sess-stop-terminal-ralplan";
4976
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
4977
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
4978
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
4979
+ active: true,
4980
+ skill: "ralplan",
4981
+ phase: "planning",
4982
+ session_id: sessionId,
4983
+ active_skills: [{
4984
+ skill: "ralplan",
4985
+ phase: "planning",
4986
+ active: true,
4987
+ session_id: sessionId,
4988
+ }],
4989
+ });
4990
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
4991
+ active: true,
4992
+ mode: "ralplan",
4993
+ current_phase: "planning",
4994
+ session_id: sessionId,
4995
+ });
4996
+ await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
4997
+ version: 1,
4998
+ mode: "ralplan",
4999
+ active: false,
5000
+ outcome: "finish",
5001
+ lifecycle_outcome: "finished",
5002
+ current_phase: "complete",
5003
+ completed_at: "2026-05-01T00:00:00.000Z",
5004
+ updated_at: "2026-05-01T00:00:00.000Z",
5005
+ });
5006
+ const result = await dispatchCodexNativeHook({
5007
+ hook_event_name: "Stop",
5008
+ cwd,
5009
+ session_id: sessionId,
5010
+ }, { cwd });
5011
+ assert.equal(result.omxEventName, "stop");
5012
+ assert.equal(result.outputJson, null);
5013
+ }
5014
+ finally {
5015
+ await rm(cwd, { recursive: true, force: true });
5016
+ }
5017
+ });
5018
+ it("does not block on stale ralplan skill-active when pinned mode state belongs to another session", async () => {
5019
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-foreign-ralplan-"));
5020
+ try {
5021
+ const stateDir = join(cwd, ".omx", "state");
5022
+ const sessionId = "sess-stop-current-ralplan";
5023
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
5024
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
5025
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
5026
+ active: true,
5027
+ skill: "ralplan",
5028
+ phase: "planning",
5029
+ session_id: sessionId,
5030
+ active_skills: [{
5031
+ skill: "ralplan",
5032
+ phase: "planning",
5033
+ active: true,
5034
+ session_id: sessionId,
5035
+ }],
5036
+ });
5037
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
5038
+ active: true,
5039
+ mode: "ralplan",
5040
+ current_phase: "planning",
5041
+ session_id: "sess-other-ralplan",
5042
+ });
5043
+ const result = await dispatchCodexNativeHook({
5044
+ hook_event_name: "Stop",
5045
+ cwd,
5046
+ session_id: sessionId,
5047
+ }, { cwd });
5048
+ assert.equal(result.omxEventName, "stop");
5049
+ assert.equal(result.outputJson, null);
5050
+ }
5051
+ finally {
5052
+ await rm(cwd, { recursive: true, force: true });
5053
+ }
5054
+ });
5055
+ it("returns an explicit ralplan waiting status while subagents are still active", async () => {
3671
5056
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-subagent-"));
3672
5057
  try {
3673
5058
  const stateDir = join(cwd, ".omx", "state");
@@ -3714,7 +5099,11 @@ esac
3714
5099
  session_id: "sess-stop-skill-subagent",
3715
5100
  }, { cwd });
3716
5101
  assert.equal(result.omxEventName, "stop");
3717
- assert.equal(result.outputJson, null);
5102
+ assert.equal(result.outputJson?.decision, "block");
5103
+ assert.match(String(result.outputJson?.reason ?? ""), /Status: waiting/);
5104
+ assert.match(String(result.outputJson?.reason ?? ""), /waiting for 1 active native subagent thread/);
5105
+ assert.match(String(result.outputJson?.reason ?? ""), /then continue from the current ralplan artifact/i);
5106
+ assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_waiting_subagent");
3718
5107
  }
3719
5108
  finally {
3720
5109
  await rm(cwd, { recursive: true, force: true });
@@ -3833,6 +5222,31 @@ esac
3833
5222
  await rm(cwd, { recursive: true, force: true });
3834
5223
  }
3835
5224
  });
5225
+ it("does not block Stop from stale root autoresearch state when the explicit session directory is missing", async () => {
5226
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-missing-session-autoresearch-"));
5227
+ try {
5228
+ const stateDir = join(cwd, ".omx", "state");
5229
+ await mkdir(stateDir, { recursive: true });
5230
+ await writeJson(join(stateDir, "autoresearch-state.json"), {
5231
+ active: true,
5232
+ mode: "autoresearch",
5233
+ current_phase: "executing",
5234
+ validation_mode: "mission-validator-script",
5235
+ mission_validator_command: "node scripts/validate.js",
5236
+ completion_artifact_path: ".omx/specs/autoresearch-demo/completion.json",
5237
+ });
5238
+ const result = await dispatchCodexNativeHook({
5239
+ hook_event_name: "Stop",
5240
+ cwd,
5241
+ session_id: "missing-session",
5242
+ }, { cwd });
5243
+ assert.equal(result.omxEventName, "stop");
5244
+ assert.equal(result.outputJson, null);
5245
+ }
5246
+ finally {
5247
+ await rm(cwd, { recursive: true, force: true });
5248
+ }
5249
+ });
3836
5250
  it("does not block Stop solely because deep-interview is active", async () => {
3837
5251
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-"));
3838
5252
  try {
@@ -4527,6 +5941,205 @@ esac
4527
5941
  await rm(cwd, { recursive: true, force: true });
4528
5942
  }
4529
5943
  });
5944
+ it("does not block a question-only pane from Ralph state owned by another Codex session", async () => {
5945
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-question-pane-"));
5946
+ const previousTmuxPane = process.env.TMUX_PANE;
5947
+ try {
5948
+ const stateDir = join(cwd, ".omx", "state");
5949
+ const questionSessionId = "sess-question-pane";
5950
+ const questionNativeSessionId = "codex-question-pane";
5951
+ await mkdir(join(stateDir, "sessions", questionSessionId), { recursive: true });
5952
+ await writeJson(join(stateDir, "session.json"), {
5953
+ session_id: questionSessionId,
5954
+ native_session_id: questionNativeSessionId,
5955
+ cwd,
5956
+ });
5957
+ await writeJson(join(stateDir, "sessions", questionSessionId, "ralph-state.json"), {
5958
+ active: true,
5959
+ mode: "ralph",
5960
+ current_phase: "executing",
5961
+ session_id: questionSessionId,
5962
+ owner_omx_session_id: "sess-ralph-owner",
5963
+ owner_codex_session_id: "codex-ralph-owner",
5964
+ thread_id: "thread-ralph-owner",
5965
+ tmux_pane_id: "%41",
5966
+ });
5967
+ process.env.TMUX_PANE = "%99";
5968
+ const result = await dispatchCodexNativeHook({
5969
+ hook_event_name: "Stop",
5970
+ cwd,
5971
+ session_id: questionNativeSessionId,
5972
+ thread_id: "thread-question-pane",
5973
+ }, { cwd });
5974
+ assert.equal(result.omxEventName, "stop");
5975
+ assert.equal(result.outputJson, null);
5976
+ }
5977
+ finally {
5978
+ if (typeof previousTmuxPane === "string")
5979
+ process.env.TMUX_PANE = previousTmuxPane;
5980
+ else
5981
+ delete process.env.TMUX_PANE;
5982
+ await rm(cwd, { recursive: true, force: true });
5983
+ }
5984
+ });
5985
+ it("does not block Stop when Ralph skill-active initialization points at another session", async () => {
5986
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-stale-skill-active-"));
5987
+ try {
5988
+ const stateDir = join(cwd, ".omx", "state");
5989
+ const currentSessionId = "sess-current-ralph";
5990
+ await mkdir(join(stateDir, "sessions", currentSessionId), { recursive: true });
5991
+ await writeJson(join(stateDir, "session.json"), {
5992
+ session_id: currentSessionId,
5993
+ native_session_id: currentSessionId,
5994
+ cwd,
5995
+ });
5996
+ await writeJson(join(stateDir, "sessions", currentSessionId, "ralph-state.json"), {
5997
+ active: true,
5998
+ mode: "ralph",
5999
+ current_phase: "verifying",
6000
+ session_id: currentSessionId,
6001
+ owner_omx_session_id: currentSessionId,
6002
+ task_slug: "stale-rebound-task",
6003
+ });
6004
+ await writeJson(join(stateDir, "sessions", currentSessionId, "skill-active-state.json"), {
6005
+ active: true,
6006
+ skill: "ralph",
6007
+ phase: "verifying",
6008
+ session_id: currentSessionId,
6009
+ initialized_mode: "ralph",
6010
+ initialized_state_path: ".omx/state/sessions/sess-old-ralph/ralph-state.json",
6011
+ active_skills: [{ skill: "ralph", phase: "verifying", active: true, session_id: currentSessionId }],
6012
+ });
6013
+ const result = await dispatchCodexNativeHook({
6014
+ hook_event_name: "Stop",
6015
+ cwd,
6016
+ session_id: currentSessionId,
6017
+ }, { cwd });
6018
+ assert.equal(result.omxEventName, "stop");
6019
+ assert.equal(result.outputJson, null);
6020
+ }
6021
+ finally {
6022
+ await rm(cwd, { recursive: true, force: true });
6023
+ }
6024
+ });
6025
+ it("blocks same-session Ralph Stop continuation when ownership identifiers match", async () => {
6026
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-owned-session-"));
6027
+ const previousTmuxPane = process.env.TMUX_PANE;
6028
+ try {
6029
+ const stateDir = join(cwd, ".omx", "state");
6030
+ const omxSessionId = "sess-ralph-owned";
6031
+ const nativeSessionId = "codex-ralph-owned";
6032
+ await mkdir(join(stateDir, "sessions", omxSessionId), { recursive: true });
6033
+ await writeJson(join(stateDir, "session.json"), {
6034
+ session_id: omxSessionId,
6035
+ native_session_id: nativeSessionId,
6036
+ cwd,
6037
+ });
6038
+ await writeJson(join(stateDir, "sessions", omxSessionId, "ralph-state.json"), {
6039
+ active: true,
6040
+ mode: "ralph",
6041
+ current_phase: "executing",
6042
+ session_id: omxSessionId,
6043
+ owner_omx_session_id: omxSessionId,
6044
+ owner_codex_session_id: nativeSessionId,
6045
+ thread_id: "thread-ralph-owned",
6046
+ tmux_pane_id: "%42",
6047
+ });
6048
+ process.env.TMUX_PANE = "%42";
6049
+ const result = await dispatchCodexNativeHook({
6050
+ hook_event_name: "Stop",
6051
+ cwd,
6052
+ session_id: nativeSessionId,
6053
+ thread_id: "thread-ralph-owned",
6054
+ }, { cwd });
6055
+ assert.equal(result.omxEventName, "stop");
6056
+ assert.deepEqual(result.outputJson, {
6057
+ decision: "block",
6058
+ reason: "OMX Ralph is still active (phase: executing; state: .omx/state/sessions/sess-ralph-owned/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
6059
+ stopReason: "ralph_executing",
6060
+ systemMessage: "OMX Ralph is still active (phase: executing; state: .omx/state/sessions/sess-ralph-owned/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
6061
+ });
6062
+ }
6063
+ finally {
6064
+ if (typeof previousTmuxPane === "string")
6065
+ process.env.TMUX_PANE = previousTmuxPane;
6066
+ else
6067
+ delete process.env.TMUX_PANE;
6068
+ await rm(cwd, { recursive: true, force: true });
6069
+ }
6070
+ });
6071
+ it("allows native verifier subagent Stop to complete while leader Ralph remains active", async () => {
6072
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-subagent-verdict-"));
6073
+ try {
6074
+ const stateDir = join(cwd, ".omx", "state");
6075
+ const omxSessionId = "sess-ralph-leader-verifier";
6076
+ const leaderNativeSessionId = "codex-ralph-leader-verifier";
6077
+ const childNativeSessionId = "codex-verifier-child";
6078
+ await mkdir(join(stateDir, "sessions", omxSessionId), { recursive: true });
6079
+ await writeSessionStart(cwd, omxSessionId, {
6080
+ nativeSessionId: leaderNativeSessionId,
6081
+ });
6082
+ await writeJson(join(stateDir, "sessions", omxSessionId, "ralph-state.json"), {
6083
+ active: true,
6084
+ mode: "ralph",
6085
+ current_phase: "verifying",
6086
+ session_id: omxSessionId,
6087
+ owner_omx_session_id: omxSessionId,
6088
+ owner_codex_session_id: leaderNativeSessionId,
6089
+ });
6090
+ const transcriptPath = join(cwd, "verifier-subagent-rollout.jsonl");
6091
+ await writeFile(transcriptPath, `${JSON.stringify({
6092
+ type: "session_meta",
6093
+ payload: {
6094
+ id: childNativeSessionId,
6095
+ source: {
6096
+ subagent: {
6097
+ thread_spawn: {
6098
+ parent_thread_id: leaderNativeSessionId,
6099
+ depth: 1,
6100
+ agent_nickname: "Verifier",
6101
+ agent_role: "verifier",
6102
+ },
6103
+ },
6104
+ },
6105
+ agent_nickname: "Verifier",
6106
+ agent_role: "verifier",
6107
+ },
6108
+ })}\n`);
6109
+ await dispatchCodexNativeHook({
6110
+ hook_event_name: "SessionStart",
6111
+ cwd,
6112
+ session_id: childNativeSessionId,
6113
+ transcript_path: transcriptPath,
6114
+ }, { cwd, sessionOwnerPid: process.pid });
6115
+ const childStop = await dispatchCodexNativeHook({
6116
+ hook_event_name: "Stop",
6117
+ cwd,
6118
+ session_id: childNativeSessionId,
6119
+ thread_id: childNativeSessionId,
6120
+ last_assistant_message: "Verdict: APPROVED. Evidence is sufficient.",
6121
+ }, { cwd });
6122
+ assert.equal(childStop.omxEventName, "stop");
6123
+ assert.equal(childStop.outputJson, null);
6124
+ const leaderStop = await dispatchCodexNativeHook({
6125
+ hook_event_name: "Stop",
6126
+ cwd,
6127
+ session_id: leaderNativeSessionId,
6128
+ thread_id: leaderNativeSessionId,
6129
+ last_assistant_message: "Waiting on verification integration.",
6130
+ }, { cwd });
6131
+ assert.equal(leaderStop.omxEventName, "stop");
6132
+ assert.deepEqual(leaderStop.outputJson, {
6133
+ decision: "block",
6134
+ reason: "OMX Ralph is still active (phase: verifying; state: .omx/state/sessions/sess-ralph-leader-verifier/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
6135
+ stopReason: "ralph_verifying",
6136
+ systemMessage: "OMX Ralph is still active (phase: verifying; state: .omx/state/sessions/sess-ralph-leader-verifier/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
6137
+ });
6138
+ }
6139
+ finally {
6140
+ await rm(cwd, { recursive: true, force: true });
6141
+ }
6142
+ });
4530
6143
  it("prefers canonical run-state terminal lifecycle before stale session Ralph state during Stop", async () => {
4531
6144
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-canonical-run-state-ralph-"));
4532
6145
  try {
@@ -5062,12 +6675,11 @@ esac
5062
6675
  await rm(cwd, { recursive: true, force: true });
5063
6676
  }
5064
6677
  });
5065
- it("suppresses native auto-nudge when root deep-interview mode state is active without an explicit session", async () => {
6678
+ it("suppresses native auto-nudge when root deep-interview mode state is active and no session is known", async () => {
5066
6679
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-deep-interview-mode-"));
5067
6680
  try {
5068
6681
  const stateDir = join(cwd, ".omx", "state");
5069
6682
  await mkdir(stateDir, { recursive: true });
5070
- process.env.OMX_SESSION_ID = "sess-stop-auto-mode";
5071
6683
  await writeJson(join(stateDir, "deep-interview-state.json"), {
5072
6684
  active: true,
5073
6685
  mode: "deep-interview",
@@ -5086,6 +6698,57 @@ esac
5086
6698
  await rm(cwd, { recursive: true, force: true });
5087
6699
  }
5088
6700
  });
6701
+ it("treats inherited OMX_SESSION_ID as session-aware for native auto-nudge Stop checks", async () => {
6702
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-env-session-"));
6703
+ try {
6704
+ const stateDir = join(cwd, ".omx", "state");
6705
+ await mkdir(stateDir, { recursive: true });
6706
+ process.env.OMX_SESSION_ID = "sess-stop-auto-mode";
6707
+ const result = await dispatchCodexNativeHook({
6708
+ hook_event_name: "Stop",
6709
+ cwd,
6710
+ thread_id: "thread-stop-auto-env-session",
6711
+ turn_id: "turn-stop-auto-env-session-1",
6712
+ last_assistant_message: "Keep going and finish the cleanup.",
6713
+ }, { cwd });
6714
+ assert.equal(result.omxEventName, "stop");
6715
+ assert.deepEqual(result.outputJson, {
6716
+ decision: "block",
6717
+ reason: DEFAULT_AUTO_NUDGE_RESPONSE,
6718
+ stopReason: "auto_nudge",
6719
+ systemMessage: "OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
6720
+ });
6721
+ const stopState = JSON.parse(await readFile(join(stateDir, "native-stop-state.json"), "utf-8"));
6722
+ assert.ok(stopState.sessions["sess-stop-auto-mode"]);
6723
+ }
6724
+ finally {
6725
+ await rm(cwd, { recursive: true, force: true });
6726
+ }
6727
+ });
6728
+ it("ignores generic SESSION_ID for native auto-nudge Stop session scoping", async () => {
6729
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-generic-session-"));
6730
+ try {
6731
+ const stateDir = join(cwd, ".omx", "state");
6732
+ await mkdir(stateDir, { recursive: true });
6733
+ process.env.SESSION_ID = "generic-shell-session";
6734
+ const result = await dispatchCodexNativeHook({
6735
+ hook_event_name: "Stop",
6736
+ cwd,
6737
+ thread_id: "thread-stop-auto-generic-session",
6738
+ turn_id: "turn-stop-auto-generic-session-1",
6739
+ last_assistant_message: "Keep going and finish the cleanup.",
6740
+ }, { cwd });
6741
+ assert.equal(result.omxEventName, "stop");
6742
+ assert.equal(result.outputJson?.decision, "block");
6743
+ const stopState = JSON.parse(await readFile(join(stateDir, "native-stop-state.json"), "utf-8"));
6744
+ const sessions = stopState.sessions;
6745
+ assert.equal(sessions["generic-shell-session"], undefined);
6746
+ assert.ok(sessions["thread-stop-auto-generic-session"]);
6747
+ }
6748
+ finally {
6749
+ await rm(cwd, { recursive: true, force: true });
6750
+ }
6751
+ });
5089
6752
  it("does not suppress native auto-nudge from stale root deep-interview mode state when the explicit session-scoped mode state is absent", async () => {
5090
6753
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-stale-root-mode-"));
5091
6754
  try {
@@ -5368,8 +7031,8 @@ esac
5368
7031
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultrawork-repeat-"));
5369
7032
  try {
5370
7033
  const stateDir = join(cwd, ".omx", "state");
5371
- await mkdir(stateDir, { recursive: true });
5372
- await writeJson(join(stateDir, "ultrawork-state.json"), {
7034
+ await mkdir(join(stateDir, "sessions", "sess-stop-ultrawork-repeat"), { recursive: true });
7035
+ await writeJson(join(stateDir, "sessions", "sess-stop-ultrawork-repeat", "ultrawork-state.json"), {
5373
7036
  active: true,
5374
7037
  current_phase: "executing",
5375
7038
  });
@@ -5441,12 +7104,10 @@ esac
5441
7104
  stop_hook_active: true,
5442
7105
  }, { cwd });
5443
7106
  assert.equal(repeated.omxEventName, "stop");
5444
- assert.deepEqual(repeated.outputJson, {
5445
- decision: "block",
5446
- reason: "OMX skill ralplan is still active (phase: planning); continue until the current ralplan workflow reaches a terminal state.",
5447
- stopReason: "skill_ralplan_planning",
5448
- systemMessage: "OMX skill ralplan is still active (phase: planning).",
5449
- });
7107
+ assert.equal(repeated.outputJson?.decision, "block");
7108
+ assert.match(String(repeated.outputJson?.reason ?? ""), /Status: continue_from_artifact/);
7109
+ assert.match(String(repeated.outputJson?.reason ?? ""), /continue from the current ralplan artifact/i);
7110
+ assert.equal(repeated.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
5450
7111
  }
5451
7112
  finally {
5452
7113
  await rm(cwd, { recursive: true, force: true });