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
@@ -13,6 +13,7 @@ import { dispatchCodexNativeHook, isCodexNativeHookMainModule, mapCodexHookEvent
13
13
  import { writeSessionStart } from "../../hooks/session.js";
14
14
  import { resetTriageConfigCache } from "../../hooks/triage-config.js";
15
15
  import { executeStateOperation } from "../../state/operations.js";
16
+ import { OMX_TMUX_HUD_OWNER_ENV } from "../../hud/reconcile.js";
16
17
  function nativeHookScriptPath() {
17
18
  return join(process.cwd(), "dist", "scripts", "codex-native-hook.js");
18
19
  }
@@ -35,6 +36,50 @@ async function writeJson(path, value) {
35
36
  await mkdir(dirname(path), { recursive: true }).catch(() => { });
36
37
  await writeFile(path, JSON.stringify(value, null, 2));
37
38
  }
39
+ function buildWorkerStopFakeTmux(tmuxLogPath, options = {}) {
40
+ return `#!/usr/bin/env bash
41
+ set -eu
42
+ echo "$@" >> "${tmuxLogPath}"
43
+ cmd="$1"
44
+ shift || true
45
+ if [[ "$cmd" == "display-message" ]]; then
46
+ fmt=""
47
+ while [[ "$#" -gt 0 ]]; do
48
+ case "$1" in
49
+ -p) ;;
50
+ -t) shift ;;
51
+ *) fmt="$1" ;;
52
+ esac
53
+ shift || true
54
+ done
55
+ case "$fmt" in
56
+ "#{pane_in_mode}") echo "0" ;;
57
+ "#{pane_id}") echo "%42" ;;
58
+ "#{pane_current_path}") pwd ;;
59
+ "#{pane_start_command}") echo "codex" ;;
60
+ "#{pane_current_command}") echo "codex" ;;
61
+ "#S") echo "omx-team-worker-stop" ;;
62
+ *) ;;
63
+ esac
64
+ exit 0
65
+ fi
66
+ if [[ "$cmd" == "capture-pane" ]]; then
67
+ echo "› ready"
68
+ exit 0
69
+ fi
70
+ if [[ "$cmd" == "send-keys" ]]; then
71
+ ${options.failSend ? "exit 1" : "exit 0"}
72
+ fi
73
+ exit 0
74
+ `;
75
+ }
76
+ async function initTempGitRepo(prefix) {
77
+ const cwd = await mkdtemp(join(tmpdir(), prefix));
78
+ execFileSync("git", ["init"], { cwd, stdio: "ignore" });
79
+ execFileSync("git", ["config", "user.email", "test@example.com"], { cwd, stdio: "ignore" });
80
+ execFileSync("git", ["config", "user.name", "Test User"], { cwd, stdio: "ignore" });
81
+ return cwd;
82
+ }
38
83
  async function writeActiveAutopilotSession(cwd, sessionId) {
39
84
  await writeJson(join(cwd, ".omx", "state", "session.json"), {
40
85
  session_id: sessionId,
@@ -96,10 +141,12 @@ const TEAM_ENV_KEYS = [
96
141
  "OMX_TEAM_STATE_ROOT",
97
142
  "OMX_TEAM_LEADER_CWD",
98
143
  "OMX_SESSION_ID",
144
+ "SESSION_ID",
99
145
  "OMX_QUESTION_RETURN_PANE",
100
146
  "OMX_LEADER_PANE_ID",
101
147
  "TMUX",
102
148
  "TMUX_PANE",
149
+ "OMX_TMUX_HUD_OWNER",
103
150
  ];
104
151
  const priorTeamEnv = new Map();
105
152
  beforeEach(() => {
@@ -763,6 +810,114 @@ describe("codex native hook dispatch", () => {
763
810
  await rm(cwd, { recursive: true, force: true });
764
811
  }
765
812
  });
813
+ it("warns completion-like prompts when active goal workflows need Codex snapshot reconciliation", async () => {
814
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-goal-warning-"));
815
+ try {
816
+ await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
817
+ version: 1,
818
+ activeGoalId: "G001-demo",
819
+ goals: [{ id: "G001-demo", status: "in_progress", objective: "Demo goal" }],
820
+ });
821
+ const result = await dispatchCodexNativeHook({
822
+ hook_event_name: "UserPromptSubmit",
823
+ cwd,
824
+ session_id: "sess-goal-warning",
825
+ thread_id: "thread-goal-warning",
826
+ prompt: "complete this goal now",
827
+ }, { cwd });
828
+ assert.match(JSON.stringify(result.outputJson), /requires Codex goal snapshot reconciliation/);
829
+ assert.match(JSON.stringify(result.outputJson), /get_goal/);
830
+ assert.match(JSON.stringify(result.outputJson), /--codex-goal-json/);
831
+ }
832
+ finally {
833
+ await rm(cwd, { recursive: true, force: true });
834
+ }
835
+ });
836
+ it("blocks Stop when a completion-like final answer skips active goal snapshot reconciliation", async () => {
837
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-goal-stop-"));
838
+ try {
839
+ await writeJson(join(cwd, ".omx", "goals", "performance", "latency", "state.json"), {
840
+ version: 1,
841
+ workflow: "performance-goal",
842
+ slug: "latency",
843
+ objective: "Reduce latency",
844
+ status: "validation_passed",
845
+ });
846
+ const result = await dispatchCodexNativeHook({
847
+ hook_event_name: "Stop",
848
+ cwd,
849
+ session_id: "sess-goal-stop",
850
+ thread_id: "thread-goal-stop",
851
+ last_assistant_message: "Performance goal complete; next call update_goal({status: \"complete\"}).",
852
+ }, { cwd });
853
+ assert.equal(result.outputJson?.decision, "block");
854
+ assert.match(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
855
+ assert.match(JSON.stringify(result.outputJson), /omx performance-goal complete --slug latency/);
856
+ assert.match(JSON.stringify(result.outputJson), /Hooks must not mutate Codex goal state/);
857
+ }
858
+ finally {
859
+ await rm(cwd, { recursive: true, force: true });
860
+ }
861
+ });
862
+ it("treats workflow keywords in native subagent prompt text as literal delegation text", async () => {
863
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-keyword-literal-"));
864
+ try {
865
+ const stateDir = join(cwd, ".omx", "state");
866
+ const canonicalSessionId = "sess-parent";
867
+ const leaderNativeSessionId = "native-parent-thread";
868
+ const childNativeSessionId = "native-child-thread";
869
+ const nowIso = new Date().toISOString();
870
+ await writeJson(join(stateDir, "session.json"), {
871
+ session_id: canonicalSessionId,
872
+ native_session_id: leaderNativeSessionId,
873
+ });
874
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
875
+ schemaVersion: 1,
876
+ sessions: {
877
+ [canonicalSessionId]: {
878
+ session_id: canonicalSessionId,
879
+ leader_thread_id: leaderNativeSessionId,
880
+ updated_at: nowIso,
881
+ threads: {
882
+ [leaderNativeSessionId]: {
883
+ thread_id: leaderNativeSessionId,
884
+ kind: "leader",
885
+ first_seen_at: nowIso,
886
+ last_seen_at: nowIso,
887
+ turn_count: 1,
888
+ },
889
+ [childNativeSessionId]: {
890
+ thread_id: childNativeSessionId,
891
+ kind: "subagent",
892
+ first_seen_at: nowIso,
893
+ last_seen_at: nowIso,
894
+ turn_count: 1,
895
+ mode: "architect",
896
+ },
897
+ },
898
+ },
899
+ },
900
+ });
901
+ const result = await dispatchCodexNativeHook({
902
+ hook_event_name: "UserPromptSubmit",
903
+ cwd,
904
+ session_id: childNativeSessionId,
905
+ thread_id: childNativeSessionId,
906
+ turn_id: "turn-child-1",
907
+ prompt: "$ralplan Architect review step. Review the draft plan and return APPROVE or ITERATE.",
908
+ }, { cwd });
909
+ assert.equal(result.omxEventName, "keyword-detector");
910
+ assert.equal(result.skillState, null);
911
+ assert.equal(result.outputJson, null);
912
+ assert.equal(existsSync(join(stateDir, "skill-active-state.json")), false);
913
+ assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "skill-active-state.json")), false);
914
+ assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "ralplan-state.json")), false);
915
+ assert.equal(existsSync(join(stateDir, "sessions", childNativeSessionId, "ralplan-state.json")), false);
916
+ }
917
+ finally {
918
+ await rm(cwd, { recursive: true, force: true });
919
+ }
920
+ });
766
921
  it("records plugin-prefixed keyword activation from UserPromptSubmit payloads", async () => {
767
922
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-plugin-prefixed-"));
768
923
  try {
@@ -786,6 +941,33 @@ describe("codex native hook dispatch", () => {
786
941
  await rm(cwd, { recursive: true, force: true });
787
942
  }
788
943
  });
944
+ it("records ultragoal prompt skill activation with goal-tool handoff guidance", async () => {
945
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-"));
946
+ try {
947
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
948
+ const result = await dispatchCodexNativeHook({
949
+ hook_event_name: "UserPromptSubmit",
950
+ cwd,
951
+ session_id: "sess-ultragoal-1",
952
+ thread_id: "thread-ultragoal-1",
953
+ turn_id: "turn-ultragoal-1",
954
+ prompt: "$ultragoal split this launch into durable goals",
955
+ }, { cwd });
956
+ assert.equal(result.omxEventName, "keyword-detector");
957
+ assert.equal(result.skillState?.skill, "ultragoal");
958
+ assert.equal(result.skillState?.initialized_mode, undefined);
959
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
960
+ assert.match(message, /"\$ultragoal" -> ultragoal/);
961
+ assert.match(message, /Ultragoal protocol:/);
962
+ assert.match(message, /get_goal/);
963
+ assert.match(message, /create_goal/);
964
+ assert.match(message, /update_goal/);
965
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ultragoal-1", "ultragoal-state.json")), false);
966
+ }
967
+ finally {
968
+ await rm(cwd, { recursive: true, force: true });
969
+ }
970
+ });
789
971
  it("normalizes the Korean keyboard typo for ulw during UserPromptSubmit activation", async () => {
790
972
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ulw-ko-"));
791
973
  try {
@@ -1505,10 +1687,12 @@ export async function onHookEvent(event) {
1505
1687
  const originalTmux = process.env.TMUX;
1506
1688
  const originalTmuxPane = process.env.TMUX_PANE;
1507
1689
  const originalPath = process.env.PATH;
1690
+ const originalHudOwner = process.env[OMX_TMUX_HUD_OWNER_ENV];
1508
1691
  const originalArgv = process.argv;
1509
1692
  try {
1510
1693
  process.env.TMUX = "1";
1511
1694
  process.env.TMUX_PANE = "%1";
1695
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = "1";
1512
1696
  await mkdir(join(cwd, ".omx", "state"), { recursive: true });
1513
1697
  await writeFile(join(cwd, ".omx", "hud-config.json"), JSON.stringify({ preset: "focused", git: { display: "branch" } }, null, 2));
1514
1698
  const binDir = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-reconcile-bin-"));
@@ -1560,11 +1744,61 @@ esac
1560
1744
  else {
1561
1745
  process.env.TMUX_PANE = originalTmuxPane;
1562
1746
  }
1747
+ if (originalHudOwner === undefined) {
1748
+ delete process.env[OMX_TMUX_HUD_OWNER_ENV];
1749
+ }
1750
+ else {
1751
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = originalHudOwner;
1752
+ }
1563
1753
  process.env.PATH = originalPath;
1564
1754
  process.argv = originalArgv;
1565
1755
  await rm(cwd, { recursive: true, force: true });
1566
1756
  }
1567
1757
  });
1758
+ it("skips prompt-submit HUD reconciliation inside unowned tmux panes", async () => {
1759
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-unowned-"));
1760
+ const originalTmux = process.env.TMUX;
1761
+ const originalTmuxPane = process.env.TMUX_PANE;
1762
+ const originalPath = process.env.PATH;
1763
+ const originalHudOwner = process.env[OMX_TMUX_HUD_OWNER_ENV];
1764
+ try {
1765
+ process.env.TMUX = "1";
1766
+ process.env.TMUX_PANE = "%claude";
1767
+ delete process.env[OMX_TMUX_HUD_OWNER_ENV];
1768
+ const binDir = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-unowned-bin-"));
1769
+ const tmuxLog = join(cwd, "tmux.log");
1770
+ await writeFile(join(binDir, "tmux"), `#!/usr/bin/env bash
1771
+ printf '%s\n' "$*" >> ${JSON.stringify(tmuxLog)}
1772
+ exit 0
1773
+ `);
1774
+ await chmod(join(binDir, "tmux"), 0o755);
1775
+ process.env.PATH = `${binDir}:${originalPath}`;
1776
+ const result = await dispatchCodexNativeHook({
1777
+ hook_event_name: "UserPromptSubmit",
1778
+ cwd,
1779
+ session_id: "sess-hud-unowned",
1780
+ prompt: "$ralplan prepare plan",
1781
+ }, { cwd });
1782
+ assert.equal(result.omxEventName, "keyword-detector");
1783
+ assert.equal(existsSync(tmuxLog), false);
1784
+ }
1785
+ finally {
1786
+ if (originalTmux === undefined)
1787
+ delete process.env.TMUX;
1788
+ else
1789
+ process.env.TMUX = originalTmux;
1790
+ if (originalTmuxPane === undefined)
1791
+ delete process.env.TMUX_PANE;
1792
+ else
1793
+ process.env.TMUX_PANE = originalTmuxPane;
1794
+ if (originalHudOwner === undefined)
1795
+ delete process.env[OMX_TMUX_HUD_OWNER_ENV];
1796
+ else
1797
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = originalHudOwner;
1798
+ process.env.PATH = originalPath;
1799
+ await rm(cwd, { recursive: true, force: true });
1800
+ }
1801
+ });
1568
1802
  it("blocks Bash omx question when no leader-pane return hint is preserved", async () => {
1569
1803
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-enforce-"));
1570
1804
  try {
@@ -2035,6 +2269,186 @@ esac
2035
2269
  await rm(cwd, { recursive: true, force: true });
2036
2270
  }
2037
2271
  });
2272
+ it("blocks Stop for untracked non-Bash-style sloppy fallback source edits", async () => {
2273
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-untracked-");
2274
+ try {
2275
+ await mkdir(join(cwd, "src"), { recursive: true });
2276
+ await writeFile(join(cwd, "src", "runtime.ts"), [
2277
+ "export function loadRuntime() {",
2278
+ " // implement a quick hack fallback if it fails",
2279
+ " return process.env.RUNTIME || 'local';",
2280
+ "}",
2281
+ ].join("\n"));
2282
+ const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-untracked" }, { cwd });
2283
+ assert.equal(result.omxEventName, "stop");
2284
+ assert.equal(result.outputJson?.decision, "block");
2285
+ assert.equal(result.outputJson?.stopReason, "sloppy_fallback_diff_audit");
2286
+ assert.match(JSON.stringify(result.outputJson), /src\/runtime\.ts/);
2287
+ assert.match(JSON.stringify(result.outputJson), /grounded design/);
2288
+ }
2289
+ finally {
2290
+ await rm(cwd, { recursive: true, force: true });
2291
+ }
2292
+ });
2293
+ it("keeps blocking repeated Stop while sloppy fallback diff remains", async () => {
2294
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-repeat-");
2295
+ try {
2296
+ await mkdir(join(cwd, "src"), { recursive: true });
2297
+ await writeFile(join(cwd, "src", "runtime.ts"), [
2298
+ "export function loadRuntime() {",
2299
+ " // implement a quick hack fallback if it fails",
2300
+ " return process.env.RUNTIME || 'local';",
2301
+ "}",
2302
+ ].join("\n"));
2303
+ const payload = { hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-repeat", turn_id: "turn-repeat" };
2304
+ const first = await dispatchCodexNativeHook(payload, { cwd });
2305
+ const repeated = await dispatchCodexNativeHook({ ...payload, stop_hook_active: true }, { cwd });
2306
+ assert.equal(first.outputJson?.decision, "block");
2307
+ assert.equal(repeated.outputJson?.decision, "block");
2308
+ assert.equal(repeated.outputJson?.stopReason, "sloppy_fallback_diff_audit");
2309
+ }
2310
+ finally {
2311
+ await rm(cwd, { recursive: true, force: true });
2312
+ }
2313
+ });
2314
+ it("blocks Stop for unstaged tracked sloppy fallback source edits", async () => {
2315
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-unstaged-");
2316
+ try {
2317
+ await mkdir(join(cwd, "src"), { recursive: true });
2318
+ await writeFile(join(cwd, "src", "runtime.ts"), "export const runtime = 'base';\n");
2319
+ execFileSync("git", ["add", "src/runtime.ts"], { cwd, stdio: "ignore" });
2320
+ execFileSync("git", ["commit", "-m", "initial"], { cwd, stdio: "ignore" });
2321
+ await writeFile(join(cwd, "src", "runtime.ts"), [
2322
+ "export function loadRuntime() {",
2323
+ " // just bypass fallback if it fails",
2324
+ " return process.env.RUNTIME || 'local';",
2325
+ "}",
2326
+ ].join("\n"));
2327
+ const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-unstaged" }, { cwd });
2328
+ assert.equal(result.omxEventName, "stop");
2329
+ assert.equal(result.outputJson?.decision, "block");
2330
+ assert.match(JSON.stringify(result.outputJson), /unstaged/);
2331
+ }
2332
+ finally {
2333
+ await rm(cwd, { recursive: true, force: true });
2334
+ }
2335
+ });
2336
+ it("blocks Stop from a subdirectory cwd for untracked sloppy source elsewhere", async () => {
2337
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-subdir-");
2338
+ try {
2339
+ await mkdir(join(cwd, "src", "nested"), { recursive: true });
2340
+ await writeFile(join(cwd, "src", "nested", "anchor.ts"), "export const anchor = true;\n");
2341
+ await writeFile(join(cwd, "src", "runtime.ts"), [
2342
+ "export function loadRuntime() {",
2343
+ " // implement a quick hack fallback if it fails",
2344
+ " return process.env.RUNTIME || 'local';",
2345
+ "}",
2346
+ ].join("\n"));
2347
+ const subdir = join(cwd, "src", "nested");
2348
+ const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd: subdir, session_id: "sess-stop-slop-subdir" }, { cwd: subdir });
2349
+ assert.equal(result.omxEventName, "stop");
2350
+ assert.equal(result.outputJson?.decision, "block");
2351
+ assert.match(JSON.stringify(result.outputJson), /src\/runtime\.ts/);
2352
+ }
2353
+ finally {
2354
+ await rm(cwd, { recursive: true, force: true });
2355
+ }
2356
+ });
2357
+ it("blocks Stop for staged sloppy fallback source edits", async () => {
2358
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-staged-");
2359
+ try {
2360
+ await mkdir(join(cwd, "src"), { recursive: true });
2361
+ await writeFile(join(cwd, "src", "runtime.ts"), "export const runtime = 'base';\n");
2362
+ execFileSync("git", ["add", "src/runtime.ts"], { cwd, stdio: "ignore" });
2363
+ execFileSync("git", ["commit", "-m", "initial"], { cwd, stdio: "ignore" });
2364
+ await writeFile(join(cwd, "src", "runtime.ts"), [
2365
+ "export function loadRuntime() {",
2366
+ " // temporary workaround fallback if it fails",
2367
+ " return process.env.RUNTIME || 'local';",
2368
+ "}",
2369
+ ].join("\n"));
2370
+ execFileSync("git", ["add", "src/runtime.ts"], { cwd, stdio: "ignore" });
2371
+ const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-staged" }, { cwd });
2372
+ assert.equal(result.omxEventName, "stop");
2373
+ assert.equal(result.outputJson?.decision, "block");
2374
+ assert.match(JSON.stringify(result.outputJson), /staged/);
2375
+ }
2376
+ finally {
2377
+ await rm(cwd, { recursive: true, force: true });
2378
+ }
2379
+ });
2380
+ it("does not block Stop for grounded compatibility fallback source edits", async () => {
2381
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-grounded-");
2382
+ try {
2383
+ await mkdir(join(cwd, "src"), { recursive: true });
2384
+ await writeFile(join(cwd, "src", "compat.ts"), [
2385
+ "export function resolveCompatMode() {",
2386
+ " // temporary fallback for legacy startup",
2387
+ " // compatibility fail-safe tested by regression coverage",
2388
+ " return 'legacy';",
2389
+ "}",
2390
+ ].join("\n"));
2391
+ const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-grounded" }, { cwd });
2392
+ assert.equal(result.omxEventName, "stop");
2393
+ assert.equal(result.outputJson, null);
2394
+ }
2395
+ finally {
2396
+ await rm(cwd, { recursive: true, force: true });
2397
+ }
2398
+ });
2399
+ it("does not block Stop when existing nearby source context grounds a new fallback line", async () => {
2400
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-existing-ground-");
2401
+ try {
2402
+ await mkdir(join(cwd, "src"), { recursive: true });
2403
+ await writeFile(join(cwd, "src", "compat.ts"), [
2404
+ "export function resolveCompatMode() {",
2405
+ " // compatibility fail-safe tested by regression coverage",
2406
+ " return 'legacy';",
2407
+ "}",
2408
+ ].join("\n"));
2409
+ execFileSync("git", ["add", "src/compat.ts"], { cwd, stdio: "ignore" });
2410
+ execFileSync("git", ["commit", "-m", "initial"], { cwd, stdio: "ignore" });
2411
+ await writeFile(join(cwd, "src", "compat.ts"), [
2412
+ "export function resolveCompatMode() {",
2413
+ " // compatibility fail-safe tested by regression coverage",
2414
+ " // temporary fallback if it fails",
2415
+ " return 'legacy';",
2416
+ "}",
2417
+ ].join("\n"));
2418
+ const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-existing-ground" }, { cwd });
2419
+ assert.equal(result.omxEventName, "stop");
2420
+ assert.equal(result.outputJson, null);
2421
+ }
2422
+ finally {
2423
+ await rm(cwd, { recursive: true, force: true });
2424
+ }
2425
+ });
2426
+ it("does not block Stop for source-adjacent test file fallback wording", async () => {
2427
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-test-file-");
2428
+ try {
2429
+ await mkdir(join(cwd, "src"), { recursive: true });
2430
+ await writeFile(join(cwd, "src", "runtime.test.ts"), "it('documents no quick hack fallback if it fails', () => {});\n");
2431
+ const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-test-file" }, { cwd });
2432
+ assert.equal(result.omxEventName, "stop");
2433
+ assert.equal(result.outputJson, null);
2434
+ }
2435
+ finally {
2436
+ await rm(cwd, { recursive: true, force: true });
2437
+ }
2438
+ });
2439
+ it("does not block Stop for docs-only fallback wording", async () => {
2440
+ const cwd = await initTempGitRepo("omx-native-hook-stop-slop-docs-");
2441
+ try {
2442
+ await mkdir(join(cwd, "docs"), { recursive: true });
2443
+ await writeFile(join(cwd, "docs", "notes.md"), "Do not implement a quick hack fallback if it fails.\n");
2444
+ const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-slop-docs" }, { cwd });
2445
+ assert.equal(result.omxEventName, "stop");
2446
+ assert.equal(result.outputJson, null);
2447
+ }
2448
+ finally {
2449
+ await rm(cwd, { recursive: true, force: true });
2450
+ }
2451
+ });
2038
2452
  it("keeps git commit Lore enforcement ahead of sloppy fallback advisory", async () => {
2039
2453
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-git-priority-"));
2040
2454
  try {
@@ -2087,32 +2501,38 @@ esac
2087
2501
  await rm(cwd, { recursive: true, force: true });
2088
2502
  }
2089
2503
  });
2090
- it("stays silent on PreToolUse for `git help commit`", async () => {
2091
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-help-commit-"));
2504
+ it("allows non-Lore git commit messages when the Lore commit guard is explicitly disabled", async () => {
2505
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-disabled-"));
2506
+ const original = process.env.OMX_LORE_COMMIT_GUARD;
2092
2507
  try {
2508
+ process.env.OMX_LORE_COMMIT_GUARD = "0";
2093
2509
  const result = await dispatchCodexNativeHook({
2094
2510
  hook_event_name: "PreToolUse",
2095
2511
  cwd,
2096
2512
  tool_name: "Bash",
2097
- tool_use_id: "tool-git-help-commit",
2098
- tool_input: { command: "git help commit" },
2513
+ tool_use_id: "tool-git-commit-lore-disabled",
2514
+ tool_input: { command: 'git commit -m "fix: use conventional commit"' },
2099
2515
  }, { cwd });
2100
2516
  assert.equal(result.omxEventName, "pre-tool-use");
2101
2517
  assert.equal(result.outputJson, null);
2102
2518
  }
2103
2519
  finally {
2520
+ if (original === undefined)
2521
+ delete process.env.OMX_LORE_COMMIT_GUARD;
2522
+ else
2523
+ process.env.OMX_LORE_COMMIT_GUARD = original;
2104
2524
  await rm(cwd, { recursive: true, force: true });
2105
2525
  }
2106
2526
  });
2107
- it("stays silent on PreToolUse for `git config alias.ci commit`", async () => {
2108
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-config-alias-commit-"));
2527
+ it("allows non-Lore git commit messages when the Lore commit guard is disabled inline", async () => {
2528
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-inline-disabled-"));
2109
2529
  try {
2110
2530
  const result = await dispatchCodexNativeHook({
2111
2531
  hook_event_name: "PreToolUse",
2112
2532
  cwd,
2113
2533
  tool_name: "Bash",
2114
- tool_use_id: "tool-git-config-alias-commit",
2115
- tool_input: { command: "git config alias.ci commit" },
2534
+ tool_use_id: "tool-git-commit-lore-inline-disabled",
2535
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=0 git commit -m "fix: conventional"' },
2116
2536
  }, { cwd });
2117
2537
  assert.equal(result.omxEventName, "pre-tool-use");
2118
2538
  assert.equal(result.outputJson, null);
@@ -2121,105 +2541,294 @@ esac
2121
2541
  await rm(cwd, { recursive: true, force: true });
2122
2542
  }
2123
2543
  });
2124
- it("stays silent on PreToolUse for `git tag commit`", async () => {
2125
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-tag-commit-"));
2544
+ it("does not treat newline-separated Lore guard assignment as inline git commit env", async () => {
2545
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-newline-assignment-"));
2126
2546
  try {
2127
2547
  const result = await dispatchCodexNativeHook({
2128
2548
  hook_event_name: "PreToolUse",
2129
2549
  cwd,
2130
2550
  tool_name: "Bash",
2131
- tool_use_id: "tool-git-tag-commit",
2132
- tool_input: { command: "git tag commit" },
2551
+ tool_use_id: "tool-git-commit-lore-newline-assignment",
2552
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=0\ngit commit -m "fix: conventional"' },
2133
2553
  }, { cwd });
2134
2554
  assert.equal(result.omxEventName, "pre-tool-use");
2135
- assert.equal(result.outputJson, null);
2555
+ assert.equal(result.outputJson?.decision, "block");
2556
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
2136
2557
  }
2137
2558
  finally {
2138
2559
  await rm(cwd, { recursive: true, force: true });
2139
2560
  }
2140
2561
  });
2141
- it("blocks PreToolUse env-prefixed git commit when the inline message is not Lore-compliant", async () => {
2142
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-env-invalid-"));
2562
+ it("restores default-on Lore guard when env -u unsets a disabled process env", async () => {
2563
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-env-unset-"));
2564
+ const original = process.env.OMX_LORE_COMMIT_GUARD;
2143
2565
  try {
2566
+ process.env.OMX_LORE_COMMIT_GUARD = "0";
2144
2567
  const result = await dispatchCodexNativeHook({
2145
2568
  hook_event_name: "PreToolUse",
2146
2569
  cwd,
2147
2570
  tool_name: "Bash",
2148
- tool_use_id: "tool-git-commit-env-invalid",
2149
- tool_input: { command: 'HUSKY=0 git commit -m "fix tests"' },
2571
+ tool_use_id: "tool-git-commit-lore-env-unset",
2572
+ tool_input: { command: 'env -u OMX_LORE_COMMIT_GUARD git commit -m "fix: conventional"' },
2150
2573
  }, { cwd });
2151
2574
  assert.equal(result.omxEventName, "pre-tool-use");
2152
- assert.deepEqual(result.outputJson, {
2153
- decision: "block",
2154
- reason: "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
2155
- hookSpecificOutput: {
2156
- hookEventName: "PreToolUse",
2157
- },
2158
- systemMessage: [
2159
- "git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
2160
- "- Add a blank line after the subject before the narrative body.",
2161
- "- Add a narrative body paragraph explaining the decision context.",
2162
- "- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
2163
- "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
2164
- ].join("\n"),
2165
- });
2575
+ assert.equal(result.outputJson?.decision, "block");
2576
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
2166
2577
  }
2167
2578
  finally {
2579
+ if (original === undefined)
2580
+ delete process.env.OMX_LORE_COMMIT_GUARD;
2581
+ else
2582
+ process.env.OMX_LORE_COMMIT_GUARD = original;
2168
2583
  await rm(cwd, { recursive: true, force: true });
2169
2584
  }
2170
2585
  });
2171
- it("blocks PreToolUse git commit when git options appear before the real commit subcommand", async () => {
2172
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-option-invalid-"));
2586
+ it("restores default-on Lore guard when env -i clears a disabled process env", async () => {
2587
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-env-ignore-"));
2588
+ const original = process.env.OMX_LORE_COMMIT_GUARD;
2173
2589
  try {
2590
+ process.env.OMX_LORE_COMMIT_GUARD = "0";
2174
2591
  const result = await dispatchCodexNativeHook({
2175
2592
  hook_event_name: "PreToolUse",
2176
2593
  cwd,
2177
2594
  tool_name: "Bash",
2178
- tool_use_id: "tool-git-commit-option-invalid",
2179
- tool_input: { command: 'git -c core.editor=true commit -m "fix tests"' },
2595
+ tool_use_id: "tool-git-commit-lore-env-ignore",
2596
+ tool_input: { command: 'env -i PATH=/usr/bin git commit -m "fix: conventional"' },
2180
2597
  }, { cwd });
2181
2598
  assert.equal(result.omxEventName, "pre-tool-use");
2182
- assert.deepEqual(result.outputJson, {
2183
- decision: "block",
2184
- reason: "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
2185
- hookSpecificOutput: {
2186
- hookEventName: "PreToolUse",
2187
- },
2188
- systemMessage: [
2189
- "git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
2190
- "- Add a blank line after the subject before the narrative body.",
2191
- "- Add a narrative body paragraph explaining the decision context.",
2192
- "- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
2193
- "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
2194
- ].join("\n"),
2195
- });
2599
+ assert.equal(result.outputJson?.decision, "block");
2600
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
2196
2601
  }
2197
2602
  finally {
2603
+ if (original === undefined)
2604
+ delete process.env.OMX_LORE_COMMIT_GUARD;
2605
+ else
2606
+ process.env.OMX_LORE_COMMIT_GUARD = original;
2198
2607
  await rm(cwd, { recursive: true, force: true });
2199
2608
  }
2200
2609
  });
2201
- it("blocks PreToolUse env wrapper-prefixed git.exe commit when the inline message is not Lore-compliant", async () => {
2202
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-exe-commit-env-wrapper-invalid-"));
2610
+ it("keeps Lore commit enforcement enabled for unknown inline guard values", async () => {
2611
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-inline-unknown-"));
2203
2612
  try {
2204
2613
  const result = await dispatchCodexNativeHook({
2205
2614
  hook_event_name: "PreToolUse",
2206
2615
  cwd,
2207
2616
  tool_name: "Bash",
2208
- tool_use_id: "tool-git-exe-commit-env-wrapper-invalid",
2209
- tool_input: { command: 'env git.exe commit -m "fix tests"' },
2617
+ tool_use_id: "tool-git-commit-lore-inline-unknown",
2618
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=maybe git commit -m "fix: conventional"' },
2210
2619
  }, { cwd });
2211
2620
  assert.equal(result.omxEventName, "pre-tool-use");
2212
- assert.deepEqual(result.outputJson, {
2213
- decision: "block",
2214
- reason: "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
2215
- hookSpecificOutput: {
2216
- hookEventName: "PreToolUse",
2217
- },
2218
- systemMessage: [
2219
- "git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
2220
- "- Add a blank line after the subject before the narrative body.",
2221
- "- Add a narrative body paragraph explaining the decision context.",
2222
- "- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
2621
+ assert.equal(result.outputJson?.decision, "block");
2622
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
2623
+ }
2624
+ finally {
2625
+ await rm(cwd, { recursive: true, force: true });
2626
+ }
2627
+ });
2628
+ it("treats Lore commit guard disabled values as trim and case tolerant", async () => {
2629
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-off-"));
2630
+ const original = process.env.OMX_LORE_COMMIT_GUARD;
2631
+ try {
2632
+ process.env.OMX_LORE_COMMIT_GUARD = " OFF ";
2633
+ const result = await dispatchCodexNativeHook({
2634
+ hook_event_name: "PreToolUse",
2635
+ cwd,
2636
+ tool_name: "Bash",
2637
+ tool_use_id: "tool-git-commit-lore-off",
2638
+ tool_input: { command: 'git commit -m "chore: conventional commit"' },
2639
+ }, { cwd });
2640
+ assert.equal(result.omxEventName, "pre-tool-use");
2641
+ assert.equal(result.outputJson, null);
2642
+ }
2643
+ finally {
2644
+ if (original === undefined)
2645
+ delete process.env.OMX_LORE_COMMIT_GUARD;
2646
+ else
2647
+ process.env.OMX_LORE_COMMIT_GUARD = original;
2648
+ await rm(cwd, { recursive: true, force: true });
2649
+ }
2650
+ });
2651
+ it("keeps Lore commit enforcement enabled for unknown guard values", async () => {
2652
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-unknown-"));
2653
+ const original = process.env.OMX_LORE_COMMIT_GUARD;
2654
+ try {
2655
+ process.env.OMX_LORE_COMMIT_GUARD = "maybe";
2656
+ const result = await dispatchCodexNativeHook({
2657
+ hook_event_name: "PreToolUse",
2658
+ cwd,
2659
+ tool_name: "Bash",
2660
+ tool_use_id: "tool-git-commit-lore-unknown",
2661
+ tool_input: { command: 'git commit -m "fix tests"' },
2662
+ }, { cwd });
2663
+ assert.equal(result.omxEventName, "pre-tool-use");
2664
+ assert.equal(result.outputJson?.decision, "block");
2665
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
2666
+ }
2667
+ finally {
2668
+ if (original === undefined)
2669
+ delete process.env.OMX_LORE_COMMIT_GUARD;
2670
+ else
2671
+ process.env.OMX_LORE_COMMIT_GUARD = original;
2672
+ await rm(cwd, { recursive: true, force: true });
2673
+ }
2674
+ });
2675
+ it("continues to later PreToolUse checks when Lore commit guard is disabled", async () => {
2676
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-lore-disabled-destructive-"));
2677
+ const original = process.env.OMX_LORE_COMMIT_GUARD;
2678
+ try {
2679
+ process.env.OMX_LORE_COMMIT_GUARD = "false";
2680
+ const result = await dispatchCodexNativeHook({
2681
+ hook_event_name: "PreToolUse",
2682
+ cwd,
2683
+ tool_name: "Bash",
2684
+ tool_use_id: "tool-lore-disabled-destructive",
2685
+ tool_input: { command: "rm -rf dist" },
2686
+ }, { cwd });
2687
+ assert.equal(result.omxEventName, "pre-tool-use");
2688
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /Lore protocol/);
2689
+ assert.match(JSON.stringify(result.outputJson), /Destructive Bash command detected/);
2690
+ }
2691
+ finally {
2692
+ if (original === undefined)
2693
+ delete process.env.OMX_LORE_COMMIT_GUARD;
2694
+ else
2695
+ process.env.OMX_LORE_COMMIT_GUARD = original;
2696
+ await rm(cwd, { recursive: true, force: true });
2697
+ }
2698
+ });
2699
+ it("stays silent on PreToolUse for `git help commit`", async () => {
2700
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-help-commit-"));
2701
+ try {
2702
+ const result = await dispatchCodexNativeHook({
2703
+ hook_event_name: "PreToolUse",
2704
+ cwd,
2705
+ tool_name: "Bash",
2706
+ tool_use_id: "tool-git-help-commit",
2707
+ tool_input: { command: "git help commit" },
2708
+ }, { cwd });
2709
+ assert.equal(result.omxEventName, "pre-tool-use");
2710
+ assert.equal(result.outputJson, null);
2711
+ }
2712
+ finally {
2713
+ await rm(cwd, { recursive: true, force: true });
2714
+ }
2715
+ });
2716
+ it("stays silent on PreToolUse for `git config alias.ci commit`", async () => {
2717
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-config-alias-commit-"));
2718
+ try {
2719
+ const result = await dispatchCodexNativeHook({
2720
+ hook_event_name: "PreToolUse",
2721
+ cwd,
2722
+ tool_name: "Bash",
2723
+ tool_use_id: "tool-git-config-alias-commit",
2724
+ tool_input: { command: "git config alias.ci commit" },
2725
+ }, { cwd });
2726
+ assert.equal(result.omxEventName, "pre-tool-use");
2727
+ assert.equal(result.outputJson, null);
2728
+ }
2729
+ finally {
2730
+ await rm(cwd, { recursive: true, force: true });
2731
+ }
2732
+ });
2733
+ it("stays silent on PreToolUse for `git tag commit`", async () => {
2734
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-tag-commit-"));
2735
+ try {
2736
+ const result = await dispatchCodexNativeHook({
2737
+ hook_event_name: "PreToolUse",
2738
+ cwd,
2739
+ tool_name: "Bash",
2740
+ tool_use_id: "tool-git-tag-commit",
2741
+ tool_input: { command: "git tag commit" },
2742
+ }, { cwd });
2743
+ assert.equal(result.omxEventName, "pre-tool-use");
2744
+ assert.equal(result.outputJson, null);
2745
+ }
2746
+ finally {
2747
+ await rm(cwd, { recursive: true, force: true });
2748
+ }
2749
+ });
2750
+ it("blocks PreToolUse env-prefixed git commit when the inline message is not Lore-compliant", async () => {
2751
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-env-invalid-"));
2752
+ try {
2753
+ const result = await dispatchCodexNativeHook({
2754
+ hook_event_name: "PreToolUse",
2755
+ cwd,
2756
+ tool_name: "Bash",
2757
+ tool_use_id: "tool-git-commit-env-invalid",
2758
+ tool_input: { command: 'HUSKY=0 git commit -m "fix tests"' },
2759
+ }, { cwd });
2760
+ assert.equal(result.omxEventName, "pre-tool-use");
2761
+ assert.deepEqual(result.outputJson, {
2762
+ decision: "block",
2763
+ reason: "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
2764
+ hookSpecificOutput: {
2765
+ hookEventName: "PreToolUse",
2766
+ },
2767
+ systemMessage: [
2768
+ "git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
2769
+ "- Add a blank line after the subject before the narrative body.",
2770
+ "- Add a narrative body paragraph explaining the decision context.",
2771
+ "- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
2772
+ "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
2773
+ ].join("\n"),
2774
+ });
2775
+ }
2776
+ finally {
2777
+ await rm(cwd, { recursive: true, force: true });
2778
+ }
2779
+ });
2780
+ it("blocks PreToolUse git commit when git options appear before the real commit subcommand", async () => {
2781
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-option-invalid-"));
2782
+ try {
2783
+ const result = await dispatchCodexNativeHook({
2784
+ hook_event_name: "PreToolUse",
2785
+ cwd,
2786
+ tool_name: "Bash",
2787
+ tool_use_id: "tool-git-commit-option-invalid",
2788
+ tool_input: { command: 'git -c core.editor=true commit -m "fix tests"' },
2789
+ }, { cwd });
2790
+ assert.equal(result.omxEventName, "pre-tool-use");
2791
+ assert.deepEqual(result.outputJson, {
2792
+ decision: "block",
2793
+ reason: "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
2794
+ hookSpecificOutput: {
2795
+ hookEventName: "PreToolUse",
2796
+ },
2797
+ systemMessage: [
2798
+ "git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
2799
+ "- Add a blank line after the subject before the narrative body.",
2800
+ "- Add a narrative body paragraph explaining the decision context.",
2801
+ "- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
2802
+ "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
2803
+ ].join("\n"),
2804
+ });
2805
+ }
2806
+ finally {
2807
+ await rm(cwd, { recursive: true, force: true });
2808
+ }
2809
+ });
2810
+ it("blocks PreToolUse env wrapper-prefixed git.exe commit when the inline message is not Lore-compliant", async () => {
2811
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-exe-commit-env-wrapper-invalid-"));
2812
+ try {
2813
+ const result = await dispatchCodexNativeHook({
2814
+ hook_event_name: "PreToolUse",
2815
+ cwd,
2816
+ tool_name: "Bash",
2817
+ tool_use_id: "tool-git-exe-commit-env-wrapper-invalid",
2818
+ tool_input: { command: 'env git.exe commit -m "fix tests"' },
2819
+ }, { cwd });
2820
+ assert.equal(result.omxEventName, "pre-tool-use");
2821
+ assert.deepEqual(result.outputJson, {
2822
+ decision: "block",
2823
+ reason: "git commit is blocked until the inline commit message satisfies the Lore format and includes the required OmX co-author trailer.",
2824
+ hookSpecificOutput: {
2825
+ hookEventName: "PreToolUse",
2826
+ },
2827
+ systemMessage: [
2828
+ "git commit is blocked until the inline commit message follows the Lore protocol and includes `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
2829
+ "- Add a blank line after the subject before the narrative body.",
2830
+ "- Add a narrative body paragraph explaining the decision context.",
2831
+ "- Add at least one Lore trailer such as `Constraint:`, `Confidence:`, or `Tested:`.",
2223
2832
  "- Add the required co-author trailer: `Co-authored-by: OmX <omx@oh-my-codex.dev>`.",
2224
2833
  ].join("\n"),
2225
2834
  });
@@ -3077,12 +3686,44 @@ esac
3077
3686
  await rm(cwd, { recursive: true, force: true });
3078
3687
  }
3079
3688
  });
3689
+ for (const rootActiveCase of [
3690
+ { mode: "autopilot", phase: "execution" },
3691
+ { mode: "ultrawork", phase: "executing" },
3692
+ { mode: "ultraqa", phase: "diagnose" },
3693
+ ]) {
3694
+ it(`returns Stop continuation output from root ${rootActiveCase.mode} state when no session is active`, async () => {
3695
+ const cwd = await mkdtemp(join(tmpdir(), `omx-native-hook-stop-root-${rootActiveCase.mode}-`));
3696
+ try {
3697
+ const stateDir = join(cwd, ".omx", "state");
3698
+ await mkdir(stateDir, { recursive: true });
3699
+ await writeJson(join(stateDir, `${rootActiveCase.mode}-state.json`), {
3700
+ active: true,
3701
+ mode: rootActiveCase.mode,
3702
+ current_phase: rootActiveCase.phase,
3703
+ });
3704
+ const result = await dispatchCodexNativeHook({
3705
+ hook_event_name: "Stop",
3706
+ cwd,
3707
+ }, { cwd });
3708
+ assert.equal(result.omxEventName, "stop");
3709
+ assert.deepEqual(result.outputJson, {
3710
+ decision: "block",
3711
+ reason: `OMX ${rootActiveCase.mode} is still active (phase: ${rootActiveCase.phase}); continue the task and gather fresh verification evidence before stopping.`,
3712
+ stopReason: `${rootActiveCase.mode}_${rootActiveCase.phase}`,
3713
+ systemMessage: `OMX ${rootActiveCase.mode} is still active (phase: ${rootActiveCase.phase}).`,
3714
+ });
3715
+ }
3716
+ finally {
3717
+ await rm(cwd, { recursive: true, force: true });
3718
+ }
3719
+ });
3720
+ }
3080
3721
  it("returns Stop continuation output while Autopilot is active", async () => {
3081
3722
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-"));
3082
3723
  try {
3083
3724
  const stateDir = join(cwd, ".omx", "state");
3084
- await mkdir(stateDir, { recursive: true });
3085
- await writeJson(join(stateDir, "autopilot-state.json"), {
3725
+ await mkdir(join(stateDir, "sessions", "sess-stop-autopilot"), { recursive: true });
3726
+ await writeJson(join(stateDir, "sessions", "sess-stop-autopilot", "autopilot-state.json"), {
3086
3727
  active: true,
3087
3728
  current_phase: "execution",
3088
3729
  });
@@ -3107,8 +3748,8 @@ esac
3107
3748
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-planning-replay-"));
3108
3749
  try {
3109
3750
  const stateDir = join(cwd, ".omx", "state");
3110
- await mkdir(stateDir, { recursive: true });
3111
- await writeJson(join(stateDir, "autopilot-state.json"), {
3751
+ await mkdir(join(stateDir, "sessions", "sess-stop-autopilot-planning-replay"), { recursive: true });
3752
+ await writeJson(join(stateDir, "sessions", "sess-stop-autopilot-planning-replay", "autopilot-state.json"), {
3112
3753
  active: true,
3113
3754
  current_phase: "planning",
3114
3755
  });
@@ -3162,12 +3803,40 @@ esac
3162
3803
  await rm(cwd, { recursive: true, force: true });
3163
3804
  }
3164
3805
  });
3806
+ for (const staleRootCase of [
3807
+ { mode: "autopilot", phase: "execution" },
3808
+ { mode: "ultrawork", phase: "executing" },
3809
+ { mode: "ultraqa", phase: "diagnose" },
3810
+ ]) {
3811
+ it(`does not block Stop from stale root ${staleRootCase.mode} state when the explicit session directory is missing`, async () => {
3812
+ const cwd = await mkdtemp(join(tmpdir(), `omx-native-hook-stop-missing-session-${staleRootCase.mode}-`));
3813
+ try {
3814
+ const stateDir = join(cwd, ".omx", "state");
3815
+ await mkdir(stateDir, { recursive: true });
3816
+ await writeJson(join(stateDir, `${staleRootCase.mode}-state.json`), {
3817
+ active: true,
3818
+ mode: staleRootCase.mode,
3819
+ current_phase: staleRootCase.phase,
3820
+ });
3821
+ const result = await dispatchCodexNativeHook({
3822
+ hook_event_name: "Stop",
3823
+ cwd,
3824
+ session_id: "missing-session",
3825
+ }, { cwd });
3826
+ assert.equal(result.omxEventName, "stop");
3827
+ assert.equal(result.outputJson, null);
3828
+ }
3829
+ finally {
3830
+ await rm(cwd, { recursive: true, force: true });
3831
+ }
3832
+ });
3833
+ }
3165
3834
  it("does not block Stop when an explicit blocked_on_user run_outcome is present on a mode state", async () => {
3166
3835
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-blocked-outcome-"));
3167
3836
  try {
3168
3837
  const stateDir = join(cwd, ".omx", "state");
3169
- await mkdir(stateDir, { recursive: true });
3170
- await writeJson(join(stateDir, "autopilot-state.json"), {
3838
+ await mkdir(join(stateDir, "sessions", "sess-stop-autopilot-blocked-outcome"), { recursive: true });
3839
+ await writeJson(join(stateDir, "sessions", "sess-stop-autopilot-blocked-outcome", "autopilot-state.json"), {
3171
3840
  active: true,
3172
3841
  current_phase: "execution",
3173
3842
  run_outcome: "blocked_on_user",
@@ -3188,8 +3857,8 @@ esac
3188
3857
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultrawork-"));
3189
3858
  try {
3190
3859
  const stateDir = join(cwd, ".omx", "state");
3191
- await mkdir(stateDir, { recursive: true });
3192
- await writeJson(join(stateDir, "ultrawork-state.json"), {
3860
+ await mkdir(join(stateDir, "sessions", "sess-stop-ultrawork"), { recursive: true });
3861
+ await writeJson(join(stateDir, "sessions", "sess-stop-ultrawork", "ultrawork-state.json"), {
3193
3862
  active: true,
3194
3863
  current_phase: "executing",
3195
3864
  });
@@ -3209,8 +3878,8 @@ esac
3209
3878
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultraqa-"));
3210
3879
  try {
3211
3880
  const stateDir = join(cwd, ".omx", "state");
3212
- await mkdir(stateDir, { recursive: true });
3213
- await writeJson(join(stateDir, "ultraqa-state.json"), {
3881
+ await mkdir(join(stateDir, "sessions", "sess-stop-ultraqa"), { recursive: true });
3882
+ await writeJson(join(stateDir, "sessions", "sess-stop-ultraqa", "ultraqa-state.json"), {
3214
3883
  active: true,
3215
3884
  current_phase: "diagnose",
3216
3885
  });
@@ -3226,6 +3895,32 @@ esac
3226
3895
  await rm(cwd, { recursive: true, force: true });
3227
3896
  }
3228
3897
  });
3898
+ it("marks leader-owned team attention during native Stop dispatch without a polling watcher", async () => {
3899
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-attention-"));
3900
+ try {
3901
+ await initTeamState("stop-attention-team", "native stop attention", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-attention" });
3902
+ const result = await dispatchCodexNativeHook({
3903
+ hook_event_name: "Stop",
3904
+ cwd,
3905
+ session_id: "sess-stop-team-attention",
3906
+ }, { cwd });
3907
+ const attention = await readTeamLeaderAttention("stop-attention-team", cwd);
3908
+ assert.equal(result.omxEventName, "stop");
3909
+ assert.equal(attention?.source, "native_stop");
3910
+ assert.equal(attention?.leader_session_active, false);
3911
+ assert.equal(attention?.leader_session_id, "sess-stop-team-attention");
3912
+ assert.match(attention?.leader_session_stopped_at ?? "", /^\d{4}-\d{2}-\d{2}T/);
3913
+ assert.deepEqual(result.outputJson, {
3914
+ decision: "block",
3915
+ reason: `OMX team pipeline is still active (stop-attention-team) at phase team-exec; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
3916
+ stopReason: "team_team-exec",
3917
+ systemMessage: "OMX team pipeline is still active at phase team-exec.",
3918
+ });
3919
+ }
3920
+ finally {
3921
+ await rm(cwd, { recursive: true, force: true });
3922
+ }
3923
+ });
3229
3924
  it("returns Stop continuation output while team phase is non-terminal", async () => {
3230
3925
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-"));
3231
3926
  try {
@@ -3315,30 +4010,492 @@ esac
3315
4010
  process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
3316
4011
  else
3317
4012
  delete process.env.OMX_TEAM_STATE_ROOT;
3318
- if (typeof prevLeaderCwd === "string")
3319
- process.env.OMX_TEAM_LEADER_CWD = prevLeaderCwd;
4013
+ if (typeof prevLeaderCwd === "string")
4014
+ process.env.OMX_TEAM_LEADER_CWD = prevLeaderCwd;
4015
+ else
4016
+ delete process.env.OMX_TEAM_LEADER_CWD;
4017
+ await rm(cwd, { recursive: true, force: true });
4018
+ }
4019
+ });
4020
+ it("blocks Stop as a team-worker task failure when worker status is terminal but task evidence is not completed", async () => {
4021
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-terminal-stale-"));
4022
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
4023
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
4024
+ const prevLeaderCwd = process.env.OMX_TEAM_LEADER_CWD;
4025
+ try {
4026
+ await initTeamState("worker-stale-team", "worker stale stop fallback", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-stale" });
4027
+ const stateDir = join(cwd, ".omx", "state");
4028
+ const workerCwd = join(cwd, ".omx", "team", "worker-stale-team", "worktrees", "worker-1");
4029
+ const workerDir = join(stateDir, "team", "worker-stale-team", "workers", "worker-1");
4030
+ await mkdir(workerCwd, { recursive: true });
4031
+ await writeJson(join(workerDir, "identity.json"), {
4032
+ name: "worker-1",
4033
+ index: 1,
4034
+ role: "executor",
4035
+ assigned_tasks: ["1"],
4036
+ worktree_path: workerCwd,
4037
+ team_state_root: stateDir,
4038
+ });
4039
+ await writeJson(join(workerDir, "status.json"), {
4040
+ state: "done",
4041
+ current_task_id: "1",
4042
+ updated_at: new Date().toISOString(),
4043
+ });
4044
+ await writeJson(join(stateDir, "team", "worker-stale-team", "tasks", "task-1.json"), {
4045
+ id: "1",
4046
+ subject: "stale hook task",
4047
+ description: "non-completed task should still block terminal worker Stop",
4048
+ status: "in_progress",
4049
+ owner: "worker-1",
4050
+ created_at: new Date().toISOString(),
4051
+ });
4052
+ process.env.OMX_TEAM_WORKER = "worker-stale-team/worker-1";
4053
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
4054
+ process.env.OMX_TEAM_LEADER_CWD = cwd;
4055
+ const payload = {
4056
+ hook_event_name: "Stop",
4057
+ cwd: workerCwd,
4058
+ session_id: "sess-stop-team-worker-stale",
4059
+ thread_id: "thread-stop-team-worker-stale",
4060
+ };
4061
+ const result = await dispatchCodexNativeHook(payload, { cwd: workerCwd });
4062
+ const replay = await dispatchCodexNativeHook({ ...payload, stop_hook_active: true }, { cwd: workerCwd });
4063
+ assert.equal(result.outputJson?.stopReason, "team_worker_worker-1_1_in_progress");
4064
+ assert.equal(replay.outputJson, null);
4065
+ }
4066
+ finally {
4067
+ if (typeof prevTeamWorker === "string")
4068
+ process.env.OMX_TEAM_WORKER = prevTeamWorker;
4069
+ else
4070
+ delete process.env.OMX_TEAM_WORKER;
4071
+ if (typeof prevTeamStateRoot === "string")
4072
+ process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
4073
+ else
4074
+ delete process.env.OMX_TEAM_STATE_ROOT;
4075
+ if (typeof prevLeaderCwd === "string")
4076
+ process.env.OMX_TEAM_LEADER_CWD = prevLeaderCwd;
4077
+ else
4078
+ delete process.env.OMX_TEAM_LEADER_CWD;
4079
+ await rm(cwd, { recursive: true, force: true });
4080
+ }
4081
+ });
4082
+ it("re-blocks live team worker Stop replays but suppresses stale terminal worker repeats", async () => {
4083
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-repeat-"));
4084
+ try {
4085
+ await initTeamState("worker-repeat-team", "worker stop repeat guard", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-repeat" });
4086
+ const stateDir = join(cwd, ".omx", "state");
4087
+ const workerDir = join(stateDir, "team", "worker-repeat-team", "workers", "worker-1");
4088
+ const taskPath = join(stateDir, "team", "worker-repeat-team", "tasks", "task-1.json");
4089
+ const workerCwd = join(cwd, ".omx", "team", "worker-repeat-team", "worktrees", "worker-1");
4090
+ await mkdir(workerCwd, { recursive: true });
4091
+ await writeJson(join(workerDir, "identity.json"), {
4092
+ name: "worker-1",
4093
+ index: 1,
4094
+ role: "executor",
4095
+ assigned_tasks: ["1"],
4096
+ worktree_path: workerCwd,
4097
+ team_state_root: stateDir,
4098
+ });
4099
+ await writeJson(join(workerDir, "status.json"), {
4100
+ state: "working",
4101
+ current_task_id: "1",
4102
+ updated_at: new Date().toISOString(),
4103
+ });
4104
+ await writeJson(taskPath, {
4105
+ id: "1",
4106
+ subject: "hook task",
4107
+ description: "finish hook task",
4108
+ status: "in_progress",
4109
+ owner: "worker-1",
4110
+ created_at: new Date().toISOString(),
4111
+ });
4112
+ process.env.OMX_TEAM_WORKER = "worker-repeat-team/worker-1";
4113
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
4114
+ process.env.OMX_TEAM_LEADER_CWD = cwd;
4115
+ const basePayload = {
4116
+ hook_event_name: "Stop",
4117
+ cwd: workerCwd,
4118
+ session_id: "sess-stop-team-worker-repeat",
4119
+ thread_id: "thread-stop-team-worker-repeat",
4120
+ turn_id: "turn-stop-team-worker-repeat-1",
4121
+ last_assistant_message: "I need to stop before this task is done.",
4122
+ };
4123
+ const expectedInProgress = {
4124
+ decision: "block",
4125
+ reason: "OMX team worker worker-1 is still assigned non-terminal task 1 (in_progress); continue the current assigned task or report a concrete blocker before stopping.",
4126
+ stopReason: "team_worker_worker-1_1_in_progress",
4127
+ systemMessage: "OMX team worker worker-1 is still assigned task 1 (in_progress).",
4128
+ };
4129
+ const first = await dispatchCodexNativeHook(basePayload, { cwd: workerCwd });
4130
+ const replay = await dispatchCodexNativeHook({ ...basePayload, stop_hook_active: true }, { cwd: workerCwd });
4131
+ const freshTurn = await dispatchCodexNativeHook({ ...basePayload, turn_id: "turn-stop-team-worker-repeat-2", stop_hook_active: true }, { cwd: workerCwd });
4132
+ await writeJson(taskPath, {
4133
+ id: "1",
4134
+ subject: "hook task",
4135
+ description: "finish hook task",
4136
+ status: "blocked",
4137
+ owner: "worker-1",
4138
+ created_at: new Date().toISOString(),
4139
+ });
4140
+ const stateChanged = await dispatchCodexNativeHook({ ...basePayload, turn_id: "turn-stop-team-worker-repeat-3", stop_hook_active: true }, { cwd: workerCwd });
4141
+ assert.deepEqual(first.outputJson, expectedInProgress);
4142
+ assert.deepEqual(replay.outputJson, expectedInProgress);
4143
+ assert.deepEqual(freshTurn.outputJson, expectedInProgress);
4144
+ assert.deepEqual(stateChanged.outputJson, {
4145
+ decision: "block",
4146
+ reason: "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.",
4147
+ stopReason: "team_worker_worker-1_1_blocked",
4148
+ systemMessage: "OMX team worker worker-1 is still assigned task 1 (blocked).",
4149
+ });
4150
+ }
4151
+ finally {
4152
+ await rm(cwd, { recursive: true, force: true });
4153
+ }
4154
+ });
4155
+ it("allows Stop for a team worker when assigned task is terminal and bypasses generic team blocking", async () => {
4156
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-terminal-"));
4157
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
4158
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
4159
+ const prevPath = process.env.PATH;
4160
+ try {
4161
+ await initTeamState("worker-stop-team-terminal", "worker stop terminal fallback", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-terminal" });
4162
+ const fakeBinDir = join(cwd, "fake-bin");
4163
+ const tmuxLogPath = join(cwd, "tmux.log");
4164
+ await mkdir(fakeBinDir, { recursive: true });
4165
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
4166
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
4167
+ const workerDir = join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "workers", "worker-1");
4168
+ await writeJson(join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "config.json"), {
4169
+ name: "worker-stop-team-terminal",
4170
+ tmux_session: "omx-team-worker-stop",
4171
+ leader_pane_id: "%42",
4172
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
4173
+ });
4174
+ await writeJson(join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "manifest.v2.json"), {
4175
+ name: "worker-stop-team-terminal",
4176
+ tmux_session: "omx-team-worker-stop",
4177
+ leader_pane_id: "%42",
4178
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
4179
+ });
4180
+ await writeJson(join(workerDir, "identity.json"), {
4181
+ name: "worker-1",
4182
+ index: 1,
4183
+ role: "executor",
4184
+ assigned_tasks: ["1"],
4185
+ worktree_path: cwd,
4186
+ team_state_root: join(cwd, ".omx", "state"),
4187
+ });
4188
+ await writeJson(join(workerDir, "status.json"), {
4189
+ state: "done",
4190
+ current_task_id: "1",
4191
+ updated_at: new Date().toISOString(),
4192
+ });
4193
+ await writeJson(join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "tasks", "task-1.json"), {
4194
+ id: "1",
4195
+ subject: "hook task",
4196
+ description: "finish hook task",
4197
+ status: "completed",
4198
+ owner: "worker-1",
4199
+ created_at: new Date().toISOString(),
4200
+ });
4201
+ process.env.OMX_TEAM_WORKER = "worker-stop-team-terminal/worker-1";
4202
+ process.env.OMX_TEAM_STATE_ROOT = join(cwd, ".omx", "state");
4203
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
4204
+ const result = await dispatchCodexNativeHook({
4205
+ hook_event_name: "Stop",
4206
+ cwd,
4207
+ session_id: "sess-stop-team-worker-terminal",
4208
+ }, { cwd });
4209
+ const replay = await dispatchCodexNativeHook({
4210
+ hook_event_name: "Stop",
4211
+ cwd,
4212
+ session_id: "sess-stop-team-worker-terminal",
4213
+ turn_id: "turn-worker-stop-terminal-replay",
4214
+ }, { cwd });
4215
+ assert.equal(result.outputJson, null);
4216
+ assert.equal(replay.outputJson, null);
4217
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
4218
+ const stopNudges = tmuxLog.match(/send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/g) || [];
4219
+ assert.equal(stopNudges.length, 1, "allowed worker Stop should nudge leader exactly once inside cooldown");
4220
+ const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
4221
+ assert.equal(nudgeState.delivery, "sent");
4222
+ }
4223
+ finally {
4224
+ if (typeof prevTeamWorker === "string")
4225
+ process.env.OMX_TEAM_WORKER = prevTeamWorker;
4226
+ else
4227
+ delete process.env.OMX_TEAM_WORKER;
4228
+ if (typeof prevTeamStateRoot === "string")
4229
+ process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
4230
+ else
4231
+ delete process.env.OMX_TEAM_STATE_ROOT;
4232
+ if (typeof prevPath === "string")
4233
+ process.env.PATH = prevPath;
4234
+ else
4235
+ delete process.env.PATH;
4236
+ await rm(cwd, { recursive: true, force: true });
4237
+ }
4238
+ });
4239
+ it("allows worker Stop when the Stop nudge helper cannot deliver", async () => {
4240
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-helper-fail-"));
4241
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
4242
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
4243
+ const prevPath = process.env.PATH;
4244
+ try {
4245
+ await initTeamState("worker-stop-helper-fail", "worker stop helper failure", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-helper-fail" });
4246
+ const fakeBinDir = join(cwd, "fake-bin");
4247
+ await mkdir(fakeBinDir, { recursive: true });
4248
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(join(cwd, "tmux.log"), { failSend: true }));
4249
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
4250
+ const stateDir = join(cwd, ".omx", "state");
4251
+ const workerDir = join(stateDir, "team", "worker-stop-helper-fail", "workers", "worker-1");
4252
+ await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "config.json"), {
4253
+ name: "worker-stop-helper-fail",
4254
+ tmux_session: "omx-team-worker-stop",
4255
+ leader_pane_id: "%42",
4256
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
4257
+ });
4258
+ await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "manifest.v2.json"), {
4259
+ name: "worker-stop-helper-fail",
4260
+ tmux_session: "omx-team-worker-stop",
4261
+ leader_pane_id: "%42",
4262
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
4263
+ });
4264
+ await writeJson(join(workerDir, "identity.json"), {
4265
+ name: "worker-1",
4266
+ assigned_tasks: ["1"],
4267
+ team_state_root: stateDir,
4268
+ });
4269
+ await writeJson(join(workerDir, "status.json"), {
4270
+ state: "done",
4271
+ current_task_id: "1",
4272
+ updated_at: new Date().toISOString(),
4273
+ });
4274
+ await writeJson(join(stateDir, "team", "worker-stop-helper-fail", "tasks", "task-1.json"), {
4275
+ id: "1",
4276
+ status: "completed",
4277
+ owner: "worker-1",
4278
+ });
4279
+ process.env.OMX_TEAM_WORKER = "worker-stop-helper-fail/worker-1";
4280
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
4281
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
4282
+ const result = await dispatchCodexNativeHook({ hook_event_name: "Stop", cwd, session_id: "sess-stop-team-worker-helper-fail" }, { cwd });
4283
+ assert.equal(result.outputJson, null);
4284
+ const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
4285
+ assert.equal(nudgeState.delivery, "deferred");
4286
+ }
4287
+ finally {
4288
+ if (typeof prevTeamWorker === "string")
4289
+ process.env.OMX_TEAM_WORKER = prevTeamWorker;
4290
+ else
4291
+ delete process.env.OMX_TEAM_WORKER;
4292
+ if (typeof prevTeamStateRoot === "string")
4293
+ process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
4294
+ else
4295
+ delete process.env.OMX_TEAM_STATE_ROOT;
4296
+ if (typeof prevPath === "string")
4297
+ process.env.PATH = prevPath;
4298
+ else
4299
+ delete process.env.PATH;
4300
+ await rm(cwd, { recursive: true, force: true });
4301
+ }
4302
+ });
4303
+ it("does not treat failed or ambiguous worker task state as completed Stop evidence", async () => {
4304
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-failed-"));
4305
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
4306
+ const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
4307
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
4308
+ const prevPath = process.env.PATH;
4309
+ try {
4310
+ await initTeamState("worker-stop-failed-task", "worker stop failed task", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-failed" });
4311
+ const fakeBinDir = join(cwd, "fake-bin");
4312
+ const tmuxLogPath = join(cwd, "tmux.log");
4313
+ await mkdir(fakeBinDir, { recursive: true });
4314
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
4315
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
4316
+ const stateDir = join(cwd, ".omx", "state");
4317
+ const workerDir = join(stateDir, "team", "worker-stop-failed-task", "workers", "worker-1");
4318
+ await writeJson(join(stateDir, "team", "worker-stop-failed-task", "config.json"), {
4319
+ name: "worker-stop-failed-task",
4320
+ tmux_session: "omx-team-worker-stop",
4321
+ leader_pane_id: "%42",
4322
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
4323
+ });
4324
+ await writeJson(join(workerDir, "identity.json"), {
4325
+ name: "worker-1",
4326
+ assigned_tasks: ["1"],
4327
+ team_state_root: stateDir,
4328
+ });
4329
+ await writeJson(join(workerDir, "status.json"), {
4330
+ state: "failed",
4331
+ current_task_id: "1",
4332
+ updated_at: new Date().toISOString(),
4333
+ });
4334
+ await writeJson(join(stateDir, "team", "worker-stop-failed-task", "tasks", "task-1.json"), {
4335
+ id: "1",
4336
+ status: "failed",
4337
+ owner: "worker-1",
4338
+ });
4339
+ process.env.OMX_TEAM_WORKER = "worker-stop-failed-task/worker-1";
4340
+ delete process.env.OMX_TEAM_INTERNAL_WORKER;
4341
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
4342
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
4343
+ const result = await dispatchCodexNativeHook({
4344
+ hook_event_name: "Stop",
4345
+ cwd,
4346
+ session_id: "sess-stop-team-worker-failed",
4347
+ thread_id: "thread-stop-team-worker-failed",
4348
+ turn_id: "turn-stop-team-worker-failed",
4349
+ }, { cwd });
4350
+ assert.equal(result.outputJson?.decision, "block");
4351
+ assert.match(String(result.outputJson?.stopReason || ""), /non_completed_task_1_failed/);
4352
+ assert.match(JSON.stringify(result.outputJson), /team/i);
4353
+ assert.equal(existsSync(join(workerDir, "worker-stop-nudge.json")), false);
4354
+ const tmuxLog = existsSync(tmuxLogPath) ? await readFile(tmuxLogPath, "utf-8") : "";
4355
+ assert.doesNotMatch(tmuxLog, /native Stop allowed/);
4356
+ }
4357
+ finally {
4358
+ if (typeof prevTeamWorker === "string")
4359
+ process.env.OMX_TEAM_WORKER = prevTeamWorker;
4360
+ else
4361
+ delete process.env.OMX_TEAM_WORKER;
4362
+ if (typeof prevInternalTeamWorker === "string")
4363
+ process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
4364
+ else
4365
+ delete process.env.OMX_TEAM_INTERNAL_WORKER;
4366
+ if (typeof prevTeamStateRoot === "string")
4367
+ process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
4368
+ else
4369
+ delete process.env.OMX_TEAM_STATE_ROOT;
4370
+ if (typeof prevPath === "string")
4371
+ process.env.PATH = prevPath;
4372
+ else
4373
+ delete process.env.PATH;
4374
+ await rm(cwd, { recursive: true, force: true });
4375
+ }
4376
+ });
4377
+ it("blocks worker Stop on missing task assignment without relying on generic team state", async () => {
4378
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-missing-assignment-"));
4379
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
4380
+ const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
4381
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
4382
+ try {
4383
+ const stateDir = join(cwd, ".omx", "state");
4384
+ const workerDir = join(stateDir, "team", "worker-missing-assignment", "workers", "worker-1");
4385
+ await mkdir(workerDir, { recursive: true });
4386
+ await writeJson(join(workerDir, "identity.json"), {
4387
+ name: "worker-1",
4388
+ assigned_tasks: [],
4389
+ team_state_root: stateDir,
4390
+ });
4391
+ await writeJson(join(workerDir, "status.json"), {
4392
+ state: "idle",
4393
+ updated_at: new Date().toISOString(),
4394
+ });
4395
+ process.env.OMX_TEAM_WORKER = "worker-missing-assignment/worker-1";
4396
+ delete process.env.OMX_TEAM_INTERNAL_WORKER;
4397
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
4398
+ const result = await dispatchCodexNativeHook({
4399
+ hook_event_name: "Stop",
4400
+ cwd,
4401
+ session_id: "sess-stop-team-worker-missing-assignment",
4402
+ thread_id: "thread-stop-team-worker-missing-assignment",
4403
+ turn_id: "turn-stop-team-worker-missing-assignment",
4404
+ }, { cwd });
4405
+ assert.equal(result.outputJson?.decision, "block");
4406
+ assert.equal(result.outputJson?.stopReason, "team_worker_worker-1_missing_task_assignment");
4407
+ assert.equal(existsSync(join(workerDir, "worker-stop-nudge.json")), false);
4408
+ }
4409
+ finally {
4410
+ if (typeof prevTeamWorker === "string")
4411
+ process.env.OMX_TEAM_WORKER = prevTeamWorker;
4412
+ else
4413
+ delete process.env.OMX_TEAM_WORKER;
4414
+ if (typeof prevInternalTeamWorker === "string")
4415
+ process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
4416
+ else
4417
+ delete process.env.OMX_TEAM_INTERNAL_WORKER;
4418
+ if (typeof prevTeamStateRoot === "string")
4419
+ process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
4420
+ else
4421
+ delete process.env.OMX_TEAM_STATE_ROOT;
4422
+ await rm(cwd, { recursive: true, force: true });
4423
+ }
4424
+ });
4425
+ it("blocks unresolved worker Stop before generic auto-nudge can bypass it", async () => {
4426
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-missing-state-"));
4427
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
4428
+ const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
4429
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
4430
+ const prevPath = process.env.PATH;
4431
+ try {
4432
+ const stateDir = join(cwd, ".omx", "state");
4433
+ const fakeBinDir = join(cwd, "fake-bin");
4434
+ const tmuxLogPath = join(cwd, "tmux.log");
4435
+ await mkdir(fakeBinDir, { recursive: true });
4436
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
4437
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
4438
+ process.env.OMX_TEAM_WORKER = "worker-missing-state/worker-1";
4439
+ delete process.env.OMX_TEAM_INTERNAL_WORKER;
4440
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
4441
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
4442
+ const result = await dispatchCodexNativeHook({
4443
+ hook_event_name: "Stop",
4444
+ cwd,
4445
+ session_id: "sess-stop-team-worker-missing-state",
4446
+ thread_id: "thread-stop-team-worker-missing-state",
4447
+ turn_id: "turn-stop-team-worker-missing-state",
4448
+ last_assistant_message: "Should I proceed?",
4449
+ }, { cwd });
4450
+ assert.equal(result.outputJson?.decision, "block");
4451
+ assert.equal(result.outputJson?.stopReason, "team_worker_worker-1_missing_worker_state");
4452
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /auto_nudge/);
4453
+ const tmuxLog = existsSync(tmuxLogPath) ? await readFile(tmuxLogPath, "utf-8") : "";
4454
+ assert.doesNotMatch(tmuxLog, /native Stop allowed/);
4455
+ }
4456
+ finally {
4457
+ if (typeof prevTeamWorker === "string")
4458
+ process.env.OMX_TEAM_WORKER = prevTeamWorker;
4459
+ else
4460
+ delete process.env.OMX_TEAM_WORKER;
4461
+ if (typeof prevInternalTeamWorker === "string")
4462
+ process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
4463
+ else
4464
+ delete process.env.OMX_TEAM_INTERNAL_WORKER;
4465
+ if (typeof prevTeamStateRoot === "string")
4466
+ process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
4467
+ else
4468
+ delete process.env.OMX_TEAM_STATE_ROOT;
4469
+ if (typeof prevPath === "string")
4470
+ process.env.PATH = prevPath;
3320
4471
  else
3321
- delete process.env.OMX_TEAM_LEADER_CWD;
4472
+ delete process.env.PATH;
3322
4473
  await rm(cwd, { recursive: true, force: true });
3323
4474
  }
3324
4475
  });
3325
- it("does not block Stop as a team-worker task failure when worker status is already terminal", async () => {
3326
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-terminal-stale-"));
4476
+ it("prefers canonical internal worker identity over public worker identity for Stop nudges", async () => {
4477
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-internal-env-"));
3327
4478
  const prevTeamWorker = process.env.OMX_TEAM_WORKER;
4479
+ const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
3328
4480
  const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
3329
- const prevLeaderCwd = process.env.OMX_TEAM_LEADER_CWD;
4481
+ const prevPath = process.env.PATH;
3330
4482
  try {
3331
- await initTeamState("worker-stale-team", "worker stale stop fallback", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-stale" });
3332
4483
  const stateDir = join(cwd, ".omx", "state");
3333
- const workerCwd = join(cwd, ".omx", "team", "worker-stale-team", "worktrees", "worker-1");
3334
- const workerDir = join(stateDir, "team", "worker-stale-team", "workers", "worker-1");
3335
- await mkdir(workerCwd, { recursive: true });
4484
+ const fakeBinDir = join(cwd, "fake-bin");
4485
+ const tmuxLogPath = join(cwd, "tmux.log");
4486
+ await mkdir(fakeBinDir, { recursive: true });
4487
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
4488
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
4489
+ const workerDir = join(stateDir, "team", "internal-stop-team", "workers", "worker-1");
4490
+ await writeJson(join(stateDir, "team", "internal-stop-team", "config.json"), {
4491
+ name: "internal-stop-team",
4492
+ tmux_session: "omx-team-worker-stop",
4493
+ leader_pane_id: "%42",
4494
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
4495
+ });
3336
4496
  await writeJson(join(workerDir, "identity.json"), {
3337
4497
  name: "worker-1",
3338
- index: 1,
3339
- role: "executor",
3340
4498
  assigned_tasks: ["1"],
3341
- worktree_path: workerCwd,
3342
4499
  team_state_root: stateDir,
3343
4500
  });
3344
4501
  await writeJson(join(workerDir, "status.json"), {
@@ -3346,156 +4503,121 @@ esac
3346
4503
  current_task_id: "1",
3347
4504
  updated_at: new Date().toISOString(),
3348
4505
  });
3349
- await writeJson(join(stateDir, "team", "worker-stale-team", "tasks", "task-1.json"), {
4506
+ await writeJson(join(stateDir, "team", "internal-stop-team", "tasks", "task-1.json"), {
3350
4507
  id: "1",
3351
- subject: "stale hook task",
3352
- description: "stale task should not trap terminal worker Stop",
3353
- status: "in_progress",
4508
+ status: "completed",
3354
4509
  owner: "worker-1",
3355
- created_at: new Date().toISOString(),
3356
4510
  });
3357
- process.env.OMX_TEAM_WORKER = "worker-stale-team/worker-1";
4511
+ process.env.OMX_TEAM_WORKER = "public-stop-team/worker-1";
4512
+ process.env.OMX_TEAM_INTERNAL_WORKER = "internal-stop-team/worker-1";
3358
4513
  process.env.OMX_TEAM_STATE_ROOT = stateDir;
3359
- process.env.OMX_TEAM_LEADER_CWD = cwd;
4514
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
3360
4515
  const result = await dispatchCodexNativeHook({
3361
4516
  hook_event_name: "Stop",
3362
- cwd: workerCwd,
3363
- session_id: "sess-stop-team-worker-stale",
3364
- }, { cwd: workerCwd });
3365
- assert.equal(result.outputJson?.stopReason, "team_team-exec");
4517
+ cwd,
4518
+ session_id: "sess-stop-team-worker-internal-env",
4519
+ thread_id: "thread-stop-team-worker-internal-env",
4520
+ turn_id: "turn-stop-team-worker-internal-env",
4521
+ }, { cwd });
4522
+ assert.equal(result.outputJson, null);
4523
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
4524
+ assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
4525
+ assert.equal(existsSync(join(workerDir, "worker-stop-nudge.json")), true);
3366
4526
  }
3367
4527
  finally {
3368
4528
  if (typeof prevTeamWorker === "string")
3369
4529
  process.env.OMX_TEAM_WORKER = prevTeamWorker;
3370
4530
  else
3371
4531
  delete process.env.OMX_TEAM_WORKER;
4532
+ if (typeof prevInternalTeamWorker === "string")
4533
+ process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
4534
+ else
4535
+ delete process.env.OMX_TEAM_INTERNAL_WORKER;
3372
4536
  if (typeof prevTeamStateRoot === "string")
3373
4537
  process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
3374
4538
  else
3375
4539
  delete process.env.OMX_TEAM_STATE_ROOT;
3376
- if (typeof prevLeaderCwd === "string")
3377
- process.env.OMX_TEAM_LEADER_CWD = prevLeaderCwd;
4540
+ if (typeof prevPath === "string")
4541
+ process.env.PATH = prevPath;
3378
4542
  else
3379
- delete process.env.OMX_TEAM_LEADER_CWD;
4543
+ delete process.env.PATH;
3380
4544
  await rm(cwd, { recursive: true, force: true });
3381
4545
  }
3382
4546
  });
3383
- it("suppresses identical team worker Stop replays but re-blocks fresh turns and task state changes", async () => {
3384
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-repeat-"));
4547
+ it("blocks worker Stop when canonical task ownership has a newer non-terminal task", async () => {
4548
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-owned-task-"));
4549
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
4550
+ const prevInternalTeamWorker = process.env.OMX_TEAM_INTERNAL_WORKER;
4551
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
4552
+ const prevPath = process.env.PATH;
3385
4553
  try {
3386
- await initTeamState("worker-repeat-team", "worker stop repeat guard", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-repeat" });
3387
4554
  const stateDir = join(cwd, ".omx", "state");
3388
- const workerDir = join(stateDir, "team", "worker-repeat-team", "workers", "worker-1");
3389
- const taskPath = join(stateDir, "team", "worker-repeat-team", "tasks", "task-1.json");
3390
- const workerCwd = join(cwd, ".omx", "team", "worker-repeat-team", "worktrees", "worker-1");
3391
- await mkdir(workerCwd, { recursive: true });
4555
+ const fakeBinDir = join(cwd, "fake-bin");
4556
+ const tmuxLogPath = join(cwd, "tmux.log");
4557
+ await mkdir(fakeBinDir, { recursive: true });
4558
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
4559
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
4560
+ const workerDir = join(stateDir, "team", "worker-owned-task", "workers", "worker-1");
4561
+ await writeJson(join(stateDir, "team", "worker-owned-task", "config.json"), {
4562
+ name: "worker-owned-task",
4563
+ tmux_session: "omx-team-worker-stop",
4564
+ leader_pane_id: "%42",
4565
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
4566
+ });
3392
4567
  await writeJson(join(workerDir, "identity.json"), {
3393
4568
  name: "worker-1",
3394
- index: 1,
3395
- role: "executor",
3396
4569
  assigned_tasks: ["1"],
3397
- worktree_path: workerCwd,
3398
4570
  team_state_root: stateDir,
3399
4571
  });
3400
- await writeJson(join(workerDir, "status.json"), {
3401
- state: "working",
3402
- current_task_id: "1",
3403
- updated_at: new Date().toISOString(),
3404
- });
3405
- await writeJson(taskPath, {
3406
- id: "1",
3407
- subject: "hook task",
3408
- description: "finish hook task",
3409
- status: "in_progress",
3410
- owner: "worker-1",
3411
- created_at: new Date().toISOString(),
3412
- });
3413
- process.env.OMX_TEAM_WORKER = "worker-repeat-team/worker-1";
3414
- process.env.OMX_TEAM_STATE_ROOT = stateDir;
3415
- process.env.OMX_TEAM_LEADER_CWD = cwd;
3416
- const basePayload = {
3417
- hook_event_name: "Stop",
3418
- cwd: workerCwd,
3419
- session_id: "sess-stop-team-worker-repeat",
3420
- thread_id: "thread-stop-team-worker-repeat",
3421
- turn_id: "turn-stop-team-worker-repeat-1",
3422
- last_assistant_message: "I need to stop before this task is done.",
3423
- };
3424
- const expectedInProgress = {
3425
- decision: "block",
3426
- reason: "OMX team worker worker-1 is still assigned non-terminal task 1 (in_progress); continue the current assigned task or report a concrete blocker before stopping.",
3427
- stopReason: "team_worker_worker-1_1_in_progress",
3428
- systemMessage: "OMX team worker worker-1 is still assigned task 1 (in_progress).",
3429
- };
3430
- const first = await dispatchCodexNativeHook(basePayload, { cwd: workerCwd });
3431
- const replay = await dispatchCodexNativeHook({ ...basePayload, stop_hook_active: true }, { cwd: workerCwd });
3432
- const freshTurn = await dispatchCodexNativeHook({ ...basePayload, turn_id: "turn-stop-team-worker-repeat-2", stop_hook_active: true }, { cwd: workerCwd });
3433
- await writeJson(taskPath, {
3434
- id: "1",
3435
- subject: "hook task",
3436
- description: "finish hook task",
3437
- status: "blocked",
3438
- owner: "worker-1",
3439
- created_at: new Date().toISOString(),
3440
- });
3441
- const stateChanged = await dispatchCodexNativeHook({ ...basePayload, turn_id: "turn-stop-team-worker-repeat-2", stop_hook_active: true }, { cwd: workerCwd });
3442
- assert.deepEqual(first.outputJson, expectedInProgress);
3443
- assert.deepEqual(replay.outputJson, null);
3444
- assert.deepEqual(freshTurn.outputJson, expectedInProgress);
3445
- assert.deepEqual(stateChanged.outputJson, {
3446
- decision: "block",
3447
- reason: "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.",
3448
- stopReason: "team_worker_worker-1_1_blocked",
3449
- systemMessage: "OMX team worker worker-1 is still assigned task 1 (blocked).",
3450
- });
3451
- }
3452
- finally {
3453
- await rm(cwd, { recursive: true, force: true });
3454
- }
3455
- });
3456
- it("does not block Stop for a team worker when assigned task is terminal", async () => {
3457
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-terminal-"));
3458
- const prevTeamWorker = process.env.OMX_TEAM_WORKER;
3459
- const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
3460
- try {
3461
- await initTeamState("worker-stop-team-terminal", "worker stop terminal fallback", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-terminal" });
3462
- const workerDir = join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "workers", "worker-1");
3463
4572
  await writeJson(join(workerDir, "status.json"), {
3464
4573
  state: "done",
3465
4574
  current_task_id: "1",
3466
4575
  updated_at: new Date().toISOString(),
3467
4576
  });
3468
- await writeJson(join(cwd, ".omx", "state", "team", "worker-stop-team-terminal", "tasks", "task-1.json"), {
4577
+ await writeJson(join(stateDir, "team", "worker-owned-task", "tasks", "task-1.json"), {
3469
4578
  id: "1",
3470
- subject: "hook task",
3471
- description: "finish hook task",
3472
4579
  status: "completed",
3473
4580
  owner: "worker-1",
3474
- created_at: new Date().toISOString(),
3475
4581
  });
3476
- process.env.OMX_TEAM_WORKER = "worker-stop-team-terminal/worker-1";
3477
- process.env.OMX_TEAM_STATE_ROOT = join(cwd, ".omx", "state");
4582
+ await writeJson(join(stateDir, "team", "worker-owned-task", "tasks", "task-2.json"), {
4583
+ id: "2",
4584
+ status: "in_progress",
4585
+ owner: "worker-1",
4586
+ });
4587
+ process.env.OMX_TEAM_WORKER = "worker-owned-task/worker-1";
4588
+ delete process.env.OMX_TEAM_INTERNAL_WORKER;
4589
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
4590
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
3478
4591
  const result = await dispatchCodexNativeHook({
3479
4592
  hook_event_name: "Stop",
3480
4593
  cwd,
3481
- session_id: "sess-stop-team-worker-terminal",
4594
+ session_id: "sess-stop-team-worker-owned-task",
4595
+ thread_id: "thread-stop-team-worker-owned-task",
4596
+ turn_id: "turn-stop-team-worker-owned-task",
3482
4597
  }, { cwd });
3483
- assert.deepEqual(result.outputJson, {
3484
- decision: "block",
3485
- reason: `OMX team pipeline is still active (worker-stop-team-terminal) at phase team-exec; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
3486
- stopReason: "team_team-exec",
3487
- systemMessage: "OMX team pipeline is still active at phase team-exec.",
3488
- });
4598
+ assert.equal(result.outputJson?.decision, "block");
4599
+ assert.equal(result.outputJson?.stopReason, "team_worker_worker-1_2_in_progress");
4600
+ assert.equal(existsSync(join(workerDir, "worker-stop-nudge.json")), false);
4601
+ const tmuxLog = existsSync(tmuxLogPath) ? await readFile(tmuxLogPath, "utf-8") : "";
4602
+ assert.doesNotMatch(tmuxLog, /native Stop allowed/);
3489
4603
  }
3490
4604
  finally {
3491
4605
  if (typeof prevTeamWorker === "string")
3492
4606
  process.env.OMX_TEAM_WORKER = prevTeamWorker;
3493
4607
  else
3494
4608
  delete process.env.OMX_TEAM_WORKER;
4609
+ if (typeof prevInternalTeamWorker === "string")
4610
+ process.env.OMX_TEAM_INTERNAL_WORKER = prevInternalTeamWorker;
4611
+ else
4612
+ delete process.env.OMX_TEAM_INTERNAL_WORKER;
3495
4613
  if (typeof prevTeamStateRoot === "string")
3496
4614
  process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
3497
4615
  else
3498
4616
  delete process.env.OMX_TEAM_STATE_ROOT;
4617
+ if (typeof prevPath === "string")
4618
+ process.env.PATH = prevPath;
4619
+ else
4620
+ delete process.env.PATH;
3499
4621
  await rm(cwd, { recursive: true, force: true });
3500
4622
  }
3501
4623
  });
@@ -3805,12 +4927,12 @@ esac
3805
4927
  session_id: "sess-stop-skill",
3806
4928
  }, { cwd });
3807
4929
  assert.equal(result.omxEventName, "stop");
3808
- assert.deepEqual(result.outputJson, {
3809
- decision: "block",
3810
- reason: "OMX skill ralplan is still active (phase: planning); continue until the current ralplan workflow reaches a terminal state.",
3811
- stopReason: "skill_ralplan_planning",
3812
- systemMessage: "OMX skill ralplan is still active (phase: planning).",
3813
- });
4930
+ assert.equal(result.outputJson?.decision, "block");
4931
+ assert.match(String(result.outputJson?.reason ?? ""), /Status: continue_from_artifact/);
4932
+ assert.match(String(result.outputJson?.reason ?? ""), /ralplan is still active \(phase: planning\)/);
4933
+ assert.match(String(result.outputJson?.reason ?? ""), /continue from the current ralplan artifact/i);
4934
+ assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
4935
+ assert.match(String(result.outputJson?.systemMessage ?? ""), /complete, paused for review, waiting for input, or still continuing/);
3814
4936
  }
3815
4937
  finally {
3816
4938
  await rm(cwd, { recursive: true, force: true });
@@ -3930,7 +5052,7 @@ esac
3930
5052
  await rm(cwd, { recursive: true, force: true });
3931
5053
  }
3932
5054
  });
3933
- it("does not block on active ralplan skill when subagents are still active", async () => {
5055
+ it("returns an explicit ralplan waiting status while subagents are still active", async () => {
3934
5056
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-subagent-"));
3935
5057
  try {
3936
5058
  const stateDir = join(cwd, ".omx", "state");
@@ -3977,7 +5099,11 @@ esac
3977
5099
  session_id: "sess-stop-skill-subagent",
3978
5100
  }, { cwd });
3979
5101
  assert.equal(result.omxEventName, "stop");
3980
- assert.equal(result.outputJson, null);
5102
+ assert.equal(result.outputJson?.decision, "block");
5103
+ assert.match(String(result.outputJson?.reason ?? ""), /Status: waiting/);
5104
+ assert.match(String(result.outputJson?.reason ?? ""), /waiting for 1 active native subagent thread/);
5105
+ assert.match(String(result.outputJson?.reason ?? ""), /then continue from the current ralplan artifact/i);
5106
+ assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_waiting_subagent");
3981
5107
  }
3982
5108
  finally {
3983
5109
  await rm(cwd, { recursive: true, force: true });
@@ -4096,6 +5222,31 @@ esac
4096
5222
  await rm(cwd, { recursive: true, force: true });
4097
5223
  }
4098
5224
  });
5225
+ it("does not block Stop from stale root autoresearch state when the explicit session directory is missing", async () => {
5226
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-missing-session-autoresearch-"));
5227
+ try {
5228
+ const stateDir = join(cwd, ".omx", "state");
5229
+ await mkdir(stateDir, { recursive: true });
5230
+ await writeJson(join(stateDir, "autoresearch-state.json"), {
5231
+ active: true,
5232
+ mode: "autoresearch",
5233
+ current_phase: "executing",
5234
+ validation_mode: "mission-validator-script",
5235
+ mission_validator_command: "node scripts/validate.js",
5236
+ completion_artifact_path: ".omx/specs/autoresearch-demo/completion.json",
5237
+ });
5238
+ const result = await dispatchCodexNativeHook({
5239
+ hook_event_name: "Stop",
5240
+ cwd,
5241
+ session_id: "missing-session",
5242
+ }, { cwd });
5243
+ assert.equal(result.omxEventName, "stop");
5244
+ assert.equal(result.outputJson, null);
5245
+ }
5246
+ finally {
5247
+ await rm(cwd, { recursive: true, force: true });
5248
+ }
5249
+ });
4099
5250
  it("does not block Stop solely because deep-interview is active", async () => {
4100
5251
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-"));
4101
5252
  try {
@@ -4831,6 +5982,46 @@ esac
4831
5982
  await rm(cwd, { recursive: true, force: true });
4832
5983
  }
4833
5984
  });
5985
+ it("does not block Stop when Ralph skill-active initialization points at another session", async () => {
5986
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-stale-skill-active-"));
5987
+ try {
5988
+ const stateDir = join(cwd, ".omx", "state");
5989
+ const currentSessionId = "sess-current-ralph";
5990
+ await mkdir(join(stateDir, "sessions", currentSessionId), { recursive: true });
5991
+ await writeJson(join(stateDir, "session.json"), {
5992
+ session_id: currentSessionId,
5993
+ native_session_id: currentSessionId,
5994
+ cwd,
5995
+ });
5996
+ await writeJson(join(stateDir, "sessions", currentSessionId, "ralph-state.json"), {
5997
+ active: true,
5998
+ mode: "ralph",
5999
+ current_phase: "verifying",
6000
+ session_id: currentSessionId,
6001
+ owner_omx_session_id: currentSessionId,
6002
+ task_slug: "stale-rebound-task",
6003
+ });
6004
+ await writeJson(join(stateDir, "sessions", currentSessionId, "skill-active-state.json"), {
6005
+ active: true,
6006
+ skill: "ralph",
6007
+ phase: "verifying",
6008
+ session_id: currentSessionId,
6009
+ initialized_mode: "ralph",
6010
+ initialized_state_path: ".omx/state/sessions/sess-old-ralph/ralph-state.json",
6011
+ active_skills: [{ skill: "ralph", phase: "verifying", active: true, session_id: currentSessionId }],
6012
+ });
6013
+ const result = await dispatchCodexNativeHook({
6014
+ hook_event_name: "Stop",
6015
+ cwd,
6016
+ session_id: currentSessionId,
6017
+ }, { cwd });
6018
+ assert.equal(result.omxEventName, "stop");
6019
+ assert.equal(result.outputJson, null);
6020
+ }
6021
+ finally {
6022
+ await rm(cwd, { recursive: true, force: true });
6023
+ }
6024
+ });
4834
6025
  it("blocks same-session Ralph Stop continuation when ownership identifiers match", async () => {
4835
6026
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-owned-session-"));
4836
6027
  const previousTmuxPane = process.env.TMUX_PANE;
@@ -4877,6 +6068,78 @@ esac
4877
6068
  await rm(cwd, { recursive: true, force: true });
4878
6069
  }
4879
6070
  });
6071
+ it("allows native verifier subagent Stop to complete while leader Ralph remains active", async () => {
6072
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-subagent-verdict-"));
6073
+ try {
6074
+ const stateDir = join(cwd, ".omx", "state");
6075
+ const omxSessionId = "sess-ralph-leader-verifier";
6076
+ const leaderNativeSessionId = "codex-ralph-leader-verifier";
6077
+ const childNativeSessionId = "codex-verifier-child";
6078
+ await mkdir(join(stateDir, "sessions", omxSessionId), { recursive: true });
6079
+ await writeSessionStart(cwd, omxSessionId, {
6080
+ nativeSessionId: leaderNativeSessionId,
6081
+ });
6082
+ await writeJson(join(stateDir, "sessions", omxSessionId, "ralph-state.json"), {
6083
+ active: true,
6084
+ mode: "ralph",
6085
+ current_phase: "verifying",
6086
+ session_id: omxSessionId,
6087
+ owner_omx_session_id: omxSessionId,
6088
+ owner_codex_session_id: leaderNativeSessionId,
6089
+ });
6090
+ const transcriptPath = join(cwd, "verifier-subagent-rollout.jsonl");
6091
+ await writeFile(transcriptPath, `${JSON.stringify({
6092
+ type: "session_meta",
6093
+ payload: {
6094
+ id: childNativeSessionId,
6095
+ source: {
6096
+ subagent: {
6097
+ thread_spawn: {
6098
+ parent_thread_id: leaderNativeSessionId,
6099
+ depth: 1,
6100
+ agent_nickname: "Verifier",
6101
+ agent_role: "verifier",
6102
+ },
6103
+ },
6104
+ },
6105
+ agent_nickname: "Verifier",
6106
+ agent_role: "verifier",
6107
+ },
6108
+ })}\n`);
6109
+ await dispatchCodexNativeHook({
6110
+ hook_event_name: "SessionStart",
6111
+ cwd,
6112
+ session_id: childNativeSessionId,
6113
+ transcript_path: transcriptPath,
6114
+ }, { cwd, sessionOwnerPid: process.pid });
6115
+ const childStop = await dispatchCodexNativeHook({
6116
+ hook_event_name: "Stop",
6117
+ cwd,
6118
+ session_id: childNativeSessionId,
6119
+ thread_id: childNativeSessionId,
6120
+ last_assistant_message: "Verdict: APPROVED. Evidence is sufficient.",
6121
+ }, { cwd });
6122
+ assert.equal(childStop.omxEventName, "stop");
6123
+ assert.equal(childStop.outputJson, null);
6124
+ const leaderStop = await dispatchCodexNativeHook({
6125
+ hook_event_name: "Stop",
6126
+ cwd,
6127
+ session_id: leaderNativeSessionId,
6128
+ thread_id: leaderNativeSessionId,
6129
+ last_assistant_message: "Waiting on verification integration.",
6130
+ }, { cwd });
6131
+ assert.equal(leaderStop.omxEventName, "stop");
6132
+ assert.deepEqual(leaderStop.outputJson, {
6133
+ decision: "block",
6134
+ reason: "OMX Ralph is still active (phase: verifying; state: .omx/state/sessions/sess-ralph-leader-verifier/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
6135
+ stopReason: "ralph_verifying",
6136
+ systemMessage: "OMX Ralph is still active (phase: verifying; state: .omx/state/sessions/sess-ralph-leader-verifier/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
6137
+ });
6138
+ }
6139
+ finally {
6140
+ await rm(cwd, { recursive: true, force: true });
6141
+ }
6142
+ });
4880
6143
  it("prefers canonical run-state terminal lifecycle before stale session Ralph state during Stop", async () => {
4881
6144
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-canonical-run-state-ralph-"));
4882
6145
  try {
@@ -5412,12 +6675,11 @@ esac
5412
6675
  await rm(cwd, { recursive: true, force: true });
5413
6676
  }
5414
6677
  });
5415
- it("suppresses native auto-nudge when root deep-interview mode state is active without an explicit session", async () => {
6678
+ it("suppresses native auto-nudge when root deep-interview mode state is active and no session is known", async () => {
5416
6679
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-deep-interview-mode-"));
5417
6680
  try {
5418
6681
  const stateDir = join(cwd, ".omx", "state");
5419
6682
  await mkdir(stateDir, { recursive: true });
5420
- process.env.OMX_SESSION_ID = "sess-stop-auto-mode";
5421
6683
  await writeJson(join(stateDir, "deep-interview-state.json"), {
5422
6684
  active: true,
5423
6685
  mode: "deep-interview",
@@ -5436,6 +6698,57 @@ esac
5436
6698
  await rm(cwd, { recursive: true, force: true });
5437
6699
  }
5438
6700
  });
6701
+ it("treats inherited OMX_SESSION_ID as session-aware for native auto-nudge Stop checks", async () => {
6702
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-env-session-"));
6703
+ try {
6704
+ const stateDir = join(cwd, ".omx", "state");
6705
+ await mkdir(stateDir, { recursive: true });
6706
+ process.env.OMX_SESSION_ID = "sess-stop-auto-mode";
6707
+ const result = await dispatchCodexNativeHook({
6708
+ hook_event_name: "Stop",
6709
+ cwd,
6710
+ thread_id: "thread-stop-auto-env-session",
6711
+ turn_id: "turn-stop-auto-env-session-1",
6712
+ last_assistant_message: "Keep going and finish the cleanup.",
6713
+ }, { cwd });
6714
+ assert.equal(result.omxEventName, "stop");
6715
+ assert.deepEqual(result.outputJson, {
6716
+ decision: "block",
6717
+ reason: DEFAULT_AUTO_NUDGE_RESPONSE,
6718
+ stopReason: "auto_nudge",
6719
+ systemMessage: "OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
6720
+ });
6721
+ const stopState = JSON.parse(await readFile(join(stateDir, "native-stop-state.json"), "utf-8"));
6722
+ assert.ok(stopState.sessions["sess-stop-auto-mode"]);
6723
+ }
6724
+ finally {
6725
+ await rm(cwd, { recursive: true, force: true });
6726
+ }
6727
+ });
6728
+ it("ignores generic SESSION_ID for native auto-nudge Stop session scoping", async () => {
6729
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-generic-session-"));
6730
+ try {
6731
+ const stateDir = join(cwd, ".omx", "state");
6732
+ await mkdir(stateDir, { recursive: true });
6733
+ process.env.SESSION_ID = "generic-shell-session";
6734
+ const result = await dispatchCodexNativeHook({
6735
+ hook_event_name: "Stop",
6736
+ cwd,
6737
+ thread_id: "thread-stop-auto-generic-session",
6738
+ turn_id: "turn-stop-auto-generic-session-1",
6739
+ last_assistant_message: "Keep going and finish the cleanup.",
6740
+ }, { cwd });
6741
+ assert.equal(result.omxEventName, "stop");
6742
+ assert.equal(result.outputJson?.decision, "block");
6743
+ const stopState = JSON.parse(await readFile(join(stateDir, "native-stop-state.json"), "utf-8"));
6744
+ const sessions = stopState.sessions;
6745
+ assert.equal(sessions["generic-shell-session"], undefined);
6746
+ assert.ok(sessions["thread-stop-auto-generic-session"]);
6747
+ }
6748
+ finally {
6749
+ await rm(cwd, { recursive: true, force: true });
6750
+ }
6751
+ });
5439
6752
  it("does not suppress native auto-nudge from stale root deep-interview mode state when the explicit session-scoped mode state is absent", async () => {
5440
6753
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-stale-root-mode-"));
5441
6754
  try {
@@ -5718,8 +7031,8 @@ esac
5718
7031
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultrawork-repeat-"));
5719
7032
  try {
5720
7033
  const stateDir = join(cwd, ".omx", "state");
5721
- await mkdir(stateDir, { recursive: true });
5722
- await writeJson(join(stateDir, "ultrawork-state.json"), {
7034
+ await mkdir(join(stateDir, "sessions", "sess-stop-ultrawork-repeat"), { recursive: true });
7035
+ await writeJson(join(stateDir, "sessions", "sess-stop-ultrawork-repeat", "ultrawork-state.json"), {
5723
7036
  active: true,
5724
7037
  current_phase: "executing",
5725
7038
  });
@@ -5791,12 +7104,10 @@ esac
5791
7104
  stop_hook_active: true,
5792
7105
  }, { cwd });
5793
7106
  assert.equal(repeated.omxEventName, "stop");
5794
- assert.deepEqual(repeated.outputJson, {
5795
- decision: "block",
5796
- reason: "OMX skill ralplan is still active (phase: planning); continue until the current ralplan workflow reaches a terminal state.",
5797
- stopReason: "skill_ralplan_planning",
5798
- systemMessage: "OMX skill ralplan is still active (phase: planning).",
5799
- });
7107
+ assert.equal(repeated.outputJson?.decision, "block");
7108
+ assert.match(String(repeated.outputJson?.reason ?? ""), /Status: continue_from_artifact/);
7109
+ assert.match(String(repeated.outputJson?.reason ?? ""), /continue from the current ralplan artifact/i);
7110
+ assert.equal(repeated.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
5800
7111
  }
5801
7112
  finally {
5802
7113
  await rm(cwd, { recursive: true, force: true });