oh-my-codex 0.15.3 → 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 (452) 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/autoresearch/goal.d.ts +90 -0
  7. package/dist/autoresearch/goal.d.ts.map +1 -0
  8. package/dist/autoresearch/goal.js +237 -0
  9. package/dist/autoresearch/goal.js.map +1 -0
  10. package/dist/autoresearch/skill-validation.d.ts +1 -0
  11. package/dist/autoresearch/skill-validation.d.ts.map +1 -1
  12. package/dist/autoresearch/skill-validation.js +10 -3
  13. package/dist/autoresearch/skill-validation.js.map +1 -1
  14. package/dist/catalog/__tests__/generator.test.js +9 -4
  15. package/dist/catalog/__tests__/generator.test.js.map +1 -1
  16. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js +20 -1
  17. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js.map +1 -1
  18. package/dist/catalog/__tests__/schema.test.js +14 -3
  19. package/dist/catalog/__tests__/schema.test.js.map +1 -1
  20. package/dist/catalog/schema.js +1 -1
  21. package/dist/catalog/schema.js.map +1 -1
  22. package/dist/cli/__tests__/autoresearch-goal.test.d.ts +2 -0
  23. package/dist/cli/__tests__/autoresearch-goal.test.d.ts.map +1 -0
  24. package/dist/cli/__tests__/autoresearch-goal.test.js +194 -0
  25. package/dist/cli/__tests__/autoresearch-goal.test.js.map +1 -0
  26. package/dist/cli/__tests__/cleanup.test.js +82 -1
  27. package/dist/cli/__tests__/cleanup.test.js.map +1 -1
  28. package/dist/cli/__tests__/codex-plugin-layout.test.js +7 -4
  29. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  30. package/dist/cli/__tests__/doctor-warning-copy.test.js +23 -0
  31. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  32. package/dist/cli/__tests__/explore.test.js +8 -1
  33. package/dist/cli/__tests__/explore.test.js.map +1 -1
  34. package/dist/cli/__tests__/index.test.js +82 -3
  35. package/dist/cli/__tests__/index.test.js.map +1 -1
  36. package/dist/cli/__tests__/launch-fallback.test.js +58 -0
  37. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  38. package/dist/cli/__tests__/native-assets.test.js +26 -1
  39. package/dist/cli/__tests__/native-assets.test.js.map +1 -1
  40. package/dist/cli/__tests__/package-bin-contract.test.js +2 -2
  41. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  42. package/dist/cli/__tests__/performance-goal.test.d.ts +2 -0
  43. package/dist/cli/__tests__/performance-goal.test.d.ts.map +1 -0
  44. package/dist/cli/__tests__/performance-goal.test.js +144 -0
  45. package/dist/cli/__tests__/performance-goal.test.js.map +1 -0
  46. package/dist/cli/__tests__/question.test.js +8 -0
  47. package/dist/cli/__tests__/question.test.js.map +1 -1
  48. package/dist/cli/__tests__/ralph-goal-mode-contract.test.d.ts +2 -0
  49. package/dist/cli/__tests__/ralph-goal-mode-contract.test.d.ts.map +1 -0
  50. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +31 -0
  51. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -0
  52. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js +5 -4
  53. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -1
  54. package/dist/cli/__tests__/ralph-prd-smoke.test.js +7 -0
  55. package/dist/cli/__tests__/ralph-prd-smoke.test.js.map +1 -1
  56. package/dist/cli/__tests__/setup-install-mode.test.js +57 -21
  57. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  58. package/dist/cli/__tests__/setup-refresh.test.js +27 -8
  59. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  60. package/dist/cli/__tests__/setup-scope.test.js +18 -9
  61. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  62. package/dist/cli/__tests__/setup-skill-validation.test.js +11 -11
  63. package/dist/cli/__tests__/setup-skill-validation.test.js.map +1 -1
  64. package/dist/cli/__tests__/setup-skills-overwrite.test.js +12 -12
  65. package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
  66. package/dist/cli/__tests__/team.test.js +187 -0
  67. package/dist/cli/__tests__/team.test.js.map +1 -1
  68. package/dist/cli/__tests__/ultragoal.test.d.ts +2 -0
  69. package/dist/cli/__tests__/ultragoal.test.d.ts.map +1 -0
  70. package/dist/cli/__tests__/ultragoal.test.js +106 -0
  71. package/dist/cli/__tests__/ultragoal.test.js.map +1 -0
  72. package/dist/cli/__tests__/uninstall.test.js +11 -0
  73. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  74. package/dist/cli/autoresearch-goal.d.ts +3 -0
  75. package/dist/cli/autoresearch-goal.d.ts.map +1 -0
  76. package/dist/cli/autoresearch-goal.js +175 -0
  77. package/dist/cli/autoresearch-goal.js.map +1 -0
  78. package/dist/cli/cleanup.d.ts +3 -1
  79. package/dist/cli/cleanup.d.ts.map +1 -1
  80. package/dist/cli/cleanup.js +42 -2
  81. package/dist/cli/cleanup.js.map +1 -1
  82. package/dist/cli/doctor.d.ts.map +1 -1
  83. package/dist/cli/doctor.js +49 -0
  84. package/dist/cli/doctor.js.map +1 -1
  85. package/dist/cli/explore.d.ts.map +1 -1
  86. package/dist/cli/explore.js +10 -2
  87. package/dist/cli/explore.js.map +1 -1
  88. package/dist/cli/index.d.ts +6 -2
  89. package/dist/cli/index.d.ts.map +1 -1
  90. package/dist/cli/index.js +145 -18
  91. package/dist/cli/index.js.map +1 -1
  92. package/dist/cli/native-assets.js +1 -1
  93. package/dist/cli/native-assets.js.map +1 -1
  94. package/dist/cli/performance-goal.d.ts +3 -0
  95. package/dist/cli/performance-goal.d.ts.map +1 -0
  96. package/dist/cli/performance-goal.js +186 -0
  97. package/dist/cli/performance-goal.js.map +1 -0
  98. package/dist/cli/ralph.d.ts.map +1 -1
  99. package/dist/cli/ralph.js +8 -0
  100. package/dist/cli/ralph.js.map +1 -1
  101. package/dist/cli/setup.d.ts.map +1 -1
  102. package/dist/cli/setup.js +13 -6
  103. package/dist/cli/setup.js.map +1 -1
  104. package/dist/cli/team.d.ts +2 -0
  105. package/dist/cli/team.d.ts.map +1 -1
  106. package/dist/cli/team.js +72 -17
  107. package/dist/cli/team.js.map +1 -1
  108. package/dist/cli/tmux-hook.d.ts.map +1 -1
  109. package/dist/cli/tmux-hook.js +2 -1
  110. package/dist/cli/tmux-hook.js.map +1 -1
  111. package/dist/cli/ultragoal.d.ts +3 -0
  112. package/dist/cli/ultragoal.d.ts.map +1 -0
  113. package/dist/cli/ultragoal.js +191 -0
  114. package/dist/cli/ultragoal.js.map +1 -0
  115. package/dist/cli/uninstall.d.ts.map +1 -1
  116. package/dist/cli/uninstall.js +4 -2
  117. package/dist/cli/uninstall.js.map +1 -1
  118. package/dist/config/__tests__/generator-idempotent.test.js +12 -1
  119. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  120. package/dist/config/__tests__/generator-notify.test.js +5 -0
  121. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  122. package/dist/config/commit-lore-guard.d.ts +3 -0
  123. package/dist/config/commit-lore-guard.d.ts.map +1 -0
  124. package/dist/config/commit-lore-guard.js +9 -0
  125. package/dist/config/commit-lore-guard.js.map +1 -0
  126. package/dist/config/generator.d.ts +3 -2
  127. package/dist/config/generator.d.ts.map +1 -1
  128. package/dist/config/generator.js +52 -8
  129. package/dist/config/generator.js.map +1 -1
  130. package/dist/config/omx-first-party-mcp.d.ts +1 -0
  131. package/dist/config/omx-first-party-mcp.d.ts.map +1 -1
  132. package/dist/config/omx-first-party-mcp.js +4 -1
  133. package/dist/config/omx-first-party-mcp.js.map +1 -1
  134. package/dist/goal-workflows/__tests__/artifacts.test.d.ts +2 -0
  135. package/dist/goal-workflows/__tests__/artifacts.test.d.ts.map +1 -0
  136. package/dist/goal-workflows/__tests__/artifacts.test.js +96 -0
  137. package/dist/goal-workflows/__tests__/artifacts.test.js.map +1 -0
  138. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.d.ts +2 -0
  139. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.d.ts.map +1 -0
  140. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +54 -0
  141. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -0
  142. package/dist/goal-workflows/artifacts.d.ts +62 -0
  143. package/dist/goal-workflows/artifacts.d.ts.map +1 -0
  144. package/dist/goal-workflows/artifacts.js +132 -0
  145. package/dist/goal-workflows/artifacts.js.map +1 -0
  146. package/dist/goal-workflows/codex-goal-snapshot.d.ts +28 -0
  147. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -0
  148. package/dist/goal-workflows/codex-goal-snapshot.js +110 -0
  149. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -0
  150. package/dist/goal-workflows/handoff.d.ts +10 -0
  151. package/dist/goal-workflows/handoff.d.ts.map +1 -0
  152. package/dist/goal-workflows/handoff.js +31 -0
  153. package/dist/goal-workflows/handoff.js.map +1 -0
  154. package/dist/goal-workflows/validation.d.ts +13 -0
  155. package/dist/goal-workflows/validation.d.ts.map +1 -0
  156. package/dist/goal-workflows/validation.js +36 -0
  157. package/dist/goal-workflows/validation.js.map +1 -0
  158. package/dist/hooks/__tests__/anti-slop-workflow.test.js +3 -3
  159. package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
  160. package/dist/hooks/__tests__/keyword-detector.test.js +45 -32
  161. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  162. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +3 -3
  163. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  164. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +2 -1
  165. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  166. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +17 -24
  167. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  168. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +3 -3
  169. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
  170. package/dist/hooks/__tests__/task-size-detector.test.js +1 -1
  171. package/dist/hooks/__tests__/task-size-detector.test.js.map +1 -1
  172. package/dist/hooks/__tests__/visual-ralph-skill.test.js +3 -3
  173. package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -1
  174. package/dist/hooks/__tests__/visual-verdict-loop.test.js +7 -11
  175. package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +1 -1
  176. package/dist/hooks/agents-overlay.d.ts.map +1 -1
  177. package/dist/hooks/agents-overlay.js +2 -2
  178. package/dist/hooks/agents-overlay.js.map +1 -1
  179. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  180. package/dist/hooks/keyword-detector.js +12 -13
  181. package/dist/hooks/keyword-detector.js.map +1 -1
  182. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  183. package/dist/hooks/keyword-registry.js +2 -10
  184. package/dist/hooks/keyword-registry.js.map +1 -1
  185. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  186. package/dist/hooks/prompt-guidance-contract.js +0 -4
  187. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  188. package/dist/hooks/session.js +2 -2
  189. package/dist/hooks/session.js.map +1 -1
  190. package/dist/hooks/task-size-detector.d.ts.map +1 -1
  191. package/dist/hooks/task-size-detector.js +1 -0
  192. package/dist/hooks/task-size-detector.js.map +1 -1
  193. package/dist/hud/__tests__/reconcile.test.js +29 -7
  194. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  195. package/dist/hud/reconcile.d.ts +2 -1
  196. package/dist/hud/reconcile.d.ts.map +1 -1
  197. package/dist/hud/reconcile.js +12 -0
  198. package/dist/hud/reconcile.js.map +1 -1
  199. package/dist/mcp/__tests__/bootstrap.test.js +15 -2
  200. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  201. package/dist/mcp/__tests__/state-paths.test.js +54 -0
  202. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  203. package/dist/mcp/__tests__/state-server.test.js +36 -0
  204. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  205. package/dist/mcp/bootstrap.d.ts +1 -1
  206. package/dist/mcp/bootstrap.d.ts.map +1 -1
  207. package/dist/mcp/bootstrap.js +9 -7
  208. package/dist/mcp/bootstrap.js.map +1 -1
  209. package/dist/mcp/state-paths.d.ts +17 -0
  210. package/dist/mcp/state-paths.d.ts.map +1 -1
  211. package/dist/mcp/state-paths.js +36 -2
  212. package/dist/mcp/state-paths.js.map +1 -1
  213. package/dist/modes/__tests__/base-session-scope.test.js +26 -0
  214. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  215. package/dist/modes/base.d.ts +1 -0
  216. package/dist/modes/base.d.ts.map +1 -1
  217. package/dist/modes/base.js +35 -5
  218. package/dist/modes/base.js.map +1 -1
  219. package/dist/notifications/__tests__/http-client.test.d.ts +2 -0
  220. package/dist/notifications/__tests__/http-client.test.d.ts.map +1 -0
  221. package/dist/notifications/__tests__/http-client.test.js +90 -0
  222. package/dist/notifications/__tests__/http-client.test.js.map +1 -0
  223. package/dist/notifications/__tests__/notifier.test.js +22 -60
  224. package/dist/notifications/__tests__/notifier.test.js.map +1 -1
  225. package/dist/notifications/dispatcher.d.ts.map +1 -1
  226. package/dist/notifications/dispatcher.js +35 -60
  227. package/dist/notifications/dispatcher.js.map +1 -1
  228. package/dist/notifications/http-client.d.ts +22 -0
  229. package/dist/notifications/http-client.d.ts.map +1 -0
  230. package/dist/notifications/http-client.js +298 -0
  231. package/dist/notifications/http-client.js.map +1 -0
  232. package/dist/notifications/notifier.d.ts +3 -2
  233. package/dist/notifications/notifier.d.ts.map +1 -1
  234. package/dist/notifications/notifier.js +17 -22
  235. package/dist/notifications/notifier.js.map +1 -1
  236. package/dist/openclaw/__tests__/dispatcher.test.js +62 -1
  237. package/dist/openclaw/__tests__/dispatcher.test.js.map +1 -1
  238. package/dist/openclaw/dispatcher.d.ts.map +1 -1
  239. package/dist/openclaw/dispatcher.js +3 -2
  240. package/dist/openclaw/dispatcher.js.map +1 -1
  241. package/dist/performance-goal/artifacts.d.ts +76 -0
  242. package/dist/performance-goal/artifacts.d.ts.map +1 -0
  243. package/dist/performance-goal/artifacts.js +221 -0
  244. package/dist/performance-goal/artifacts.js.map +1 -0
  245. package/dist/pipeline/__tests__/stages.test.js +30 -5
  246. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  247. package/dist/pipeline/stages/team-exec.d.ts.map +1 -1
  248. package/dist/pipeline/stages/team-exec.js +2 -19
  249. package/dist/pipeline/stages/team-exec.js.map +1 -1
  250. package/dist/planning/__tests__/artifacts.test.js +16 -1
  251. package/dist/planning/__tests__/artifacts.test.js.map +1 -1
  252. package/dist/planning/artifacts.d.ts +1 -0
  253. package/dist/planning/artifacts.d.ts.map +1 -1
  254. package/dist/planning/artifacts.js +9 -12
  255. package/dist/planning/artifacts.js.map +1 -1
  256. package/dist/ralplan/__tests__/runtime.test.js +2 -0
  257. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  258. package/dist/ralplan/runtime.d.ts.map +1 -1
  259. package/dist/ralplan/runtime.js +6 -0
  260. package/dist/ralplan/runtime.js.map +1 -1
  261. package/dist/scripts/__tests__/codex-native-hook.test.js +1516 -205
  262. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  263. package/dist/scripts/__tests__/hook-derived-watcher.test.js +33 -1
  264. package/dist/scripts/__tests__/hook-derived-watcher.test.js.map +1 -1
  265. package/dist/scripts/__tests__/run-test-files.test.js +36 -0
  266. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
  267. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  268. package/dist/scripts/codex-native-hook.js +497 -51
  269. package/dist/scripts/codex-native-hook.js.map +1 -1
  270. package/dist/scripts/codex-native-pre-post.d.ts +7 -0
  271. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  272. package/dist/scripts/codex-native-pre-post.js +222 -19
  273. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  274. package/dist/scripts/hook-derived-watcher.js +2 -1
  275. package/dist/scripts/hook-derived-watcher.js.map +1 -1
  276. package/dist/scripts/notify-fallback-watcher.js +2 -1
  277. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  278. package/dist/scripts/notify-hook/orchestration-intent.d.ts +1 -2
  279. package/dist/scripts/notify-hook/orchestration-intent.d.ts.map +1 -1
  280. package/dist/scripts/notify-hook/orchestration-intent.js +2 -3
  281. package/dist/scripts/notify-hook/orchestration-intent.js.map +1 -1
  282. package/dist/scripts/notify-hook/team-leader-nudge.d.ts +0 -2
  283. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  284. package/dist/scripts/notify-hook/team-leader-nudge.js +8 -60
  285. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  286. package/dist/scripts/notify-hook/team-worker-stop.d.ts +15 -0
  287. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -0
  288. package/dist/scripts/notify-hook/team-worker-stop.js +224 -0
  289. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -0
  290. package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
  291. package/dist/scripts/notify-hook/team-worker.js +26 -18
  292. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  293. package/dist/scripts/run-test-files.js +17 -1
  294. package/dist/scripts/run-test-files.js.map +1 -1
  295. package/dist/scripts/sync-plugin-mirror.js +2 -2
  296. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  297. package/dist/state/__tests__/operations.test.js +26 -0
  298. package/dist/state/__tests__/operations.test.js.map +1 -1
  299. package/dist/state/__tests__/skill-active.test.js +35 -0
  300. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  301. package/dist/state/operations.d.ts +3 -1
  302. package/dist/state/operations.d.ts.map +1 -1
  303. package/dist/state/operations.js +8 -4
  304. package/dist/state/operations.js.map +1 -1
  305. package/dist/state/skill-active.d.ts +1 -0
  306. package/dist/state/skill-active.d.ts.map +1 -1
  307. package/dist/state/skill-active.js +54 -13
  308. package/dist/state/skill-active.js.map +1 -1
  309. package/dist/team/__tests__/api-interop.test.js +59 -0
  310. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  311. package/dist/team/__tests__/approved-execution.test.d.ts +2 -0
  312. package/dist/team/__tests__/approved-execution.test.d.ts.map +1 -0
  313. package/dist/team/__tests__/approved-execution.test.js +124 -0
  314. package/dist/team/__tests__/approved-execution.test.js.map +1 -0
  315. package/dist/team/__tests__/delivery-e2e-smoke.test.js +2 -4
  316. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  317. package/dist/team/__tests__/delivery-log.test.d.ts +2 -0
  318. package/dist/team/__tests__/delivery-log.test.d.ts.map +1 -0
  319. package/dist/team/__tests__/delivery-log.test.js +44 -0
  320. package/dist/team/__tests__/delivery-log.test.js.map +1 -0
  321. package/dist/team/__tests__/role-router.test.js +4 -4
  322. package/dist/team/__tests__/role-router.test.js.map +1 -1
  323. package/dist/team/__tests__/runtime-boxed-state.test.d.ts +2 -0
  324. package/dist/team/__tests__/runtime-boxed-state.test.d.ts.map +1 -0
  325. package/dist/team/__tests__/runtime-boxed-state.test.js +39 -0
  326. package/dist/team/__tests__/runtime-boxed-state.test.js.map +1 -0
  327. package/dist/team/__tests__/runtime.test.js +118 -6
  328. package/dist/team/__tests__/runtime.test.js.map +1 -1
  329. package/dist/team/__tests__/state-root.test.js +13 -0
  330. package/dist/team/__tests__/state-root.test.js.map +1 -1
  331. package/dist/team/__tests__/tmux-session.test.js +3 -0
  332. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  333. package/dist/team/__tests__/worker-bootstrap.test.js +50 -0
  334. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  335. package/dist/team/api-interop.d.ts.map +1 -1
  336. package/dist/team/api-interop.js +4 -3
  337. package/dist/team/api-interop.js.map +1 -1
  338. package/dist/team/approved-execution.d.ts +37 -0
  339. package/dist/team/approved-execution.d.ts.map +1 -0
  340. package/dist/team/approved-execution.js +136 -0
  341. package/dist/team/approved-execution.js.map +1 -0
  342. package/dist/team/delivery-log.d.ts.map +1 -1
  343. package/dist/team/delivery-log.js +2 -1
  344. package/dist/team/delivery-log.js.map +1 -1
  345. package/dist/team/followup-planner.js +2 -2
  346. package/dist/team/followup-planner.js.map +1 -1
  347. package/dist/team/goal-workflow.d.ts +20 -0
  348. package/dist/team/goal-workflow.d.ts.map +1 -0
  349. package/dist/team/goal-workflow.js +57 -0
  350. package/dist/team/goal-workflow.js.map +1 -0
  351. package/dist/team/orchestrator.js +2 -2
  352. package/dist/team/orchestrator.js.map +1 -1
  353. package/dist/team/role-router.js +5 -5
  354. package/dist/team/role-router.js.map +1 -1
  355. package/dist/team/runtime.d.ts +6 -0
  356. package/dist/team/runtime.d.ts.map +1 -1
  357. package/dist/team/runtime.js +46 -6
  358. package/dist/team/runtime.js.map +1 -1
  359. package/dist/team/scaling.d.ts.map +1 -1
  360. package/dist/team/scaling.js +2 -0
  361. package/dist/team/scaling.js.map +1 -1
  362. package/dist/team/tmux-session.d.ts.map +1 -1
  363. package/dist/team/tmux-session.js +4 -2
  364. package/dist/team/tmux-session.js.map +1 -1
  365. package/dist/team/worker-bootstrap.d.ts +2 -0
  366. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  367. package/dist/team/worker-bootstrap.js +19 -2
  368. package/dist/team/worker-bootstrap.js.map +1 -1
  369. package/dist/ultragoal/__tests__/artifacts.test.d.ts +2 -0
  370. package/dist/ultragoal/__tests__/artifacts.test.d.ts.map +1 -0
  371. package/dist/ultragoal/__tests__/artifacts.test.js +93 -0
  372. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -0
  373. package/dist/ultragoal/artifacts.d.ts +89 -0
  374. package/dist/ultragoal/artifacts.d.ts.map +1 -0
  375. package/dist/ultragoal/artifacts.js +233 -0
  376. package/dist/ultragoal/artifacts.js.map +1 -0
  377. package/dist/utils/__tests__/agents-model-table.test.js +3 -1
  378. package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
  379. package/dist/utils/__tests__/paths.test.js +31 -1
  380. package/dist/utils/__tests__/paths.test.js.map +1 -1
  381. package/dist/utils/agents-model-table.d.ts.map +1 -1
  382. package/dist/utils/agents-model-table.js +12 -1
  383. package/dist/utils/agents-model-table.js.map +1 -1
  384. package/dist/utils/paths.d.ts +2 -0
  385. package/dist/utils/paths.d.ts.map +1 -1
  386. package/dist/utils/paths.js +23 -7
  387. package/dist/utils/paths.js.map +1 -1
  388. package/dist/verification/__tests__/ci-rust-gates.test.js +30 -19
  389. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  390. package/package.json +5 -5
  391. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  392. package/plugins/oh-my-codex/skills/ask/SKILL.md +58 -0
  393. package/plugins/oh-my-codex/skills/autoresearch-goal/SKILL.md +36 -0
  394. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +2 -2
  395. package/plugins/oh-my-codex/skills/performance-goal/SKILL.md +65 -0
  396. package/plugins/oh-my-codex/skills/plan/SKILL.md +1 -1
  397. package/plugins/oh-my-codex/skills/ralph/SKILL.md +22 -3
  398. package/plugins/oh-my-codex/skills/team/SKILL.md +6 -2
  399. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +49 -0
  400. package/plugins/oh-my-codex/skills/visual-ralph/SKILL.md +9 -9
  401. package/prompts/api-reviewer.md +1 -1
  402. package/prompts/code-reviewer.md +2 -0
  403. package/prompts/performance-reviewer.md +1 -1
  404. package/prompts/quality-reviewer.md +1 -1
  405. package/prompts/quality-strategist.md +2 -2
  406. package/prompts/style-reviewer.md +1 -1
  407. package/prompts/test-engineer.md +1 -1
  408. package/skills/ask/SKILL.md +58 -0
  409. package/skills/ask-claude/SKILL.md +3 -54
  410. package/skills/ask-gemini/SKILL.md +3 -54
  411. package/skills/autoresearch-goal/SKILL.md +36 -0
  412. package/skills/build-fix/SKILL.md +4 -139
  413. package/skills/deepsearch/SKILL.md +4 -32
  414. package/skills/ecomode/SKILL.md +4 -108
  415. package/skills/help/SKILL.md +4 -196
  416. package/skills/note/SKILL.md +4 -56
  417. package/skills/omx-setup/SKILL.md +2 -2
  418. package/skills/performance-goal/SKILL.md +65 -0
  419. package/skills/plan/SKILL.md +1 -1
  420. package/skills/ralph/SKILL.md +22 -3
  421. package/skills/ralph-init/SKILL.md +4 -40
  422. package/skills/review/SKILL.md +4 -32
  423. package/skills/security-review/SKILL.md +4 -294
  424. package/skills/swarm/SKILL.md +4 -19
  425. package/skills/tdd/SKILL.md +4 -100
  426. package/skills/team/SKILL.md +6 -2
  427. package/skills/trace/SKILL.md +4 -27
  428. package/skills/ultragoal/SKILL.md +49 -0
  429. package/skills/visual-ralph/SKILL.md +9 -9
  430. package/skills/visual-verdict/SKILL.md +4 -70
  431. package/skills/web-clone/SKILL.md +4 -18
  432. package/src/scripts/__tests__/codex-native-hook.test.ts +1654 -157
  433. package/src/scripts/__tests__/hook-derived-watcher.test.ts +45 -1
  434. package/src/scripts/__tests__/run-test-files.test.ts +46 -0
  435. package/src/scripts/codex-native-hook.ts +592 -52
  436. package/src/scripts/codex-native-pre-post.ts +252 -20
  437. package/src/scripts/hook-derived-watcher.ts +2 -1
  438. package/src/scripts/notify-fallback-watcher.ts +2 -1
  439. package/src/scripts/notify-hook/orchestration-intent.ts +1 -3
  440. package/src/scripts/notify-hook/team-leader-nudge.ts +7 -63
  441. package/src/scripts/notify-hook/team-worker-stop.ts +246 -0
  442. package/src/scripts/notify-hook/team-worker.ts +23 -14
  443. package/src/scripts/run-test-files.ts +20 -1
  444. package/src/scripts/sync-plugin-mirror.ts +2 -2
  445. package/templates/catalog-manifest.json +45 -27
  446. package/plugins/oh-my-codex/skills/ask-claude/SKILL.md +0 -61
  447. package/plugins/oh-my-codex/skills/ask-gemini/SKILL.md +0 -61
  448. package/plugins/oh-my-codex/skills/help/SKILL.md +0 -202
  449. package/plugins/oh-my-codex/skills/note/SKILL.md +0 -62
  450. package/plugins/oh-my-codex/skills/security-review/SKILL.md +0 -300
  451. package/plugins/oh-my-codex/skills/trace/SKILL.md +0 -33
  452. package/plugins/oh-my-codex/skills/visual-verdict/SKILL.md +0 -76
@@ -23,6 +23,7 @@ import {
23
23
  import { writeSessionStart } from "../../hooks/session.js";
24
24
  import { resetTriageConfigCache } from "../../hooks/triage-config.js";
25
25
  import { executeStateOperation } from "../../state/operations.js";
26
+ import { OMX_TMUX_HUD_OWNER_ENV } from "../../hud/reconcile.js";
26
27
 
27
28
  function nativeHookScriptPath(): string {
28
29
  return join(process.cwd(), "dist", "scripts", "codex-native-hook.js");
@@ -57,6 +58,52 @@ async function writeJson(path: string, value: unknown): Promise<void> {
57
58
  await writeFile(path, JSON.stringify(value, null, 2));
58
59
  }
59
60
 
61
+ function buildWorkerStopFakeTmux(tmuxLogPath: string, options: { failSend?: boolean } = {}): string {
62
+ return `#!/usr/bin/env bash
63
+ set -eu
64
+ echo "$@" >> "${tmuxLogPath}"
65
+ cmd="$1"
66
+ shift || true
67
+ if [[ "$cmd" == "display-message" ]]; then
68
+ fmt=""
69
+ while [[ "$#" -gt 0 ]]; do
70
+ case "$1" in
71
+ -p) ;;
72
+ -t) shift ;;
73
+ *) fmt="$1" ;;
74
+ esac
75
+ shift || true
76
+ done
77
+ case "$fmt" in
78
+ "#{pane_in_mode}") echo "0" ;;
79
+ "#{pane_id}") echo "%42" ;;
80
+ "#{pane_current_path}") pwd ;;
81
+ "#{pane_start_command}") echo "codex" ;;
82
+ "#{pane_current_command}") echo "codex" ;;
83
+ "#S") echo "omx-team-worker-stop" ;;
84
+ *) ;;
85
+ esac
86
+ exit 0
87
+ fi
88
+ if [[ "$cmd" == "capture-pane" ]]; then
89
+ echo "› ready"
90
+ exit 0
91
+ fi
92
+ if [[ "$cmd" == "send-keys" ]]; then
93
+ ${options.failSend ? "exit 1" : "exit 0"}
94
+ fi
95
+ exit 0
96
+ `;
97
+ }
98
+
99
+ async function initTempGitRepo(prefix: string): Promise<string> {
100
+ const cwd = await mkdtemp(join(tmpdir(), prefix));
101
+ execFileSync("git", ["init"], { cwd, stdio: "ignore" });
102
+ execFileSync("git", ["config", "user.email", "test@example.com"], { cwd, stdio: "ignore" });
103
+ execFileSync("git", ["config", "user.name", "Test User"], { cwd, stdio: "ignore" });
104
+ return cwd;
105
+ }
106
+
60
107
  async function writeActiveAutopilotSession(cwd: string, sessionId: string): Promise<void> {
61
108
  await writeJson(join(cwd, ".omx", "state", "session.json"), {
62
109
  session_id: sessionId,
@@ -141,10 +188,12 @@ const TEAM_ENV_KEYS = [
141
188
  "OMX_TEAM_STATE_ROOT",
142
189
  "OMX_TEAM_LEADER_CWD",
143
190
  "OMX_SESSION_ID",
191
+ "SESSION_ID",
144
192
  "OMX_QUESTION_RETURN_PANE",
145
193
  "OMX_LEADER_PANE_ID",
146
194
  "TMUX",
147
195
  "TMUX_PANE",
196
+ "OMX_TMUX_HUD_OWNER",
148
197
  ] as const;
149
198
 
150
199
  const priorTeamEnv = new Map<(typeof TEAM_ENV_KEYS)[number], string | undefined>();
@@ -999,6 +1048,124 @@ describe("codex native hook dispatch", () => {
999
1048
  }
1000
1049
  });
1001
1050
 
1051
+ it("warns completion-like prompts when active goal workflows need Codex snapshot reconciliation", async () => {
1052
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-goal-warning-"));
1053
+ try {
1054
+ await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
1055
+ version: 1,
1056
+ activeGoalId: "G001-demo",
1057
+ goals: [{ id: "G001-demo", status: "in_progress", objective: "Demo goal" }],
1058
+ });
1059
+
1060
+ const result = await dispatchCodexNativeHook({
1061
+ hook_event_name: "UserPromptSubmit",
1062
+ cwd,
1063
+ session_id: "sess-goal-warning",
1064
+ thread_id: "thread-goal-warning",
1065
+ prompt: "complete this goal now",
1066
+ }, { cwd });
1067
+
1068
+ assert.match(JSON.stringify(result.outputJson), /requires Codex goal snapshot reconciliation/);
1069
+ assert.match(JSON.stringify(result.outputJson), /get_goal/);
1070
+ assert.match(JSON.stringify(result.outputJson), /--codex-goal-json/);
1071
+ } finally {
1072
+ await rm(cwd, { recursive: true, force: true });
1073
+ }
1074
+ });
1075
+
1076
+ it("blocks Stop when a completion-like final answer skips active goal snapshot reconciliation", async () => {
1077
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-goal-stop-"));
1078
+ try {
1079
+ await writeJson(join(cwd, ".omx", "goals", "performance", "latency", "state.json"), {
1080
+ version: 1,
1081
+ workflow: "performance-goal",
1082
+ slug: "latency",
1083
+ objective: "Reduce latency",
1084
+ status: "validation_passed",
1085
+ });
1086
+
1087
+ const result = await dispatchCodexNativeHook({
1088
+ hook_event_name: "Stop",
1089
+ cwd,
1090
+ session_id: "sess-goal-stop",
1091
+ thread_id: "thread-goal-stop",
1092
+ last_assistant_message: "Performance goal complete; next call update_goal({status: \"complete\"}).",
1093
+ }, { cwd });
1094
+
1095
+ assert.equal(result.outputJson?.decision, "block");
1096
+ assert.match(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
1097
+ assert.match(JSON.stringify(result.outputJson), /omx performance-goal complete --slug latency/);
1098
+ assert.match(JSON.stringify(result.outputJson), /Hooks must not mutate Codex goal state/);
1099
+ } finally {
1100
+ await rm(cwd, { recursive: true, force: true });
1101
+ }
1102
+ });
1103
+
1104
+ it("treats workflow keywords in native subagent prompt text as literal delegation text", async () => {
1105
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-keyword-literal-"));
1106
+ try {
1107
+ const stateDir = join(cwd, ".omx", "state");
1108
+ const canonicalSessionId = "sess-parent";
1109
+ const leaderNativeSessionId = "native-parent-thread";
1110
+ const childNativeSessionId = "native-child-thread";
1111
+ const nowIso = new Date().toISOString();
1112
+
1113
+ await writeJson(join(stateDir, "session.json"), {
1114
+ session_id: canonicalSessionId,
1115
+ native_session_id: leaderNativeSessionId,
1116
+ });
1117
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
1118
+ schemaVersion: 1,
1119
+ sessions: {
1120
+ [canonicalSessionId]: {
1121
+ session_id: canonicalSessionId,
1122
+ leader_thread_id: leaderNativeSessionId,
1123
+ updated_at: nowIso,
1124
+ threads: {
1125
+ [leaderNativeSessionId]: {
1126
+ thread_id: leaderNativeSessionId,
1127
+ kind: "leader",
1128
+ first_seen_at: nowIso,
1129
+ last_seen_at: nowIso,
1130
+ turn_count: 1,
1131
+ },
1132
+ [childNativeSessionId]: {
1133
+ thread_id: childNativeSessionId,
1134
+ kind: "subagent",
1135
+ first_seen_at: nowIso,
1136
+ last_seen_at: nowIso,
1137
+ turn_count: 1,
1138
+ mode: "architect",
1139
+ },
1140
+ },
1141
+ },
1142
+ },
1143
+ });
1144
+
1145
+ const result = await dispatchCodexNativeHook(
1146
+ {
1147
+ hook_event_name: "UserPromptSubmit",
1148
+ cwd,
1149
+ session_id: childNativeSessionId,
1150
+ thread_id: childNativeSessionId,
1151
+ turn_id: "turn-child-1",
1152
+ prompt: "$ralplan Architect review step. Review the draft plan and return APPROVE or ITERATE.",
1153
+ },
1154
+ { cwd },
1155
+ );
1156
+
1157
+ assert.equal(result.omxEventName, "keyword-detector");
1158
+ assert.equal(result.skillState, null);
1159
+ assert.equal(result.outputJson, null);
1160
+ assert.equal(existsSync(join(stateDir, "skill-active-state.json")), false);
1161
+ assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "skill-active-state.json")), false);
1162
+ assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "ralplan-state.json")), false);
1163
+ assert.equal(existsSync(join(stateDir, "sessions", childNativeSessionId, "ralplan-state.json")), false);
1164
+ } finally {
1165
+ await rm(cwd, { recursive: true, force: true });
1166
+ }
1167
+ });
1168
+
1002
1169
  it("records plugin-prefixed keyword activation from UserPromptSubmit payloads", async () => {
1003
1170
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-plugin-prefixed-"));
1004
1171
  try {
@@ -1028,6 +1195,39 @@ describe("codex native hook dispatch", () => {
1028
1195
  }
1029
1196
  });
1030
1197
 
1198
+ it("records ultragoal prompt skill activation with goal-tool handoff guidance", async () => {
1199
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-"));
1200
+ try {
1201
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1202
+ const result = await dispatchCodexNativeHook(
1203
+ {
1204
+ hook_event_name: "UserPromptSubmit",
1205
+ cwd,
1206
+ session_id: "sess-ultragoal-1",
1207
+ thread_id: "thread-ultragoal-1",
1208
+ turn_id: "turn-ultragoal-1",
1209
+ prompt: "$ultragoal split this launch into durable goals",
1210
+ },
1211
+ { cwd },
1212
+ );
1213
+
1214
+ assert.equal(result.omxEventName, "keyword-detector");
1215
+ assert.equal(result.skillState?.skill, "ultragoal");
1216
+ assert.equal(result.skillState?.initialized_mode, undefined);
1217
+ const message = String(
1218
+ (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
1219
+ );
1220
+ assert.match(message, /"\$ultragoal" -> ultragoal/);
1221
+ assert.match(message, /Ultragoal protocol:/);
1222
+ assert.match(message, /get_goal/);
1223
+ assert.match(message, /create_goal/);
1224
+ assert.match(message, /update_goal/);
1225
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ultragoal-1", "ultragoal-state.json")), false);
1226
+ } finally {
1227
+ await rm(cwd, { recursive: true, force: true });
1228
+ }
1229
+ });
1230
+
1031
1231
  it("normalizes the Korean keyboard typo for ulw during UserPromptSubmit activation", async () => {
1032
1232
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ulw-ko-"));
1033
1233
  try {
@@ -1362,7 +1562,6 @@ describe("codex native hook dispatch", () => {
1362
1562
  }
1363
1563
  });
1364
1564
 
1365
-
1366
1565
  it("includes leader-pane preservation guidance when a pane hint is available", async () => {
1367
1566
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-pane-hint-"));
1368
1567
  try {
@@ -1502,7 +1701,6 @@ describe("codex native hook dispatch", () => {
1502
1701
  }
1503
1702
  });
1504
1703
 
1505
-
1506
1704
  it("ignores generic wrapper fields so metadata cannot trigger workflow routing or Stop blocking", async () => {
1507
1705
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-wrapper-metadata-"));
1508
1706
  try {
@@ -1916,10 +2114,12 @@ export async function onHookEvent(event) {
1916
2114
  const originalTmux = process.env.TMUX;
1917
2115
  const originalTmuxPane = process.env.TMUX_PANE;
1918
2116
  const originalPath = process.env.PATH;
2117
+ const originalHudOwner = process.env[OMX_TMUX_HUD_OWNER_ENV];
1919
2118
  const originalArgv = process.argv;
1920
2119
  try {
1921
2120
  process.env.TMUX = "1";
1922
2121
  process.env.TMUX_PANE = "%1";
2122
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = "1";
1923
2123
  await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1924
2124
  await writeFile(
1925
2125
  join(cwd, ".omx", "hud-config.json"),
@@ -1980,12 +2180,64 @@ esac
1980
2180
  } else {
1981
2181
  process.env.TMUX_PANE = originalTmuxPane;
1982
2182
  }
2183
+ if (originalHudOwner === undefined) {
2184
+ delete process.env[OMX_TMUX_HUD_OWNER_ENV];
2185
+ } else {
2186
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = originalHudOwner;
2187
+ }
1983
2188
  process.env.PATH = originalPath;
1984
2189
  process.argv = originalArgv;
1985
2190
  await rm(cwd, { recursive: true, force: true });
1986
2191
  }
1987
2192
  });
1988
2193
 
2194
+ it("skips prompt-submit HUD reconciliation inside unowned tmux panes", async () => {
2195
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-unowned-"));
2196
+ const originalTmux = process.env.TMUX;
2197
+ const originalTmuxPane = process.env.TMUX_PANE;
2198
+ const originalPath = process.env.PATH;
2199
+ const originalHudOwner = process.env[OMX_TMUX_HUD_OWNER_ENV];
2200
+ try {
2201
+ process.env.TMUX = "1";
2202
+ process.env.TMUX_PANE = "%claude";
2203
+ delete process.env[OMX_TMUX_HUD_OWNER_ENV];
2204
+
2205
+ const binDir = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-unowned-bin-"));
2206
+ const tmuxLog = join(cwd, "tmux.log");
2207
+ await writeFile(
2208
+ join(binDir, "tmux"),
2209
+ `#!/usr/bin/env bash
2210
+ printf '%s\n' "$*" >> ${JSON.stringify(tmuxLog)}
2211
+ exit 0
2212
+ `,
2213
+ );
2214
+ await chmod(join(binDir, "tmux"), 0o755);
2215
+ process.env.PATH = `${binDir}:${originalPath}`;
2216
+
2217
+ const result = await dispatchCodexNativeHook(
2218
+ {
2219
+ hook_event_name: "UserPromptSubmit",
2220
+ cwd,
2221
+ session_id: "sess-hud-unowned",
2222
+ prompt: "$ralplan prepare plan",
2223
+ },
2224
+ { cwd },
2225
+ );
2226
+
2227
+ assert.equal(result.omxEventName, "keyword-detector");
2228
+ assert.equal(existsSync(tmuxLog), false);
2229
+ } finally {
2230
+ if (originalTmux === undefined) delete process.env.TMUX;
2231
+ else process.env.TMUX = originalTmux;
2232
+ if (originalTmuxPane === undefined) delete process.env.TMUX_PANE;
2233
+ else process.env.TMUX_PANE = originalTmuxPane;
2234
+ if (originalHudOwner === undefined) delete process.env[OMX_TMUX_HUD_OWNER_ENV];
2235
+ else process.env[OMX_TMUX_HUD_OWNER_ENV] = originalHudOwner;
2236
+ process.env.PATH = originalPath;
2237
+ await rm(cwd, { recursive: true, force: true });
2238
+ }
2239
+ });
2240
+
1989
2241
  it("blocks Bash omx question when no leader-pane return hint is preserved", async () => {
1990
2242
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-enforce-"));
1991
2243
  try {
@@ -2538,124 +2790,595 @@ esac
2538
2790
  }
2539
2791
  });
2540
2792
 
2541
- it("keeps git commit Lore enforcement ahead of sloppy fallback advisory", async () => {
2542
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-git-priority-"));
2793
+ it("blocks Stop for untracked non-Bash-style sloppy fallback source edits", async () => {
2794
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-untracked-");
2543
2795
  try {
2796
+ await mkdir(join(cwd, "src"), { recursive: true });
2797
+ await writeFile(
2798
+ join(cwd, "src", "runtime.ts"),
2799
+ [
2800
+ "export function loadRuntime() {",
2801
+ " // implement a quick hack fallback if it fails",
2802
+ " return process.env.RUNTIME || 'local';",
2803
+ "}",
2804
+ ].join("\n"),
2805
+ );
2806
+
2544
2807
  const result = await dispatchCodexNativeHook(
2545
- {
2546
- hook_event_name: "PreToolUse",
2547
- cwd,
2548
- tool_name: "Bash",
2549
- tool_use_id: "tool-slop-git-priority",
2550
- tool_input: { command: 'git commit -m "quick hack fallback if it fails"' },
2551
- },
2808
+ { hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-untracked" },
2552
2809
  { cwd },
2553
2810
  );
2554
2811
 
2555
- assert.equal(result.omxEventName, "pre-tool-use");
2812
+ assert.equal(result.omxEventName, "stop");
2556
2813
  assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
2557
- assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
2558
- assert.doesNotMatch(JSON.stringify(result.outputJson), /don't make potential slop/);
2814
+ assert.equal((result.outputJson as { stopReason?: string } | null)?.stopReason, "sloppy_fallback_diff_audit");
2815
+ assert.match(JSON.stringify(result.outputJson), /src\/runtime\.ts/);
2816
+ assert.match(JSON.stringify(result.outputJson), /grounded design/);
2559
2817
  } finally {
2560
2818
  await rm(cwd, { recursive: true, force: true });
2561
2819
  }
2562
2820
  });
2563
2821
 
2564
- it("blocks PreToolUse git commit with supported response shape when the inline message is not Lore-compliant", async () => {
2565
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-invalid-"));
2822
+ it("keeps blocking repeated Stop while sloppy fallback diff remains", async () => {
2823
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-repeat-");
2824
+ try {
2825
+ await mkdir(join(cwd, "src"), { recursive: true });
2826
+ await writeFile(
2827
+ join(cwd, "src", "runtime.ts"),
2828
+ [
2829
+ "export function loadRuntime() {",
2830
+ " // implement a quick hack fallback if it fails",
2831
+ " return process.env.RUNTIME || 'local';",
2832
+ "}",
2833
+ ].join("\n"),
2834
+ );
2835
+ const payload = { hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-repeat", turn_id: "turn-repeat" };
2836
+
2837
+ const first = await dispatchCodexNativeHook(payload, { cwd });
2838
+ const repeated = await dispatchCodexNativeHook({ ...payload, stop_hook_active: true }, { cwd });
2839
+
2840
+ assert.equal((first.outputJson as { decision?: string } | null)?.decision, "block");
2841
+ assert.equal((repeated.outputJson as { decision?: string } | null)?.decision, "block");
2842
+ assert.equal((repeated.outputJson as { stopReason?: string } | null)?.stopReason, "sloppy_fallback_diff_audit");
2843
+ } finally {
2844
+ await rm(cwd, { recursive: true, force: true });
2845
+ }
2846
+ });
2847
+
2848
+ it("blocks Stop for unstaged tracked sloppy fallback source edits", async () => {
2849
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-unstaged-");
2566
2850
  try {
2851
+ await mkdir(join(cwd, "src"), { recursive: true });
2852
+ await writeFile(join(cwd, "src", "runtime.ts"), "export const runtime = 'base';\n");
2853
+ execFileSync("git", ["add", "src/runtime.ts"], { cwd, stdio: "ignore" });
2854
+ execFileSync("git", ["commit", "-m", "initial"], { cwd, stdio: "ignore" });
2855
+ await writeFile(
2856
+ join(cwd, "src", "runtime.ts"),
2857
+ [
2858
+ "export function loadRuntime() {",
2859
+ " // just bypass fallback if it fails",
2860
+ " return process.env.RUNTIME || 'local';",
2861
+ "}",
2862
+ ].join("\n"),
2863
+ );
2864
+
2567
2865
  const result = await dispatchCodexNativeHook(
2568
- {
2569
- hook_event_name: "PreToolUse",
2570
- cwd,
2571
- tool_name: "Bash",
2572
- tool_use_id: "tool-git-commit-invalid",
2573
- tool_input: { command: 'git commit -m "fix tests"' },
2574
- },
2866
+ { hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-unstaged" },
2575
2867
  { cwd },
2576
2868
  );
2577
2869
 
2578
- assert.equal(result.omxEventName, "pre-tool-use");
2579
- assert.deepEqual(result.outputJson, {
2580
- decision: "block",
2581
- reason:
2582
- "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
2583
- hookSpecificOutput: {
2584
- hookEventName: "PreToolUse",
2585
- },
2586
- systemMessage: [
2587
- "git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
2588
- "- Add a blank line after the subject before the narrative body.",
2589
- "- Add a narrative body paragraph explaining the decision context.",
2590
- "- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
2591
- "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
2870
+ assert.equal(result.omxEventName, "stop");
2871
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
2872
+ assert.match(JSON.stringify(result.outputJson), /unstaged/);
2873
+ } finally {
2874
+ await rm(cwd, { recursive: true, force: true });
2875
+ }
2876
+ });
2877
+
2878
+ it("blocks Stop from a subdirectory cwd for untracked sloppy source elsewhere", async () => {
2879
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-subdir-");
2880
+ try {
2881
+ await mkdir(join(cwd, "src", "nested"), { recursive: true });
2882
+ await writeFile(join(cwd, "src", "nested", "anchor.ts"), "export const anchor = true;\n");
2883
+ await writeFile(
2884
+ join(cwd, "src", "runtime.ts"),
2885
+ [
2886
+ "export function loadRuntime() {",
2887
+ " // implement a quick hack fallback if it fails",
2888
+ " return process.env.RUNTIME || 'local';",
2889
+ "}",
2592
2890
  ].join("\n"),
2593
- });
2594
- const hookSpecificOutput = (result.outputJson as { hookSpecificOutput?: Record<string, unknown> })
2595
- .hookSpecificOutput ?? {};
2596
- assert.equal("additionalContext" in hookSpecificOutput, false);
2891
+ );
2892
+
2893
+ const subdir = join(cwd, "src", "nested");
2894
+ const result = await dispatchCodexNativeHook(
2895
+ { hook_event_name: "Stop", cwd: subdir, session_id: "sess-stop-slop-subdir" },
2896
+ { cwd: subdir },
2897
+ );
2898
+
2899
+ assert.equal(result.omxEventName, "stop");
2900
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
2901
+ assert.match(JSON.stringify(result.outputJson), /src\/runtime\.ts/);
2597
2902
  } finally {
2598
2903
  await rm(cwd, { recursive: true, force: true });
2599
2904
  }
2600
2905
  });
2601
2906
 
2602
- it("stays silent on PreToolUse for `git help commit`", async () => {
2603
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-help-commit-"));
2907
+ it("blocks Stop for staged sloppy fallback source edits", async () => {
2908
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-staged-");
2604
2909
  try {
2910
+ await mkdir(join(cwd, "src"), { recursive: true });
2911
+ await writeFile(join(cwd, "src", "runtime.ts"), "export const runtime = 'base';\n");
2912
+ execFileSync("git", ["add", "src/runtime.ts"], { cwd, stdio: "ignore" });
2913
+ execFileSync("git", ["commit", "-m", "initial"], { cwd, stdio: "ignore" });
2914
+ await writeFile(
2915
+ join(cwd, "src", "runtime.ts"),
2916
+ [
2917
+ "export function loadRuntime() {",
2918
+ " // temporary workaround fallback if it fails",
2919
+ " return process.env.RUNTIME || 'local';",
2920
+ "}",
2921
+ ].join("\n"),
2922
+ );
2923
+ execFileSync("git", ["add", "src/runtime.ts"], { cwd, stdio: "ignore" });
2924
+
2605
2925
  const result = await dispatchCodexNativeHook(
2606
- {
2607
- hook_event_name: "PreToolUse",
2608
- cwd,
2609
- tool_name: "Bash",
2610
- tool_use_id: "tool-git-help-commit",
2611
- tool_input: { command: "git help commit" },
2612
- },
2926
+ { hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-staged" },
2613
2927
  { cwd },
2614
2928
  );
2615
2929
 
2616
- assert.equal(result.omxEventName, "pre-tool-use");
2617
- assert.equal(result.outputJson, null);
2930
+ assert.equal(result.omxEventName, "stop");
2931
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
2932
+ assert.match(JSON.stringify(result.outputJson), /staged/);
2618
2933
  } finally {
2619
2934
  await rm(cwd, { recursive: true, force: true });
2620
2935
  }
2621
2936
  });
2622
2937
 
2623
- it("stays silent on PreToolUse for `git config alias.ci commit`", async () => {
2624
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-config-alias-commit-"));
2938
+ it("does not block Stop for grounded compatibility fallback source edits", async () => {
2939
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-grounded-");
2625
2940
  try {
2941
+ await mkdir(join(cwd, "src"), { recursive: true });
2942
+ await writeFile(
2943
+ join(cwd, "src", "compat.ts"),
2944
+ [
2945
+ "export function resolveCompatMode() {",
2946
+ " // temporary fallback for legacy startup",
2947
+ " // compatibility fail-safe tested by regression coverage",
2948
+ " return 'legacy';",
2949
+ "}",
2950
+ ].join("\n"),
2951
+ );
2952
+
2626
2953
  const result = await dispatchCodexNativeHook(
2627
- {
2628
- hook_event_name: "PreToolUse",
2629
- cwd,
2630
- tool_name: "Bash",
2631
- tool_use_id: "tool-git-config-alias-commit",
2632
- tool_input: { command: "git config alias.ci commit" },
2633
- },
2954
+ { hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-grounded" },
2634
2955
  { cwd },
2635
2956
  );
2636
2957
 
2637
- assert.equal(result.omxEventName, "pre-tool-use");
2958
+ assert.equal(result.omxEventName, "stop");
2638
2959
  assert.equal(result.outputJson, null);
2639
2960
  } finally {
2640
2961
  await rm(cwd, { recursive: true, force: true });
2641
2962
  }
2642
2963
  });
2643
2964
 
2644
- it("stays silent on PreToolUse for `git tag commit`", async () => {
2645
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-tag-commit-"));
2965
+ it("does not block Stop when existing nearby source context grounds a new fallback line", async () => {
2966
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-existing-ground-");
2646
2967
  try {
2968
+ await mkdir(join(cwd, "src"), { recursive: true });
2969
+ await writeFile(
2970
+ join(cwd, "src", "compat.ts"),
2971
+ [
2972
+ "export function resolveCompatMode() {",
2973
+ " // compatibility fail-safe tested by regression coverage",
2974
+ " return 'legacy';",
2975
+ "}",
2976
+ ].join("\n"),
2977
+ );
2978
+ execFileSync("git", ["add", "src/compat.ts"], { cwd, stdio: "ignore" });
2979
+ execFileSync("git", ["commit", "-m", "initial"], { cwd, stdio: "ignore" });
2980
+ await writeFile(
2981
+ join(cwd, "src", "compat.ts"),
2982
+ [
2983
+ "export function resolveCompatMode() {",
2984
+ " // compatibility fail-safe tested by regression coverage",
2985
+ " // temporary fallback if it fails",
2986
+ " return 'legacy';",
2987
+ "}",
2988
+ ].join("\n"),
2989
+ );
2990
+
2647
2991
  const result = await dispatchCodexNativeHook(
2648
- {
2649
- hook_event_name: "PreToolUse",
2650
- cwd,
2651
- tool_name: "Bash",
2652
- tool_use_id: "tool-git-tag-commit",
2653
- tool_input: { command: "git tag commit" },
2654
- },
2992
+ { hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-existing-ground" },
2655
2993
  { cwd },
2656
2994
  );
2657
2995
 
2658
- assert.equal(result.omxEventName, "pre-tool-use");
2996
+ assert.equal(result.omxEventName, "stop");
2997
+ assert.equal(result.outputJson, null);
2998
+ } finally {
2999
+ await rm(cwd, { recursive: true, force: true });
3000
+ }
3001
+ });
3002
+
3003
+ it("does not block Stop for source-adjacent test file fallback wording", async () => {
3004
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-test-file-");
3005
+ try {
3006
+ await mkdir(join(cwd, "src"), { recursive: true });
3007
+ await writeFile(
3008
+ join(cwd, "src", "runtime.test.ts"),
3009
+ "it('documents no quick hack fallback if it fails', () => {});\n",
3010
+ );
3011
+
3012
+ const result = await dispatchCodexNativeHook(
3013
+ { hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-test-file" },
3014
+ { cwd },
3015
+ );
3016
+
3017
+ assert.equal(result.omxEventName, "stop");
3018
+ assert.equal(result.outputJson, null);
3019
+ } finally {
3020
+ await rm(cwd, { recursive: true, force: true });
3021
+ }
3022
+ });
3023
+
3024
+ it("does not block Stop for docs-only fallback wording", async () => {
3025
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-docs-");
3026
+ try {
3027
+ await mkdir(join(cwd, "docs"), { recursive: true });
3028
+ await writeFile(
3029
+ join(cwd, "docs", "notes.md"),
3030
+ "Do not implement a quick hack fallback if it fails.\n",
3031
+ );
3032
+
3033
+ const result = await dispatchCodexNativeHook(
3034
+ { hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-docs" },
3035
+ { cwd },
3036
+ );
3037
+
3038
+ assert.equal(result.omxEventName, "stop");
3039
+ assert.equal(result.outputJson, null);
3040
+ } finally {
3041
+ await rm(cwd, { recursive: true, force: true });
3042
+ }
3043
+ });
3044
+
3045
+ it("keeps git commit Lore enforcement ahead of sloppy fallback advisory", async () => {
3046
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-git-priority-"));
3047
+ try {
3048
+ const result = await dispatchCodexNativeHook(
3049
+ {
3050
+ hook_event_name: "PreToolUse",
3051
+ cwd,
3052
+ tool_name: "Bash",
3053
+ tool_use_id: "tool-slop-git-priority",
3054
+ tool_input: { command: 'git commit -m "quick hack fallback if it fails"' },
3055
+ },
3056
+ { cwd },
3057
+ );
3058
+
3059
+ assert.equal(result.omxEventName, "pre-tool-use");
3060
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
3061
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
3062
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /don't make potential slop/);
3063
+ } finally {
3064
+ await rm(cwd, { recursive: true, force: true });
3065
+ }
3066
+ });
3067
+
3068
+ it("blocks PreToolUse git commit with supported response shape when the inline message is not Lore-compliant", async () => {
3069
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-invalid-"));
3070
+ try {
3071
+ const result = await dispatchCodexNativeHook(
3072
+ {
3073
+ hook_event_name: "PreToolUse",
3074
+ cwd,
3075
+ tool_name: "Bash",
3076
+ tool_use_id: "tool-git-commit-invalid",
3077
+ tool_input: { command: 'git commit -m "fix tests"' },
3078
+ },
3079
+ { cwd },
3080
+ );
3081
+
3082
+ assert.equal(result.omxEventName, "pre-tool-use");
3083
+ assert.deepEqual(result.outputJson, {
3084
+ decision: "block",
3085
+ reason:
3086
+ "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
3087
+ hookSpecificOutput: {
3088
+ hookEventName: "PreToolUse",
3089
+ },
3090
+ systemMessage: [
3091
+ "git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
3092
+ "- Add a blank line after the subject before the narrative body.",
3093
+ "- Add a narrative body paragraph explaining the decision context.",
3094
+ "- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
3095
+ "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
3096
+ ].join("\n"),
3097
+ });
3098
+ const hookSpecificOutput = (result.outputJson as { hookSpecificOutput?: Record<string, unknown> })
3099
+ .hookSpecificOutput ?? {};
3100
+ assert.equal("additionalContext" in hookSpecificOutput, false);
3101
+ } finally {
3102
+ await rm(cwd, { recursive: true, force: true });
3103
+ }
3104
+ });
3105
+
3106
+ it("allows non-Lore git commit messages when the Lore commit guard is explicitly disabled", async () => {
3107
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-disabled-"));
3108
+ const original = process.env.OMX_LORE_COMMIT_GUARD;
3109
+ try {
3110
+ process.env.OMX_LORE_COMMIT_GUARD = "0";
3111
+ const result = await dispatchCodexNativeHook(
3112
+ {
3113
+ hook_event_name: "PreToolUse",
3114
+ cwd,
3115
+ tool_name: "Bash",
3116
+ tool_use_id: "tool-git-commit-lore-disabled",
3117
+ tool_input: { command: 'git commit -m "fix: use conventional commit"' },
3118
+ },
3119
+ { cwd },
3120
+ );
3121
+
3122
+ assert.equal(result.omxEventName, "pre-tool-use");
3123
+ assert.equal(result.outputJson, null);
3124
+ } finally {
3125
+ if (original === undefined) delete process.env.OMX_LORE_COMMIT_GUARD;
3126
+ else process.env.OMX_LORE_COMMIT_GUARD = original;
3127
+ await rm(cwd, { recursive: true, force: true });
3128
+ }
3129
+ });
3130
+
3131
+ it("allows non-Lore git commit messages when the Lore commit guard is disabled inline", async () => {
3132
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-inline-disabled-"));
3133
+ try {
3134
+ const result = await dispatchCodexNativeHook(
3135
+ {
3136
+ hook_event_name: "PreToolUse",
3137
+ cwd,
3138
+ tool_name: "Bash",
3139
+ tool_use_id: "tool-git-commit-lore-inline-disabled",
3140
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=0 git commit -m "fix: conventional"' },
3141
+ },
3142
+ { cwd },
3143
+ );
3144
+
3145
+ assert.equal(result.omxEventName, "pre-tool-use");
3146
+ assert.equal(result.outputJson, null);
3147
+ } finally {
3148
+ await rm(cwd, { recursive: true, force: true });
3149
+ }
3150
+ });
3151
+
3152
+ it("does not treat newline-separated Lore guard assignment as inline git commit env", async () => {
3153
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-newline-assignment-"));
3154
+ try {
3155
+ const result = await dispatchCodexNativeHook(
3156
+ {
3157
+ hook_event_name: "PreToolUse",
3158
+ cwd,
3159
+ tool_name: "Bash",
3160
+ tool_use_id: "tool-git-commit-lore-newline-assignment",
3161
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=0\ngit commit -m "fix: conventional"' },
3162
+ },
3163
+ { cwd },
3164
+ );
3165
+
3166
+ assert.equal(result.omxEventName, "pre-tool-use");
3167
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
3168
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
3169
+ } finally {
3170
+ await rm(cwd, { recursive: true, force: true });
3171
+ }
3172
+ });
3173
+
3174
+ it("restores default-on Lore guard when env -u unsets a disabled process env", async () => {
3175
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-env-unset-"));
3176
+ const original = process.env.OMX_LORE_COMMIT_GUARD;
3177
+ try {
3178
+ process.env.OMX_LORE_COMMIT_GUARD = "0";
3179
+ const result = await dispatchCodexNativeHook(
3180
+ {
3181
+ hook_event_name: "PreToolUse",
3182
+ cwd,
3183
+ tool_name: "Bash",
3184
+ tool_use_id: "tool-git-commit-lore-env-unset",
3185
+ tool_input: { command: 'env -u OMX_LORE_COMMIT_GUARD git commit -m "fix: conventional"' },
3186
+ },
3187
+ { cwd },
3188
+ );
3189
+
3190
+ assert.equal(result.omxEventName, "pre-tool-use");
3191
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
3192
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
3193
+ } finally {
3194
+ if (original === undefined) delete process.env.OMX_LORE_COMMIT_GUARD;
3195
+ else process.env.OMX_LORE_COMMIT_GUARD = original;
3196
+ await rm(cwd, { recursive: true, force: true });
3197
+ }
3198
+ });
3199
+
3200
+ it("restores default-on Lore guard when env -i clears a disabled process env", async () => {
3201
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-env-ignore-"));
3202
+ const original = process.env.OMX_LORE_COMMIT_GUARD;
3203
+ try {
3204
+ process.env.OMX_LORE_COMMIT_GUARD = "0";
3205
+ const result = await dispatchCodexNativeHook(
3206
+ {
3207
+ hook_event_name: "PreToolUse",
3208
+ cwd,
3209
+ tool_name: "Bash",
3210
+ tool_use_id: "tool-git-commit-lore-env-ignore",
3211
+ tool_input: { command: 'env -i PATH=/usr/bin git commit -m "fix: conventional"' },
3212
+ },
3213
+ { cwd },
3214
+ );
3215
+
3216
+ assert.equal(result.omxEventName, "pre-tool-use");
3217
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
3218
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
3219
+ } finally {
3220
+ if (original === undefined) delete process.env.OMX_LORE_COMMIT_GUARD;
3221
+ else process.env.OMX_LORE_COMMIT_GUARD = original;
3222
+ await rm(cwd, { recursive: true, force: true });
3223
+ }
3224
+ });
3225
+
3226
+ it("keeps Lore commit enforcement enabled for unknown inline guard values", async () => {
3227
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-inline-unknown-"));
3228
+ try {
3229
+ const result = await dispatchCodexNativeHook(
3230
+ {
3231
+ hook_event_name: "PreToolUse",
3232
+ cwd,
3233
+ tool_name: "Bash",
3234
+ tool_use_id: "tool-git-commit-lore-inline-unknown",
3235
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=maybe git commit -m "fix: conventional"' },
3236
+ },
3237
+ { cwd },
3238
+ );
3239
+
3240
+ assert.equal(result.omxEventName, "pre-tool-use");
3241
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
3242
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
3243
+ } finally {
3244
+ await rm(cwd, { recursive: true, force: true });
3245
+ }
3246
+ });
3247
+
3248
+ it("treats Lore commit guard disabled values as trim and case tolerant", async () => {
3249
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-off-"));
3250
+ const original = process.env.OMX_LORE_COMMIT_GUARD;
3251
+ try {
3252
+ process.env.OMX_LORE_COMMIT_GUARD = " OFF ";
3253
+ const result = await dispatchCodexNativeHook(
3254
+ {
3255
+ hook_event_name: "PreToolUse",
3256
+ cwd,
3257
+ tool_name: "Bash",
3258
+ tool_use_id: "tool-git-commit-lore-off",
3259
+ tool_input: { command: 'git commit -m "chore: conventional commit"' },
3260
+ },
3261
+ { cwd },
3262
+ );
3263
+
3264
+ assert.equal(result.omxEventName, "pre-tool-use");
3265
+ assert.equal(result.outputJson, null);
3266
+ } finally {
3267
+ if (original === undefined) delete process.env.OMX_LORE_COMMIT_GUARD;
3268
+ else process.env.OMX_LORE_COMMIT_GUARD = original;
3269
+ await rm(cwd, { recursive: true, force: true });
3270
+ }
3271
+ });
3272
+
3273
+ it("keeps Lore commit enforcement enabled for unknown guard values", async () => {
3274
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-unknown-"));
3275
+ const original = process.env.OMX_LORE_COMMIT_GUARD;
3276
+ try {
3277
+ process.env.OMX_LORE_COMMIT_GUARD = "maybe";
3278
+ const result = await dispatchCodexNativeHook(
3279
+ {
3280
+ hook_event_name: "PreToolUse",
3281
+ cwd,
3282
+ tool_name: "Bash",
3283
+ tool_use_id: "tool-git-commit-lore-unknown",
3284
+ tool_input: { command: 'git commit -m "fix tests"' },
3285
+ },
3286
+ { cwd },
3287
+ );
3288
+
3289
+ assert.equal(result.omxEventName, "pre-tool-use");
3290
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
3291
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
3292
+ } finally {
3293
+ if (original === undefined) delete process.env.OMX_LORE_COMMIT_GUARD;
3294
+ else process.env.OMX_LORE_COMMIT_GUARD = original;
3295
+ await rm(cwd, { recursive: true, force: true });
3296
+ }
3297
+ });
3298
+
3299
+ it("continues to later PreToolUse checks when Lore commit guard is disabled", async () => {
3300
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-lore-disabled-destructive-"));
3301
+ const original = process.env.OMX_LORE_COMMIT_GUARD;
3302
+ try {
3303
+ process.env.OMX_LORE_COMMIT_GUARD = "false";
3304
+ const result = await dispatchCodexNativeHook(
3305
+ {
3306
+ hook_event_name: "PreToolUse",
3307
+ cwd,
3308
+ tool_name: "Bash",
3309
+ tool_use_id: "tool-lore-disabled-destructive",
3310
+ tool_input: { command: "rm -rf dist" },
3311
+ },
3312
+ { cwd },
3313
+ );
3314
+
3315
+ assert.equal(result.omxEventName, "pre-tool-use");
3316
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /Lore protocol/);
3317
+ assert.match(JSON.stringify(result.outputJson), /Destructive Bash command detected/);
3318
+ } finally {
3319
+ if (original === undefined) delete process.env.OMX_LORE_COMMIT_GUARD;
3320
+ else process.env.OMX_LORE_COMMIT_GUARD = original;
3321
+ await rm(cwd, { recursive: true, force: true });
3322
+ }
3323
+ });
3324
+
3325
+ it("stays silent on PreToolUse for `git help commit`", async () => {
3326
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-help-commit-"));
3327
+ try {
3328
+ const result = await dispatchCodexNativeHook(
3329
+ {
3330
+ hook_event_name: "PreToolUse",
3331
+ cwd,
3332
+ tool_name: "Bash",
3333
+ tool_use_id: "tool-git-help-commit",
3334
+ tool_input: { command: "git help commit" },
3335
+ },
3336
+ { cwd },
3337
+ );
3338
+
3339
+ assert.equal(result.omxEventName, "pre-tool-use");
3340
+ assert.equal(result.outputJson, null);
3341
+ } finally {
3342
+ await rm(cwd, { recursive: true, force: true });
3343
+ }
3344
+ });
3345
+
3346
+ it("stays silent on PreToolUse for `git config alias.ci commit`", async () => {
3347
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-config-alias-commit-"));
3348
+ try {
3349
+ const result = await dispatchCodexNativeHook(
3350
+ {
3351
+ hook_event_name: "PreToolUse",
3352
+ cwd,
3353
+ tool_name: "Bash",
3354
+ tool_use_id: "tool-git-config-alias-commit",
3355
+ tool_input: { command: "git config alias.ci commit" },
3356
+ },
3357
+ { cwd },
3358
+ );
3359
+
3360
+ assert.equal(result.omxEventName, "pre-tool-use");
3361
+ assert.equal(result.outputJson, null);
3362
+ } finally {
3363
+ await rm(cwd, { recursive: true, force: true });
3364
+ }
3365
+ });
3366
+
3367
+ it("stays silent on PreToolUse for `git tag commit`", async () => {
3368
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-tag-commit-"));
3369
+ try {
3370
+ const result = await dispatchCodexNativeHook(
3371
+ {
3372
+ hook_event_name: "PreToolUse",
3373
+ cwd,
3374
+ tool_name: "Bash",
3375
+ tool_use_id: "tool-git-tag-commit",
3376
+ tool_input: { command: "git tag commit" },
3377
+ },
3378
+ { cwd },
3379
+ );
3380
+
3381
+ assert.equal(result.omxEventName, "pre-tool-use");
2659
3382
  assert.equal(result.outputJson, null);
2660
3383
  } finally {
2661
3384
  await rm(cwd, { recursive: true, force: true });
@@ -3837,12 +4560,49 @@ esac
3837
4560
  }
3838
4561
  });
3839
4562
 
4563
+ for (const rootActiveCase of [
4564
+ { mode: "autopilot", phase: "execution" },
4565
+ { mode: "ultrawork", phase: "executing" },
4566
+ { mode: "ultraqa", phase: "diagnose" },
4567
+ ] as const) {
4568
+ it(`returns Stop continuation output from root ${rootActiveCase.mode} state when no session is active`, async () => {
4569
+ const cwd = await mkdtemp(join(tmpdir(), `omx-native-hook-stop-root-${rootActiveCase.mode}-`));
4570
+ try {
4571
+ const stateDir = join(cwd, ".omx", "state");
4572
+ await mkdir(stateDir, { recursive: true });
4573
+ await writeJson(join(stateDir, `${rootActiveCase.mode}-state.json`), {
4574
+ active: true,
4575
+ mode: rootActiveCase.mode,
4576
+ current_phase: rootActiveCase.phase,
4577
+ });
4578
+
4579
+ const result = await dispatchCodexNativeHook(
4580
+ {
4581
+ hook_event_name: "Stop",
4582
+ cwd,
4583
+ },
4584
+ { cwd },
4585
+ );
4586
+
4587
+ assert.equal(result.omxEventName, "stop");
4588
+ assert.deepEqual(result.outputJson, {
4589
+ decision: "block",
4590
+ reason: `OMX ${rootActiveCase.mode} is still active (phase: ${rootActiveCase.phase}); continue the task and gather fresh verification evidence before stopping.`,
4591
+ stopReason: `${rootActiveCase.mode}_${rootActiveCase.phase}`,
4592
+ systemMessage: `OMX ${rootActiveCase.mode} is still active (phase: ${rootActiveCase.phase}).`,
4593
+ });
4594
+ } finally {
4595
+ await rm(cwd, { recursive: true, force: true });
4596
+ }
4597
+ });
4598
+ }
4599
+
3840
4600
  it("returns Stop continuation output while Autopilot is active", async () => {
3841
4601
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-"));
3842
4602
  try {
3843
4603
  const stateDir = join(cwd, ".omx", "state");
3844
- await mkdir(stateDir, { recursive: true });
3845
- await writeJson(join(stateDir, "autopilot-state.json"), {
4604
+ await mkdir(join(stateDir, "sessions", "sess-stop-autopilot"), { recursive: true });
4605
+ await writeJson(join(stateDir, "sessions", "sess-stop-autopilot", "autopilot-state.json"), {
3846
4606
  active: true,
3847
4607
  current_phase: "execution",
3848
4608
  });
@@ -3873,8 +4633,8 @@ esac
3873
4633
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-planning-replay-"));
3874
4634
  try {
3875
4635
  const stateDir = join(cwd, ".omx", "state");
3876
- await mkdir(stateDir, { recursive: true });
3877
- await writeJson(join(stateDir, "autopilot-state.json"), {
4636
+ await mkdir(join(stateDir, "sessions", "sess-stop-autopilot-planning-replay"), { recursive: true });
4637
+ await writeJson(join(stateDir, "sessions", "sess-stop-autopilot-planning-replay", "autopilot-state.json"), {
3878
4638
  active: true,
3879
4639
  current_phase: "planning",
3880
4640
  });
@@ -3939,12 +4699,45 @@ esac
3939
4699
  }
3940
4700
  });
3941
4701
 
4702
+ for (const staleRootCase of [
4703
+ { mode: "autopilot", phase: "execution" },
4704
+ { mode: "ultrawork", phase: "executing" },
4705
+ { mode: "ultraqa", phase: "diagnose" },
4706
+ ] as const) {
4707
+ it(`does not block Stop from stale root ${staleRootCase.mode} state when the explicit session directory is missing`, async () => {
4708
+ const cwd = await mkdtemp(join(tmpdir(), `omx-native-hook-stop-missing-session-${staleRootCase.mode}-`));
4709
+ try {
4710
+ const stateDir = join(cwd, ".omx", "state");
4711
+ await mkdir(stateDir, { recursive: true });
4712
+ await writeJson(join(stateDir, `${staleRootCase.mode}-state.json`), {
4713
+ active: true,
4714
+ mode: staleRootCase.mode,
4715
+ current_phase: staleRootCase.phase,
4716
+ });
4717
+
4718
+ const result = await dispatchCodexNativeHook(
4719
+ {
4720
+ hook_event_name: "Stop",
4721
+ cwd,
4722
+ session_id: "missing-session",
4723
+ },
4724
+ { cwd },
4725
+ );
4726
+
4727
+ assert.equal(result.omxEventName, "stop");
4728
+ assert.equal(result.outputJson, null);
4729
+ } finally {
4730
+ await rm(cwd, { recursive: true, force: true });
4731
+ }
4732
+ });
4733
+ }
4734
+
3942
4735
  it("does not block Stop when an explicit blocked_on_user run_outcome is present on a mode state", async () => {
3943
4736
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-blocked-outcome-"));
3944
4737
  try {
3945
4738
  const stateDir = join(cwd, ".omx", "state");
3946
- await mkdir(stateDir, { recursive: true });
3947
- await writeJson(join(stateDir, "autopilot-state.json"), {
4739
+ await mkdir(join(stateDir, "sessions", "sess-stop-autopilot-blocked-outcome"), { recursive: true });
4740
+ await writeJson(join(stateDir, "sessions", "sess-stop-autopilot-blocked-outcome", "autopilot-state.json"), {
3948
4741
  active: true,
3949
4742
  current_phase: "execution",
3950
4743
  run_outcome: "blocked_on_user",
@@ -3970,8 +4763,8 @@ esac
3970
4763
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultrawork-"));
3971
4764
  try {
3972
4765
  const stateDir = join(cwd, ".omx", "state");
3973
- await mkdir(stateDir, { recursive: true });
3974
- await writeJson(join(stateDir, "ultrawork-state.json"), {
4766
+ await mkdir(join(stateDir, "sessions", "sess-stop-ultrawork"), { recursive: true });
4767
+ await writeJson(join(stateDir, "sessions", "sess-stop-ultrawork", "ultrawork-state.json"), {
3975
4768
  active: true,
3976
4769
  current_phase: "executing",
3977
4770
  });
@@ -3997,8 +4790,8 @@ esac
3997
4790
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultraqa-"));
3998
4791
  try {
3999
4792
  const stateDir = join(cwd, ".omx", "state");
4000
- await mkdir(stateDir, { recursive: true });
4001
- await writeJson(join(stateDir, "ultraqa-state.json"), {
4793
+ await mkdir(join(stateDir, "sessions", "sess-stop-ultraqa"), { recursive: true });
4794
+ await writeJson(join(stateDir, "sessions", "sess-stop-ultraqa", "ultraqa-state.json"), {
4002
4795
  active: true,
4003
4796
  current_phase: "diagnose",
4004
4797
  });
@@ -4020,6 +4813,46 @@ esac
4020
4813
  }
4021
4814
  });
4022
4815
 
4816
+ it("marks leader-owned team attention during native Stop dispatch without a polling watcher", async () => {
4817
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-attention-"));
4818
+ try {
4819
+ await initTeamState(
4820
+ "stop-attention-team",
4821
+ "native stop attention",
4822
+ "executor",
4823
+ 1,
4824
+ cwd,
4825
+ undefined,
4826
+ { ...process.env, OMX_SESSION_ID: "sess-stop-team-attention" },
4827
+ );
4828
+
4829
+ const result = await dispatchCodexNativeHook(
4830
+ {
4831
+ hook_event_name: "Stop",
4832
+ cwd,
4833
+ session_id: "sess-stop-team-attention",
4834
+ },
4835
+ { cwd },
4836
+ );
4837
+
4838
+ const attention = await readTeamLeaderAttention("stop-attention-team", cwd);
4839
+ assert.equal(result.omxEventName, "stop");
4840
+ assert.equal(attention?.source, "native_stop");
4841
+ assert.equal(attention?.leader_session_active, false);
4842
+ assert.equal(attention?.leader_session_id, "sess-stop-team-attention");
4843
+ assert.match(attention?.leader_session_stopped_at ?? "", /^\d{4}-\d{2}-\d{2}T/);
4844
+ assert.deepEqual(result.outputJson, {
4845
+ decision: "block",
4846
+ reason:
4847
+ `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}`,
4848
+ stopReason: "team_team-exec",
4849
+ systemMessage: "OMX team pipeline is still active at phase team-exec.",
4850
+ });
4851
+ } finally {
4852
+ await rm(cwd, { recursive: true, force: true });
4853
+ }
4854
+ });
4855
+
4023
4856
  it("returns Stop continuation output while team phase is non-terminal", async () => {
4024
4857
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-"));
4025
4858
  try {
@@ -4131,7 +4964,7 @@ esac
4131
4964
  }
4132
4965
  });
4133
4966
 
4134
- it("does not block Stop as a team-worker task failure when worker status is already terminal", async () => {
4967
+ it("blocks Stop as a team-worker task failure when worker status is terminal but task evidence is not completed", async () => {
4135
4968
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-terminal-stale-"));
4136
4969
  const prevTeamWorker = process.env.OMX_TEAM_WORKER;
4137
4970
  const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
@@ -4166,7 +4999,7 @@ esac
4166
4999
  await writeJson(join(stateDir, "team", "worker-stale-team", "tasks", "task-1.json"), {
4167
5000
  id: "1",
4168
5001
  subject: "stale hook task",
4169
- description: "stale task should not trap terminal worker Stop",
5002
+ description: "non-completed task should still block terminal worker Stop",
4170
5003
  status: "in_progress",
4171
5004
  owner: "worker-1",
4172
5005
  created_at: new Date().toISOString(),
@@ -4176,16 +5009,23 @@ esac
4176
5009
  process.env.OMX_TEAM_STATE_ROOT = stateDir;
4177
5010
  process.env.OMX_TEAM_LEADER_CWD = cwd;
4178
5011
 
4179
- const result = await dispatchCodexNativeHook(
4180
- {
4181
- hook_event_name: "Stop",
4182
- cwd: workerCwd,
4183
- session_id: "sess-stop-team-worker-stale",
4184
- },
5012
+ const payload = {
5013
+ hook_event_name: "Stop",
5014
+ cwd: workerCwd,
5015
+ session_id: "sess-stop-team-worker-stale",
5016
+ thread_id: "thread-stop-team-worker-stale",
5017
+ };
5018
+ const result = await dispatchCodexNativeHook(payload, { cwd: workerCwd });
5019
+ const replay = await dispatchCodexNativeHook(
5020
+ { ...payload, stop_hook_active: true },
4185
5021
  { cwd: workerCwd },
4186
5022
  );
4187
5023
 
4188
- assert.equal((result.outputJson as { stopReason?: string } | null)?.stopReason, "team_team-exec");
5024
+ assert.equal(
5025
+ (result.outputJson as { stopReason?: string } | null)?.stopReason,
5026
+ "team_worker_worker-1_1_in_progress",
5027
+ );
5028
+ assert.equal(replay.outputJson, null);
4189
5029
  } finally {
4190
5030
  if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
4191
5031
  else delete process.env.OMX_TEAM_WORKER;
@@ -4197,7 +5037,7 @@ esac
4197
5037
  }
4198
5038
  });
4199
5039
 
4200
- it("suppresses identical team worker Stop replays but re-blocks fresh turns and task state changes", async () => {
5040
+ it("re-blocks live team worker Stop replays but suppresses stale terminal worker repeats", async () => {
4201
5041
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-repeat-"));
4202
5042
  try {
4203
5043
  await initTeamState(
@@ -4268,85 +5108,516 @@ esac
4268
5108
 
4269
5109
  await writeJson(taskPath, {
4270
5110
  id: "1",
4271
- subject: "hook task",
4272
- description: "finish hook task",
4273
- status: "blocked",
5111
+ subject: "hook task",
5112
+ description: "finish hook task",
5113
+ status: "blocked",
5114
+ owner: "worker-1",
5115
+ created_at: new Date().toISOString(),
5116
+ });
5117
+ const stateChanged = await dispatchCodexNativeHook(
5118
+ { ...basePayload, turn_id: "turn-stop-team-worker-repeat-3", stop_hook_active: true },
5119
+ { cwd: workerCwd },
5120
+ );
5121
+
5122
+ assert.deepEqual(first.outputJson, expectedInProgress);
5123
+ assert.deepEqual(replay.outputJson, expectedInProgress);
5124
+ assert.deepEqual(freshTurn.outputJson, expectedInProgress);
5125
+ assert.deepEqual(stateChanged.outputJson, {
5126
+ decision: "block",
5127
+ reason:
5128
+ "OMX team worker worker-1 is still assigned non-terminal task 1 (blocked); continue the current assigned task or report a concrete blocker before stopping.",
5129
+ stopReason: "team_worker_worker-1_1_blocked",
5130
+ systemMessage: "OMX team worker worker-1 is still assigned task 1 (blocked).",
5131
+ });
5132
+ } finally {
5133
+ await rm(cwd, { recursive: true, force: true });
5134
+ }
5135
+ });
5136
+
5137
+ it("allows Stop for a team worker when assigned task is terminal and bypasses generic team blocking", async () => {
5138
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-terminal-"));
5139
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
5140
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
5141
+ const prevPath = process.env.PATH;
5142
+ try {
5143
+ await initTeamState(
5144
+ "worker-stop-team-terminal",
5145
+ "worker stop terminal fallback",
5146
+ "executor",
5147
+ 1,
5148
+ cwd,
5149
+ undefined,
5150
+ { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-terminal" },
5151
+ );
5152
+ const fakeBinDir = join(cwd, "fake-bin");
5153
+ const tmuxLogPath = join(cwd, "tmux.log");
5154
+ await mkdir(fakeBinDir, { recursive: true });
5155
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
5156
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
5157
+ const workerDir = join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "workers", "worker-1");
5158
+ await writeJson(join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "config.json"), {
5159
+ name: "worker-stop-team-terminal",
5160
+ tmux_session: "omx-team-worker-stop",
5161
+ leader_pane_id: "%42",
5162
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
5163
+ });
5164
+ await writeJson(join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "manifest.v2.json"), {
5165
+ name: "worker-stop-team-terminal",
5166
+ tmux_session: "omx-team-worker-stop",
5167
+ leader_pane_id: "%42",
5168
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
5169
+ });
5170
+ await writeJson(join(workerDir, "identity.json"), {
5171
+ name: "worker-1",
5172
+ index: 1,
5173
+ role: "executor",
5174
+ assigned_tasks: ["1"],
5175
+ worktree_path: cwd,
5176
+ team_state_root: join(cwd, ".omx", "state"),
5177
+ });
5178
+ await writeJson(join(workerDir, "status.json"), {
5179
+ state: "done",
5180
+ current_task_id: "1",
5181
+ updated_at: new Date().toISOString(),
5182
+ });
5183
+ await writeJson(join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "tasks", "task-1.json"), {
5184
+ id: "1",
5185
+ subject: "hook task",
5186
+ description: "finish hook task",
5187
+ status: "completed",
5188
+ owner: "worker-1",
5189
+ created_at: new Date().toISOString(),
5190
+ });
5191
+
5192
+ process.env.OMX_TEAM_WORKER = "worker-stop-team-terminal/worker-1";
5193
+ process.env.OMX_TEAM_STATE_ROOT = join(cwd, ".omx", "state");
5194
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
5195
+
5196
+ const result = await dispatchCodexNativeHook(
5197
+ {
5198
+ hook_event_name: "Stop",
5199
+ cwd,
5200
+ session_id: "sess-stop-team-worker-terminal",
5201
+ },
5202
+ { cwd },
5203
+ );
5204
+ const replay = await dispatchCodexNativeHook(
5205
+ {
5206
+ hook_event_name: "Stop",
5207
+ cwd,
5208
+ session_id: "sess-stop-team-worker-terminal",
5209
+ turn_id: "turn-worker-stop-terminal-replay",
5210
+ },
5211
+ { cwd },
5212
+ );
5213
+
5214
+ assert.equal(result.outputJson, null);
5215
+ assert.equal(replay.outputJson, null);
5216
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
5217
+ const stopNudges = tmuxLog.match(/send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/g) || [];
5218
+ assert.equal(stopNudges.length, 1, "allowed worker Stop should nudge leader exactly once inside cooldown");
5219
+ const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
5220
+ assert.equal(nudgeState.delivery, "sent");
5221
+ } finally {
5222
+ if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
5223
+ else delete process.env.OMX_TEAM_WORKER;
5224
+ if (typeof prevTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
5225
+ else delete process.env.OMX_TEAM_STATE_ROOT;
5226
+ if (typeof prevPath === "string") process.env.PATH = prevPath;
5227
+ else delete process.env.PATH;
5228
+ await rm(cwd, { recursive: true, force: true });
5229
+ }
5230
+ });
5231
+
5232
+ it("allows worker Stop when the Stop nudge helper cannot deliver", async () => {
5233
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-helper-fail-"));
5234
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
5235
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
5236
+ const prevPath = process.env.PATH;
5237
+ try {
5238
+ await initTeamState(
5239
+ "worker-stop-helper-fail",
5240
+ "worker stop helper failure",
5241
+ "executor",
5242
+ 1,
5243
+ cwd,
5244
+ undefined,
5245
+ { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-helper-fail" },
5246
+ );
5247
+ const fakeBinDir = join(cwd, "fake-bin");
5248
+ await mkdir(fakeBinDir, { recursive: true });
5249
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(join(cwd, "tmux.log"), { failSend: true }));
5250
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
5251
+ const stateDir = join(cwd, ".omx", "state");
5252
+ const workerDir = join(stateDir, "team", "worker-stop-helper-fail", "workers", "worker-1");
5253
+ await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "config.json"), {
5254
+ name: "worker-stop-helper-fail",
5255
+ tmux_session: "omx-team-worker-stop",
5256
+ leader_pane_id: "%42",
5257
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
5258
+ });
5259
+ await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "manifest.v2.json"), {
5260
+ name: "worker-stop-helper-fail",
5261
+ tmux_session: "omx-team-worker-stop",
5262
+ leader_pane_id: "%42",
5263
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
5264
+ });
5265
+ await writeJson(join(workerDir, "identity.json"), {
5266
+ name: "worker-1",
5267
+ assigned_tasks: ["1"],
5268
+ team_state_root: stateDir,
5269
+ });
5270
+ await writeJson(join(workerDir, "status.json"), {
5271
+ state: "done",
5272
+ current_task_id: "1",
5273
+ updated_at: new Date().toISOString(),
5274
+ });
5275
+ await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "tasks", "task-1.json"), {
5276
+ id: "1",
5277
+ status: "completed",
5278
+ owner: "worker-1",
5279
+ });
5280
+
5281
+ process.env.OMX_TEAM_WORKER = "worker-stop-helper-fail/worker-1";
5282
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
5283
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
5284
+
5285
+ const result = await dispatchCodexNativeHook(
5286
+ { hook_event_name: "Stop", cwd, session_id: "sess-stop-team-worker-helper-fail" },
5287
+ { cwd },
5288
+ );
5289
+
5290
+ assert.equal(result.outputJson, null);
5291
+ const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
5292
+ assert.equal(nudgeState.delivery, "deferred");
5293
+ } finally {
5294
+ if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
5295
+ else delete process.env.OMX_TEAM_WORKER;
5296
+ if (typeof prevTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
5297
+ else delete process.env.OMX_TEAM_STATE_ROOT;
5298
+ if (typeof prevPath === "string") process.env.PATH = prevPath;
5299
+ else delete process.env.PATH;
5300
+ await rm(cwd, { recursive: true, force: true });
5301
+ }
5302
+ });
5303
+
5304
+ it("does not treat failed or ambiguous worker task state as completed Stop evidence", async () => {
5305
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-failed-"));
5306
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
5307
+ const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
5308
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
5309
+ const prevPath = process.env.PATH;
5310
+ try {
5311
+ await initTeamState(
5312
+ "worker-stop-failed-task",
5313
+ "worker stop failed task",
5314
+ "executor",
5315
+ 1,
5316
+ cwd,
5317
+ undefined,
5318
+ { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-failed" },
5319
+ );
5320
+ const fakeBinDir = join(cwd, "fake-bin");
5321
+ const tmuxLogPath = join(cwd, "tmux.log");
5322
+ await mkdir(fakeBinDir, { recursive: true });
5323
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
5324
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
5325
+ const stateDir = join(cwd, ".omx", "state");
5326
+ const workerDir = join(stateDir, "team", "worker-stop-failed-task", "workers", "worker-1");
5327
+ await writeJson(join(stateDir, "team", "worker-stop-failed-task", "config.json"), {
5328
+ name: "worker-stop-failed-task",
5329
+ tmux_session: "omx-team-worker-stop",
5330
+ leader_pane_id: "%42",
5331
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
5332
+ });
5333
+ await writeJson(join(workerDir, "identity.json"), {
5334
+ name: "worker-1",
5335
+ assigned_tasks: ["1"],
5336
+ team_state_root: stateDir,
5337
+ });
5338
+ await writeJson(join(workerDir, "status.json"), {
5339
+ state: "failed",
5340
+ current_task_id: "1",
5341
+ updated_at: new Date().toISOString(),
5342
+ });
5343
+ await writeJson(join(stateDir, "team", "worker-stop-failed-task", "tasks", "task-1.json"), {
5344
+ id: "1",
5345
+ status: "failed",
5346
+ owner: "worker-1",
5347
+ });
5348
+
5349
+ process.env.OMX_TEAM_WORKER = "worker-stop-failed-task/worker-1";
5350
+ delete process.env.OMX_TEAM_INTERNAL_WORKER;
5351
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
5352
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
5353
+
5354
+ const result = await dispatchCodexNativeHook(
5355
+ {
5356
+ hook_event_name: "Stop",
5357
+ cwd,
5358
+ session_id: "sess-stop-team-worker-failed",
5359
+ thread_id: "thread-stop-team-worker-failed",
5360
+ turn_id: "turn-stop-team-worker-failed",
5361
+ },
5362
+ { cwd },
5363
+ );
5364
+
5365
+ assert.equal(result.outputJson?.decision, "block");
5366
+ assert.match(String(result.outputJson?.stopReason || ""), /non_completed_task_1_failed/);
5367
+ assert.match(JSON.stringify(result.outputJson), /team/i);
5368
+ assert.equal(existsSync(join(workerDir, "worker-stop-nudge.json")), false);
5369
+ const tmuxLog = existsSync(tmuxLogPath) ? await readFile(tmuxLogPath, "utf-8") : "";
5370
+ assert.doesNotMatch(tmuxLog, /native Stop allowed/);
5371
+ } finally {
5372
+ if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
5373
+ else delete process.env.OMX_TEAM_WORKER;
5374
+ if (typeof prevInternalTeamWorker === "string") process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
5375
+ else delete process.env.OMX_TEAM_INTERNAL_WORKER;
5376
+ if (typeof prevTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
5377
+ else delete process.env.OMX_TEAM_STATE_ROOT;
5378
+ if (typeof prevPath === "string") process.env.PATH = prevPath;
5379
+ else delete process.env.PATH;
5380
+ await rm(cwd, { recursive: true, force: true });
5381
+ }
5382
+ });
5383
+
5384
+ it("blocks worker Stop on missing task assignment without relying on generic team state", async () => {
5385
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-missing-assignment-"));
5386
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
5387
+ const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
5388
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
5389
+ try {
5390
+ const stateDir = join(cwd, ".omx", "state");
5391
+ const workerDir = join(stateDir, "team", "worker-missing-assignment", "workers", "worker-1");
5392
+ await mkdir(workerDir, { recursive: true });
5393
+ await writeJson(join(workerDir, "identity.json"), {
5394
+ name: "worker-1",
5395
+ assigned_tasks: [],
5396
+ team_state_root: stateDir,
5397
+ });
5398
+ await writeJson(join(workerDir, "status.json"), {
5399
+ state: "idle",
5400
+ updated_at: new Date().toISOString(),
5401
+ });
5402
+
5403
+ process.env.OMX_TEAM_WORKER = "worker-missing-assignment/worker-1";
5404
+ delete process.env.OMX_TEAM_INTERNAL_WORKER;
5405
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
5406
+
5407
+ const result = await dispatchCodexNativeHook(
5408
+ {
5409
+ hook_event_name: "Stop",
5410
+ cwd,
5411
+ session_id: "sess-stop-team-worker-missing-assignment",
5412
+ thread_id: "thread-stop-team-worker-missing-assignment",
5413
+ turn_id: "turn-stop-team-worker-missing-assignment",
5414
+ },
5415
+ { cwd },
5416
+ );
5417
+
5418
+ assert.equal(result.outputJson?.decision, "block");
5419
+ assert.equal(result.outputJson?.stopReason, "team_worker_worker-1_missing_task_assignment");
5420
+ assert.equal(existsSync(join(workerDir, "worker-stop-nudge.json")), false);
5421
+ } finally {
5422
+ if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
5423
+ else delete process.env.OMX_TEAM_WORKER;
5424
+ if (typeof prevInternalTeamWorker === "string") process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
5425
+ else delete process.env.OMX_TEAM_INTERNAL_WORKER;
5426
+ if (typeof prevTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
5427
+ else delete process.env.OMX_TEAM_STATE_ROOT;
5428
+ await rm(cwd, { recursive: true, force: true });
5429
+ }
5430
+ });
5431
+
5432
+ it("blocks unresolved worker Stop before generic auto-nudge can bypass it", async () => {
5433
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-missing-state-"));
5434
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
5435
+ const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
5436
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
5437
+ const prevPath = process.env.PATH;
5438
+ try {
5439
+ const stateDir = join(cwd, ".omx", "state");
5440
+ const fakeBinDir = join(cwd, "fake-bin");
5441
+ const tmuxLogPath = join(cwd, "tmux.log");
5442
+ await mkdir(fakeBinDir, { recursive: true });
5443
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
5444
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
5445
+
5446
+ process.env.OMX_TEAM_WORKER = "worker-missing-state/worker-1";
5447
+ delete process.env.OMX_TEAM_INTERNAL_WORKER;
5448
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
5449
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
5450
+
5451
+ const result = await dispatchCodexNativeHook(
5452
+ {
5453
+ hook_event_name: "Stop",
5454
+ cwd,
5455
+ session_id: "sess-stop-team-worker-missing-state",
5456
+ thread_id: "thread-stop-team-worker-missing-state",
5457
+ turn_id: "turn-stop-team-worker-missing-state",
5458
+ last_assistant_message: "Should I proceed?",
5459
+ },
5460
+ { cwd },
5461
+ );
5462
+
5463
+ assert.equal(result.outputJson?.decision, "block");
5464
+ assert.equal(result.outputJson?.stopReason, "team_worker_worker-1_missing_worker_state");
5465
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /auto_nudge/);
5466
+ const tmuxLog = existsSync(tmuxLogPath) ? await readFile(tmuxLogPath, "utf-8") : "";
5467
+ assert.doesNotMatch(tmuxLog, /native Stop allowed/);
5468
+ } finally {
5469
+ if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
5470
+ else delete process.env.OMX_TEAM_WORKER;
5471
+ if (typeof prevInternalTeamWorker === "string") process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
5472
+ else delete process.env.OMX_TEAM_INTERNAL_WORKER;
5473
+ if (typeof prevTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
5474
+ else delete process.env.OMX_TEAM_STATE_ROOT;
5475
+ if (typeof prevPath === "string") process.env.PATH = prevPath;
5476
+ else delete process.env.PATH;
5477
+ await rm(cwd, { recursive: true, force: true });
5478
+ }
5479
+ });
5480
+
5481
+ it("prefers canonical internal worker identity over public worker identity for Stop nudges", async () => {
5482
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-internal-env-"));
5483
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
5484
+ const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
5485
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
5486
+ const prevPath = process.env.PATH;
5487
+ try {
5488
+ const stateDir = join(cwd, ".omx", "state");
5489
+ const fakeBinDir = join(cwd, "fake-bin");
5490
+ const tmuxLogPath = join(cwd, "tmux.log");
5491
+ await mkdir(fakeBinDir, { recursive: true });
5492
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
5493
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
5494
+ const workerDir = join(stateDir, "team", "internal-stop-team", "workers", "worker-1");
5495
+ await writeJson(join(stateDir, "team", "internal-stop-team", "config.json"), {
5496
+ name: "internal-stop-team",
5497
+ tmux_session: "omx-team-worker-stop",
5498
+ leader_pane_id: "%42",
5499
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
5500
+ });
5501
+ await writeJson(join(workerDir, "identity.json"), {
5502
+ name: "worker-1",
5503
+ assigned_tasks: ["1"],
5504
+ team_state_root: stateDir,
5505
+ });
5506
+ await writeJson(join(workerDir, "status.json"), {
5507
+ state: "done",
5508
+ current_task_id: "1",
5509
+ updated_at: new Date().toISOString(),
5510
+ });
5511
+ await writeJson(join(stateDir, "team", "internal-stop-team", "tasks", "task-1.json"), {
5512
+ id: "1",
5513
+ status: "completed",
4274
5514
  owner: "worker-1",
4275
- created_at: new Date().toISOString(),
4276
5515
  });
4277
- const stateChanged = await dispatchCodexNativeHook(
4278
- { ...basePayload, turn_id: "turn-stop-team-worker-repeat-2", stop_hook_active: true },
4279
- { cwd: workerCwd },
5516
+
5517
+ process.env.OMX_TEAM_WORKER = "public-stop-team/worker-1";
5518
+ process.env.OMX_TEAM_INTERNAL_WORKER = "internal-stop-team/worker-1";
5519
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
5520
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
5521
+
5522
+ const result = await dispatchCodexNativeHook(
5523
+ {
5524
+ hook_event_name: "Stop",
5525
+ cwd,
5526
+ session_id: "sess-stop-team-worker-internal-env",
5527
+ thread_id: "thread-stop-team-worker-internal-env",
5528
+ turn_id: "turn-stop-team-worker-internal-env",
5529
+ },
5530
+ { cwd },
4280
5531
  );
4281
5532
 
4282
- assert.deepEqual(first.outputJson, expectedInProgress);
4283
- assert.deepEqual(replay.outputJson, null);
4284
- assert.deepEqual(freshTurn.outputJson, expectedInProgress);
4285
- assert.deepEqual(stateChanged.outputJson, {
4286
- decision: "block",
4287
- reason:
4288
- "OMX team worker worker-1 is still assigned non-terminal task 1 (blocked); continue the current assigned task or report a concrete blocker before stopping.",
4289
- stopReason: "team_worker_worker-1_1_blocked",
4290
- systemMessage: "OMX team worker worker-1 is still assigned task 1 (blocked).",
4291
- });
5533
+ assert.equal(result.outputJson, null);
5534
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
5535
+ assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
5536
+ assert.equal(existsSync(join(workerDir, "worker-stop-nudge.json")), true);
4292
5537
  } finally {
5538
+ if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
5539
+ else delete process.env.OMX_TEAM_WORKER;
5540
+ if (typeof prevInternalTeamWorker === "string") process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
5541
+ else delete process.env.OMX_TEAM_INTERNAL_WORKER;
5542
+ if (typeof prevTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
5543
+ else delete process.env.OMX_TEAM_STATE_ROOT;
5544
+ if (typeof prevPath === "string") process.env.PATH = prevPath;
5545
+ else delete process.env.PATH;
4293
5546
  await rm(cwd, { recursive: true, force: true });
4294
5547
  }
4295
5548
  });
4296
5549
 
4297
- it("does not block Stop for a team worker when assigned task is terminal", async () => {
4298
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-terminal-"));
5550
+ it("blocks worker Stop when canonical task ownership has a newer non-terminal task", async () => {
5551
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-owned-task-"));
4299
5552
  const prevTeamWorker = process.env.OMX_TEAM_WORKER;
5553
+ const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
4300
5554
  const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
5555
+ const prevPath = process.env.PATH;
4301
5556
  try {
4302
- await initTeamState(
4303
- "worker-stop-team-terminal",
4304
- "worker stop terminal fallback",
4305
- "executor",
4306
- 1,
4307
- cwd,
4308
- undefined,
4309
- { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-terminal" },
4310
- );
4311
- const workerDir = join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "workers", "worker-1");
5557
+ const stateDir = join(cwd, ".omx", "state");
5558
+ const fakeBinDir = join(cwd, "fake-bin");
5559
+ const tmuxLogPath = join(cwd, "tmux.log");
5560
+ await mkdir(fakeBinDir, { recursive: true });
5561
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
5562
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
5563
+ const workerDir = join(stateDir, "team", "worker-owned-task", "workers", "worker-1");
5564
+ await writeJson(join(stateDir, "team", "worker-owned-task", "config.json"), {
5565
+ name: "worker-owned-task",
5566
+ tmux_session: "omx-team-worker-stop",
5567
+ leader_pane_id: "%42",
5568
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
5569
+ });
5570
+ await writeJson(join(workerDir, "identity.json"), {
5571
+ name: "worker-1",
5572
+ assigned_tasks: ["1"],
5573
+ team_state_root: stateDir,
5574
+ });
4312
5575
  await writeJson(join(workerDir, "status.json"), {
4313
5576
  state: "done",
4314
5577
  current_task_id: "1",
4315
5578
  updated_at: new Date().toISOString(),
4316
5579
  });
4317
- await writeJson(join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "tasks", "task-1.json"), {
5580
+ await writeJson(join(stateDir, "team", "worker-owned-task", "tasks", "task-1.json"), {
4318
5581
  id: "1",
4319
- subject: "hook task",
4320
- description: "finish hook task",
4321
5582
  status: "completed",
4322
5583
  owner: "worker-1",
4323
- created_at: new Date().toISOString(),
5584
+ });
5585
+ await writeJson(join(stateDir, "team", "worker-owned-task", "tasks", "task-2.json"), {
5586
+ id: "2",
5587
+ status: "in_progress",
5588
+ owner: "worker-1",
4324
5589
  });
4325
5590
 
4326
- process.env.OMX_TEAM_WORKER = "worker-stop-team-terminal/worker-1";
4327
- process.env.OMX_TEAM_STATE_ROOT = join(cwd, ".omx", "state");
5591
+ process.env.OMX_TEAM_WORKER = "worker-owned-task/worker-1";
5592
+ delete process.env.OMX_TEAM_INTERNAL_WORKER;
5593
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
5594
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
4328
5595
 
4329
5596
  const result = await dispatchCodexNativeHook(
4330
5597
  {
4331
5598
  hook_event_name: "Stop",
4332
5599
  cwd,
4333
- session_id: "sess-stop-team-worker-terminal",
5600
+ session_id: "sess-stop-team-worker-owned-task",
5601
+ thread_id: "thread-stop-team-worker-owned-task",
5602
+ turn_id: "turn-stop-team-worker-owned-task",
4334
5603
  },
4335
5604
  { cwd },
4336
5605
  );
4337
5606
 
4338
- assert.deepEqual(result.outputJson, {
4339
- decision: "block",
4340
- reason:
4341
- `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}`,
4342
- stopReason: "team_team-exec",
4343
- systemMessage: "OMX team pipeline is still active at phase team-exec.",
4344
- });
5607
+ assert.equal(result.outputJson?.decision, "block");
5608
+ assert.equal(result.outputJson?.stopReason, "team_worker_worker-1_2_in_progress");
5609
+ assert.equal(existsSync(join(workerDir, "worker-stop-nudge.json")), false);
5610
+ const tmuxLog = existsSync(tmuxLogPath) ? await readFile(tmuxLogPath, "utf-8") : "";
5611
+ assert.doesNotMatch(tmuxLog, /native Stop allowed/);
4345
5612
  } finally {
4346
5613
  if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
4347
5614
  else delete process.env.OMX_TEAM_WORKER;
5615
+ if (typeof prevInternalTeamWorker === "string") process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
5616
+ else delete process.env.OMX_TEAM_INTERNAL_WORKER;
4348
5617
  if (typeof prevTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
4349
5618
  else delete process.env.OMX_TEAM_STATE_ROOT;
5619
+ if (typeof prevPath === "string") process.env.PATH = prevPath;
5620
+ else delete process.env.PATH;
4350
5621
  await rm(cwd, { recursive: true, force: true });
4351
5622
  }
4352
5623
  });
@@ -4655,7 +5926,6 @@ esac
4655
5926
  }
4656
5927
  });
4657
5928
 
4658
-
4659
5929
  it("reads canonical Stop fallback team state from OMX_TEAM_STATE_ROOT when configured", async () => {
4660
5930
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-root-"));
4661
5931
  const sharedRoot = join(cwd, "shared-root");
@@ -4808,13 +6078,12 @@ esac
4808
6078
  );
4809
6079
 
4810
6080
  assert.equal(result.omxEventName, "stop");
4811
- assert.deepEqual(result.outputJson, {
4812
- decision: "block",
4813
- reason:
4814
- "OMX skill ralplan is still active (phase: planning); continue until the current ralplan workflow reaches a terminal state.",
4815
- stopReason: "skill_ralplan_planning",
4816
- systemMessage: "OMX skill ralplan is still active (phase: planning).",
4817
- });
6081
+ assert.equal(result.outputJson?.decision, "block");
6082
+ assert.match(String(result.outputJson?.reason ?? ""), /Status: continue_from_artifact/);
6083
+ assert.match(String(result.outputJson?.reason ?? ""), /ralplan is still active \(phase: planning\)/);
6084
+ assert.match(String(result.outputJson?.reason ?? ""), /continue from the current ralplan artifact/i);
6085
+ assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
6086
+ assert.match(String(result.outputJson?.systemMessage ?? ""), /complete, paused for review, waiting for input, or still continuing/);
4818
6087
  } finally {
4819
6088
  await rm(cwd, { recursive: true, force: true });
4820
6089
  }
@@ -4949,7 +6218,7 @@ esac
4949
6218
  }
4950
6219
  });
4951
6220
 
4952
- it("does not block on active ralplan skill when subagents are still active", async () => {
6221
+ it("returns an explicit ralplan waiting status while subagents are still active", async () => {
4953
6222
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-subagent-"));
4954
6223
  try {
4955
6224
  const stateDir = join(cwd, ".omx", "state");
@@ -5001,7 +6270,11 @@ esac
5001
6270
  );
5002
6271
 
5003
6272
  assert.equal(result.omxEventName, "stop");
5004
- assert.equal(result.outputJson, null);
6273
+ assert.equal(result.outputJson?.decision, "block");
6274
+ assert.match(String(result.outputJson?.reason ?? ""), /Status: waiting/);
6275
+ assert.match(String(result.outputJson?.reason ?? ""), /waiting for 1 active native subagent thread/);
6276
+ assert.match(String(result.outputJson?.reason ?? ""), /then continue from the current ralplan artifact/i);
6277
+ assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_waiting_subagent");
5005
6278
  } finally {
5006
6279
  await rm(cwd, { recursive: true, force: true });
5007
6280
  }
@@ -5140,6 +6413,36 @@ esac
5140
6413
  }
5141
6414
  });
5142
6415
 
6416
+ it("does not block Stop from stale root autoresearch state when the explicit session directory is missing", async () => {
6417
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-missing-session-autoresearch-"));
6418
+ try {
6419
+ const stateDir = join(cwd, ".omx", "state");
6420
+ await mkdir(stateDir, { recursive: true });
6421
+ await writeJson(join(stateDir, "autoresearch-state.json"), {
6422
+ active: true,
6423
+ mode: "autoresearch",
6424
+ current_phase: "executing",
6425
+ validation_mode: "mission-validator-script",
6426
+ mission_validator_command: "node scripts/validate.js",
6427
+ completion_artifact_path: ".omx/specs/autoresearch-demo/completion.json",
6428
+ });
6429
+
6430
+ const result = await dispatchCodexNativeHook(
6431
+ {
6432
+ hook_event_name: "Stop",
6433
+ cwd,
6434
+ session_id: "missing-session",
6435
+ },
6436
+ { cwd },
6437
+ );
6438
+
6439
+ assert.equal(result.omxEventName, "stop");
6440
+ assert.equal(result.outputJson, null);
6441
+ } finally {
6442
+ await rm(cwd, { recursive: true, force: true });
6443
+ }
6444
+ });
6445
+
5143
6446
  it("does not block Stop solely because deep-interview is active", async () => {
5144
6447
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-"));
5145
6448
  try {
@@ -5999,6 +7302,51 @@ esac
5999
7302
  }
6000
7303
  });
6001
7304
 
7305
+ it("does not block Stop when Ralph skill-active initialization points at another session", async () => {
7306
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-stale-skill-active-"));
7307
+ try {
7308
+ const stateDir = join(cwd, ".omx", "state");
7309
+ const currentSessionId = "sess-current-ralph";
7310
+ await mkdir(join(stateDir, "sessions", currentSessionId), { recursive: true });
7311
+ await writeJson(join(stateDir, "session.json"), {
7312
+ session_id: currentSessionId,
7313
+ native_session_id: currentSessionId,
7314
+ cwd,
7315
+ });
7316
+ await writeJson(join(stateDir, "sessions", currentSessionId, "ralph-state.json"), {
7317
+ active: true,
7318
+ mode: "ralph",
7319
+ current_phase: "verifying",
7320
+ session_id: currentSessionId,
7321
+ owner_omx_session_id: currentSessionId,
7322
+ task_slug: "stale-rebound-task",
7323
+ });
7324
+ await writeJson(join(stateDir, "sessions", currentSessionId, "skill-active-state.json"), {
7325
+ active: true,
7326
+ skill: "ralph",
7327
+ phase: "verifying",
7328
+ session_id: currentSessionId,
7329
+ initialized_mode: "ralph",
7330
+ initialized_state_path: ".omx/state/sessions/sess-old-ralph/ralph-state.json",
7331
+ active_skills: [{ skill: "ralph", phase: "verifying", active: true, session_id: currentSessionId }],
7332
+ });
7333
+
7334
+ const result = await dispatchCodexNativeHook(
7335
+ {
7336
+ hook_event_name: "Stop",
7337
+ cwd,
7338
+ session_id: currentSessionId,
7339
+ },
7340
+ { cwd },
7341
+ );
7342
+
7343
+ assert.equal(result.omxEventName, "stop");
7344
+ assert.equal(result.outputJson, null);
7345
+ } finally {
7346
+ await rm(cwd, { recursive: true, force: true });
7347
+ }
7348
+ });
7349
+
6002
7350
  it("blocks same-session Ralph Stop continuation when ownership identifiers match", async () => {
6003
7351
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-owned-session-"));
6004
7352
  const previousTmuxPane = process.env.TMUX_PANE;
@@ -6050,6 +7398,98 @@ esac
6050
7398
  }
6051
7399
  });
6052
7400
 
7401
+ it("allows native verifier subagent Stop to complete while leader Ralph remains active", async () => {
7402
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-subagent-verdict-"));
7403
+ try {
7404
+ const stateDir = join(cwd, ".omx", "state");
7405
+ const omxSessionId = "sess-ralph-leader-verifier";
7406
+ const leaderNativeSessionId = "codex-ralph-leader-verifier";
7407
+ const childNativeSessionId = "codex-verifier-child";
7408
+ await mkdir(join(stateDir, "sessions", omxSessionId), { recursive: true });
7409
+ await writeSessionStart(cwd, omxSessionId, {
7410
+ nativeSessionId: leaderNativeSessionId,
7411
+ });
7412
+ await writeJson(join(stateDir, "sessions", omxSessionId, "ralph-state.json"), {
7413
+ active: true,
7414
+ mode: "ralph",
7415
+ current_phase: "verifying",
7416
+ session_id: omxSessionId,
7417
+ owner_omx_session_id: omxSessionId,
7418
+ owner_codex_session_id: leaderNativeSessionId,
7419
+ });
7420
+
7421
+ const transcriptPath = join(cwd, "verifier-subagent-rollout.jsonl");
7422
+ await writeFile(
7423
+ transcriptPath,
7424
+ `${JSON.stringify({
7425
+ type: "session_meta",
7426
+ payload: {
7427
+ id: childNativeSessionId,
7428
+ source: {
7429
+ subagent: {
7430
+ thread_spawn: {
7431
+ parent_thread_id: leaderNativeSessionId,
7432
+ depth: 1,
7433
+ agent_nickname: "Verifier",
7434
+ agent_role: "verifier",
7435
+ },
7436
+ },
7437
+ },
7438
+ agent_nickname: "Verifier",
7439
+ agent_role: "verifier",
7440
+ },
7441
+ })}\n`,
7442
+ );
7443
+
7444
+ await dispatchCodexNativeHook(
7445
+ {
7446
+ hook_event_name: "SessionStart",
7447
+ cwd,
7448
+ session_id: childNativeSessionId,
7449
+ transcript_path: transcriptPath,
7450
+ },
7451
+ { cwd, sessionOwnerPid: process.pid },
7452
+ );
7453
+
7454
+ const childStop = await dispatchCodexNativeHook(
7455
+ {
7456
+ hook_event_name: "Stop",
7457
+ cwd,
7458
+ session_id: childNativeSessionId,
7459
+ thread_id: childNativeSessionId,
7460
+ last_assistant_message: "Verdict: APPROVED. Evidence is sufficient.",
7461
+ },
7462
+ { cwd },
7463
+ );
7464
+
7465
+ assert.equal(childStop.omxEventName, "stop");
7466
+ assert.equal(childStop.outputJson, null);
7467
+
7468
+ const leaderStop = await dispatchCodexNativeHook(
7469
+ {
7470
+ hook_event_name: "Stop",
7471
+ cwd,
7472
+ session_id: leaderNativeSessionId,
7473
+ thread_id: leaderNativeSessionId,
7474
+ last_assistant_message: "Waiting on verification integration.",
7475
+ },
7476
+ { cwd },
7477
+ );
7478
+
7479
+ assert.equal(leaderStop.omxEventName, "stop");
7480
+ assert.deepEqual(leaderStop.outputJson, {
7481
+ decision: "block",
7482
+ reason:
7483
+ "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.",
7484
+ stopReason: "ralph_verifying",
7485
+ systemMessage:
7486
+ "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.",
7487
+ });
7488
+ } finally {
7489
+ await rm(cwd, { recursive: true, force: true });
7490
+ }
7491
+ });
7492
+
6053
7493
  it("prefers canonical run-state terminal lifecycle before stale session Ralph state during Stop", async () => {
6054
7494
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-canonical-run-state-ralph-"));
6055
7495
  try {
@@ -6340,7 +7780,6 @@ esac
6340
7780
  }
6341
7781
  });
6342
7782
 
6343
-
6344
7783
  it("returns Stop continuation output for native auto-nudge stall prompts", async () => {
6345
7784
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-"));
6346
7785
  try {
@@ -6720,12 +8159,11 @@ esac
6720
8159
  }
6721
8160
  });
6722
8161
 
6723
- it("suppresses native auto-nudge when root deep-interview mode state is active without an explicit session", async () => {
8162
+ it("suppresses native auto-nudge when root deep-interview mode state is active and no session is known", async () => {
6724
8163
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-deep-interview-mode-"));
6725
8164
  try {
6726
8165
  const stateDir = join(cwd, ".omx", "state");
6727
8166
  await mkdir(stateDir, { recursive: true });
6728
- process.env.OMX_SESSION_ID = "sess-stop-auto-mode";
6729
8167
  await writeJson(join(stateDir, "deep-interview-state.json"), {
6730
8168
  active: true,
6731
8169
  mode: "deep-interview",
@@ -6749,6 +8187,68 @@ esac
6749
8187
  }
6750
8188
  });
6751
8189
 
8190
+ it("treats inherited OMX_SESSION_ID as session-aware for native auto-nudge Stop checks", async () => {
8191
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-env-session-"));
8192
+ try {
8193
+ const stateDir = join(cwd, ".omx", "state");
8194
+ await mkdir(stateDir, { recursive: true });
8195
+ process.env.OMX_SESSION_ID = "sess-stop-auto-mode";
8196
+
8197
+ const result = await dispatchCodexNativeHook(
8198
+ {
8199
+ hook_event_name: "Stop",
8200
+ cwd,
8201
+ thread_id: "thread-stop-auto-env-session",
8202
+ turn_id: "turn-stop-auto-env-session-1",
8203
+ last_assistant_message: "Keep going and finish the cleanup.",
8204
+ },
8205
+ { cwd },
8206
+ );
8207
+
8208
+ assert.equal(result.omxEventName, "stop");
8209
+ assert.deepEqual(result.outputJson, {
8210
+ decision: "block",
8211
+ reason: DEFAULT_AUTO_NUDGE_RESPONSE,
8212
+ stopReason: "auto_nudge",
8213
+ systemMessage:
8214
+ "OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
8215
+ });
8216
+ const stopState = JSON.parse(await readFile(join(stateDir, "native-stop-state.json"), "utf-8")) as Record<string, unknown>;
8217
+ assert.ok((stopState.sessions as Record<string, unknown>)["sess-stop-auto-mode"]);
8218
+ } finally {
8219
+ await rm(cwd, { recursive: true, force: true });
8220
+ }
8221
+ });
8222
+
8223
+
8224
+ it("ignores generic SESSION_ID for native auto-nudge Stop session scoping", async () => {
8225
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-generic-session-"));
8226
+ try {
8227
+ const stateDir = join(cwd, ".omx", "state");
8228
+ await mkdir(stateDir, { recursive: true });
8229
+ process.env.SESSION_ID = "generic-shell-session";
8230
+
8231
+ const result = await dispatchCodexNativeHook(
8232
+ {
8233
+ hook_event_name: "Stop",
8234
+ cwd,
8235
+ thread_id: "thread-stop-auto-generic-session",
8236
+ turn_id: "turn-stop-auto-generic-session-1",
8237
+ last_assistant_message: "Keep going and finish the cleanup.",
8238
+ },
8239
+ { cwd },
8240
+ );
8241
+
8242
+ assert.equal(result.omxEventName, "stop");
8243
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
8244
+ const stopState = JSON.parse(await readFile(join(stateDir, "native-stop-state.json"), "utf-8")) as Record<string, unknown>;
8245
+ const sessions = stopState.sessions as Record<string, unknown>;
8246
+ assert.equal(sessions["generic-shell-session"], undefined);
8247
+ assert.ok(sessions["thread-stop-auto-generic-session"]);
8248
+ } finally {
8249
+ await rm(cwd, { recursive: true, force: true });
8250
+ }
8251
+ });
6752
8252
  it("does not suppress native auto-nudge from stale root deep-interview mode state when the explicit session-scoped mode state is absent", async () => {
6753
8253
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-stale-root-mode-"));
6754
8254
  try {
@@ -7093,8 +8593,8 @@ esac
7093
8593
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultrawork-repeat-"));
7094
8594
  try {
7095
8595
  const stateDir = join(cwd, ".omx", "state");
7096
- await mkdir(stateDir, { recursive: true });
7097
- await writeJson(join(stateDir, "ultrawork-state.json"), {
8596
+ await mkdir(join(stateDir, "sessions", "sess-stop-ultrawork-repeat"), { recursive: true });
8597
+ await writeJson(join(stateDir, "sessions", "sess-stop-ultrawork-repeat", "ultrawork-state.json"), {
7098
8598
  active: true,
7099
8599
  current_phase: "executing",
7100
8600
  });
@@ -7188,13 +8688,10 @@ esac
7188
8688
  );
7189
8689
 
7190
8690
  assert.equal(repeated.omxEventName, "stop");
7191
- assert.deepEqual(repeated.outputJson, {
7192
- decision: "block",
7193
- reason:
7194
- "OMX skill ralplan is still active (phase: planning); continue until the current ralplan workflow reaches a terminal state.",
7195
- stopReason: "skill_ralplan_planning",
7196
- systemMessage: "OMX skill ralplan is still active (phase: planning).",
7197
- });
8691
+ assert.equal(repeated.outputJson?.decision, "block");
8692
+ assert.match(String(repeated.outputJson?.reason ?? ""), /Status: continue_from_artifact/);
8693
+ assert.match(String(repeated.outputJson?.reason ?? ""), /continue from the current ralplan artifact/i);
8694
+ assert.equal(repeated.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
7198
8695
  } finally {
7199
8696
  await rm(cwd, { recursive: true, force: true });
7200
8697
  }