oh-my-codex 0.18.0 → 0.18.1

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 (293) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +43 -19
  4. package/crates/omx-api/src/lib.rs +66 -9
  5. package/crates/omx-sparkshell/src/exec.rs +125 -3
  6. package/crates/omx-sparkshell/src/main.rs +126 -36
  7. package/crates/omx-sparkshell/tests/execution.rs +225 -1
  8. package/dist/cli/__tests__/codex-plugin-layout.test.js +15 -7
  9. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  10. package/dist/cli/__tests__/doctor-warning-copy.test.js +76 -3
  11. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  12. package/dist/cli/__tests__/index.test.js +49 -1
  13. package/dist/cli/__tests__/index.test.js.map +1 -1
  14. package/dist/cli/__tests__/install-docs-contract.test.d.ts +2 -0
  15. package/dist/cli/__tests__/install-docs-contract.test.d.ts.map +1 -0
  16. package/dist/cli/__tests__/install-docs-contract.test.js +55 -0
  17. package/dist/cli/__tests__/install-docs-contract.test.js.map +1 -0
  18. package/dist/cli/__tests__/launch-fallback.test.js +115 -0
  19. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  20. package/dist/cli/__tests__/question.test.js +27 -41
  21. package/dist/cli/__tests__/question.test.js.map +1 -1
  22. package/dist/cli/__tests__/setup-install-mode.test.js +94 -35
  23. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  24. package/dist/cli/__tests__/sparkshell-cli.test.js +20 -1
  25. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  26. package/dist/cli/__tests__/sparkshell-packaging.test.js +1 -0
  27. package/dist/cli/__tests__/sparkshell-packaging.test.js.map +1 -1
  28. package/dist/cli/__tests__/ultragoal.test.js +227 -4
  29. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  30. package/dist/cli/__tests__/update.test.js +72 -1
  31. package/dist/cli/__tests__/update.test.js.map +1 -1
  32. package/dist/cli/codex-feature-probe.d.ts +5 -0
  33. package/dist/cli/codex-feature-probe.d.ts.map +1 -1
  34. package/dist/cli/codex-feature-probe.js +13 -7
  35. package/dist/cli/codex-feature-probe.js.map +1 -1
  36. package/dist/cli/doctor.d.ts +7 -0
  37. package/dist/cli/doctor.d.ts.map +1 -1
  38. package/dist/cli/doctor.js +119 -10
  39. package/dist/cli/doctor.js.map +1 -1
  40. package/dist/cli/index.d.ts +3 -1
  41. package/dist/cli/index.d.ts.map +1 -1
  42. package/dist/cli/index.js +345 -90
  43. package/dist/cli/index.js.map +1 -1
  44. package/dist/cli/plugin-marketplace.d.ts +2 -0
  45. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  46. package/dist/cli/plugin-marketplace.js +15 -1
  47. package/dist/cli/plugin-marketplace.js.map +1 -1
  48. package/dist/cli/setup.d.ts.map +1 -1
  49. package/dist/cli/setup.js +71 -11
  50. package/dist/cli/setup.js.map +1 -1
  51. package/dist/cli/sparkshell.d.ts +7 -1
  52. package/dist/cli/sparkshell.d.ts.map +1 -1
  53. package/dist/cli/sparkshell.js +13 -3
  54. package/dist/cli/sparkshell.js.map +1 -1
  55. package/dist/cli/ultragoal.d.ts +1 -1
  56. package/dist/cli/ultragoal.d.ts.map +1 -1
  57. package/dist/cli/ultragoal.js +184 -10
  58. package/dist/cli/ultragoal.js.map +1 -1
  59. package/dist/cli/update.d.ts +2 -0
  60. package/dist/cli/update.d.ts.map +1 -1
  61. package/dist/cli/update.js +14 -3
  62. package/dist/cli/update.js.map +1 -1
  63. package/dist/compat/__tests__/doctor-contract.test.js +3 -0
  64. package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
  65. package/dist/config/__tests__/codex-feature-flags.test.js +11 -1
  66. package/dist/config/__tests__/codex-feature-flags.test.js.map +1 -1
  67. package/dist/config/__tests__/codex-hooks.test.js +19 -8
  68. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  69. package/dist/config/__tests__/commit-lore-guard.test.d.ts +2 -0
  70. package/dist/config/__tests__/commit-lore-guard.test.d.ts.map +1 -0
  71. package/dist/config/__tests__/commit-lore-guard.test.js +20 -0
  72. package/dist/config/__tests__/commit-lore-guard.test.js.map +1 -0
  73. package/dist/config/codex-feature-flags.d.ts +4 -0
  74. package/dist/config/codex-feature-flags.d.ts.map +1 -1
  75. package/dist/config/codex-feature-flags.js +4 -0
  76. package/dist/config/codex-feature-flags.js.map +1 -1
  77. package/dist/config/codex-hooks.js +6 -6
  78. package/dist/config/codex-hooks.js.map +1 -1
  79. package/dist/config/commit-lore-guard.d.ts +1 -0
  80. package/dist/config/commit-lore-guard.d.ts.map +1 -1
  81. package/dist/config/commit-lore-guard.js +29 -3
  82. package/dist/config/commit-lore-guard.js.map +1 -1
  83. package/dist/config/generator.d.ts +3 -1
  84. package/dist/config/generator.d.ts.map +1 -1
  85. package/dist/config/generator.js +24 -10
  86. package/dist/config/generator.js.map +1 -1
  87. package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -0
  88. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
  89. package/dist/goal-workflows/codex-goal-snapshot.js +5 -1
  90. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
  91. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +10 -6
  92. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  93. package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts +1 -1
  94. package/dist/hooks/__tests__/consensus-execution-handoff.test.js +13 -11
  95. package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
  96. package/dist/hooks/__tests__/deep-interview-contract.test.js +4 -3
  97. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  98. package/dist/hooks/__tests__/keyword-detector.test.js +4 -3
  99. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  100. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +33 -0
  101. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  102. package/dist/hooks/extensibility/__tests__/dispatcher.test.js +26 -3
  103. package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
  104. package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
  105. package/dist/hooks/extensibility/dispatcher.js +29 -14
  106. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  107. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  108. package/dist/hooks/keyword-detector.js +8 -3
  109. package/dist/hooks/keyword-detector.js.map +1 -1
  110. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  111. package/dist/hooks/prompt-guidance-contract.js +3 -2
  112. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  113. package/dist/hud/__tests__/hud-tmux-injection.test.js +14 -8
  114. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  115. package/dist/hud/__tests__/reconcile.test.js +2 -2
  116. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  117. package/dist/hud/__tests__/resource-leak-watch.test.d.ts +2 -0
  118. package/dist/hud/__tests__/resource-leak-watch.test.d.ts.map +1 -0
  119. package/dist/hud/__tests__/resource-leak-watch.test.js +28 -0
  120. package/dist/hud/__tests__/resource-leak-watch.test.js.map +1 -0
  121. package/dist/hud/index.d.ts +1 -1
  122. package/dist/hud/index.d.ts.map +1 -1
  123. package/dist/hud/index.js +10 -4
  124. package/dist/hud/index.js.map +1 -1
  125. package/dist/hud/tmux.js +2 -2
  126. package/dist/hud/tmux.js.map +1 -1
  127. package/dist/notifications/__tests__/http-client-resource.test.d.ts +2 -0
  128. package/dist/notifications/__tests__/http-client-resource.test.d.ts.map +1 -0
  129. package/dist/notifications/__tests__/http-client-resource.test.js +41 -0
  130. package/dist/notifications/__tests__/http-client-resource.test.js.map +1 -0
  131. package/dist/notifications/__tests__/verbosity.test.js +20 -0
  132. package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
  133. package/dist/notifications/config.d.ts.map +1 -1
  134. package/dist/notifications/config.js +6 -3
  135. package/dist/notifications/config.js.map +1 -1
  136. package/dist/notifications/http-client.d.ts.map +1 -1
  137. package/dist/notifications/http-client.js +78 -27
  138. package/dist/notifications/http-client.js.map +1 -1
  139. package/dist/notifications/types.d.ts +2 -0
  140. package/dist/notifications/types.d.ts.map +1 -1
  141. package/dist/openclaw/__tests__/dispatcher.test.js +49 -1
  142. package/dist/openclaw/__tests__/dispatcher.test.js.map +1 -1
  143. package/dist/openclaw/dispatcher.d.ts +7 -4
  144. package/dist/openclaw/dispatcher.d.ts.map +1 -1
  145. package/dist/openclaw/dispatcher.js +32 -69
  146. package/dist/openclaw/dispatcher.js.map +1 -1
  147. package/dist/pipeline/__tests__/orchestrator.test.js +65 -3
  148. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  149. package/dist/pipeline/__tests__/stages.test.js +50 -5
  150. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  151. package/dist/pipeline/index.d.ts +8 -2
  152. package/dist/pipeline/index.d.ts.map +1 -1
  153. package/dist/pipeline/index.js +5 -2
  154. package/dist/pipeline/index.js.map +1 -1
  155. package/dist/pipeline/orchestrator.d.ts +5 -4
  156. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  157. package/dist/pipeline/orchestrator.js +56 -15
  158. package/dist/pipeline/orchestrator.js.map +1 -1
  159. package/dist/pipeline/stages/code-review.d.ts +2 -2
  160. package/dist/pipeline/stages/code-review.d.ts.map +1 -1
  161. package/dist/pipeline/stages/code-review.js +5 -3
  162. package/dist/pipeline/stages/code-review.js.map +1 -1
  163. package/dist/pipeline/stages/deep-interview.d.ts +15 -0
  164. package/dist/pipeline/stages/deep-interview.d.ts.map +1 -0
  165. package/dist/pipeline/stages/deep-interview.js +32 -0
  166. package/dist/pipeline/stages/deep-interview.js.map +1 -0
  167. package/dist/pipeline/stages/ralph-verify.d.ts +5 -5
  168. package/dist/pipeline/stages/ralph-verify.d.ts.map +1 -1
  169. package/dist/pipeline/stages/ralph-verify.js +2 -2
  170. package/dist/pipeline/stages/ralph-verify.js.map +1 -1
  171. package/dist/pipeline/stages/ultragoal.d.ts +19 -0
  172. package/dist/pipeline/stages/ultragoal.d.ts.map +1 -0
  173. package/dist/pipeline/stages/ultragoal.js +38 -0
  174. package/dist/pipeline/stages/ultragoal.js.map +1 -0
  175. package/dist/pipeline/stages/ultraqa.d.ts +30 -0
  176. package/dist/pipeline/stages/ultraqa.d.ts.map +1 -0
  177. package/dist/pipeline/stages/ultraqa.js +46 -0
  178. package/dist/pipeline/stages/ultraqa.js.map +1 -0
  179. package/dist/pipeline/types.d.ts +8 -6
  180. package/dist/pipeline/types.d.ts.map +1 -1
  181. package/dist/pipeline/types.js +2 -2
  182. package/dist/scripts/__tests__/codex-native-hook.test.js +705 -45
  183. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  184. package/dist/scripts/__tests__/smoke-packed-install.test.js +23 -1
  185. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  186. package/dist/scripts/__tests__/verify-native-agents.test.js +16 -1
  187. package/dist/scripts/__tests__/verify-native-agents.test.js.map +1 -1
  188. package/dist/scripts/cleanup-explore-harness.js +1 -0
  189. package/dist/scripts/cleanup-explore-harness.js.map +1 -1
  190. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  191. package/dist/scripts/codex-native-hook.js +158 -10
  192. package/dist/scripts/codex-native-hook.js.map +1 -1
  193. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  194. package/dist/scripts/codex-native-pre-post.js +9 -1
  195. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  196. package/dist/scripts/notify-hook/process-runner.d.ts.map +1 -1
  197. package/dist/scripts/notify-hook/process-runner.js +39 -17
  198. package/dist/scripts/notify-hook/process-runner.js.map +1 -1
  199. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  200. package/dist/scripts/notify-hook/team-dispatch.js +9 -5
  201. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  202. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +1 -1
  203. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  204. package/dist/scripts/notify-hook/team-tmux-guard.js +7 -1
  205. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  206. package/dist/scripts/smoke-packed-install.d.ts +3 -0
  207. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  208. package/dist/scripts/smoke-packed-install.js +99 -1
  209. package/dist/scripts/smoke-packed-install.js.map +1 -1
  210. package/dist/scripts/sync-plugin-mirror.js +2 -2
  211. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  212. package/dist/scripts/verify-native-agents.js +2 -2
  213. package/dist/scripts/verify-native-agents.js.map +1 -1
  214. package/dist/sidecar/__tests__/resource-leak-watch.test.d.ts +2 -0
  215. package/dist/sidecar/__tests__/resource-leak-watch.test.d.ts.map +1 -0
  216. package/dist/sidecar/__tests__/resource-leak-watch.test.js +38 -0
  217. package/dist/sidecar/__tests__/resource-leak-watch.test.js.map +1 -0
  218. package/dist/sidecar/index.d.ts +1 -1
  219. package/dist/sidecar/index.d.ts.map +1 -1
  220. package/dist/sidecar/index.js +29 -12
  221. package/dist/sidecar/index.js.map +1 -1
  222. package/dist/state/__tests__/operations-ralph-phase.test.js +88 -1
  223. package/dist/state/__tests__/operations-ralph-phase.test.js.map +1 -1
  224. package/dist/state/operations.d.ts.map +1 -1
  225. package/dist/state/operations.js +11 -0
  226. package/dist/state/operations.js.map +1 -1
  227. package/dist/team/__tests__/tmux-session.test.js +111 -3
  228. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  229. package/dist/team/tmux-session.d.ts.map +1 -1
  230. package/dist/team/tmux-session.js +39 -18
  231. package/dist/team/tmux-session.js.map +1 -1
  232. package/dist/ultragoal/__tests__/artifacts.test.js +714 -10
  233. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  234. package/dist/ultragoal/__tests__/docs-contract.test.js +57 -1
  235. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  236. package/dist/ultragoal/__tests__/steering-fixtures.d.ts +68 -0
  237. package/dist/ultragoal/__tests__/steering-fixtures.d.ts.map +1 -0
  238. package/dist/ultragoal/__tests__/steering-fixtures.js +259 -0
  239. package/dist/ultragoal/__tests__/steering-fixtures.js.map +1 -0
  240. package/dist/ultragoal/__tests__/steering-fixtures.test.d.ts +2 -0
  241. package/dist/ultragoal/__tests__/steering-fixtures.test.d.ts.map +1 -0
  242. package/dist/ultragoal/__tests__/steering-fixtures.test.js +65 -0
  243. package/dist/ultragoal/__tests__/steering-fixtures.test.js.map +1 -0
  244. package/dist/ultragoal/artifacts.d.ts +97 -2
  245. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  246. package/dist/ultragoal/artifacts.js +811 -256
  247. package/dist/ultragoal/artifacts.js.map +1 -1
  248. package/dist/utils/__tests__/sleep-resource.test.d.ts +2 -0
  249. package/dist/utils/__tests__/sleep-resource.test.d.ts.map +1 -0
  250. package/dist/utils/__tests__/sleep-resource.test.js +39 -0
  251. package/dist/utils/__tests__/sleep-resource.test.js.map +1 -0
  252. package/dist/utils/sleep.d.ts.map +1 -1
  253. package/dist/utils/sleep.js +17 -6
  254. package/dist/utils/sleep.js.map +1 -1
  255. package/package.json +2 -1
  256. package/plugins/oh-my-codex/.codex-plugin/plugin.json +4 -3
  257. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +56 -0
  258. package/plugins/oh-my-codex/hooks/hooks.json +77 -0
  259. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +77 -47
  260. package/plugins/oh-my-codex/skills/cancel/SKILL.md +2 -2
  261. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +8 -8
  262. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +1 -1
  263. package/plugins/oh-my-codex/skills/pipeline/SKILL.md +22 -11
  264. package/plugins/oh-my-codex/skills/plan/SKILL.md +8 -8
  265. package/plugins/oh-my-codex/skills/ralph/SKILL.md +7 -0
  266. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +4 -4
  267. package/plugins/oh-my-codex/skills/team/SKILL.md +1 -1
  268. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +38 -4
  269. package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +1 -1
  270. package/prompts/planner.md +1 -1
  271. package/skills/autopilot/SKILL.md +77 -47
  272. package/skills/cancel/SKILL.md +2 -2
  273. package/skills/deep-interview/SKILL.md +8 -8
  274. package/skills/omx-setup/SKILL.md +1 -1
  275. package/skills/pipeline/SKILL.md +22 -11
  276. package/skills/plan/SKILL.md +8 -8
  277. package/skills/ralph/SKILL.md +7 -0
  278. package/skills/ralplan/SKILL.md +4 -4
  279. package/skills/team/SKILL.md +1 -1
  280. package/skills/ultragoal/SKILL.md +38 -4
  281. package/skills/ultrawork/SKILL.md +1 -1
  282. package/src/scripts/__tests__/codex-native-hook.test.ts +867 -81
  283. package/src/scripts/__tests__/smoke-packed-install.test.ts +31 -0
  284. package/src/scripts/__tests__/verify-native-agents.test.ts +21 -1
  285. package/src/scripts/cleanup-explore-harness.ts +1 -0
  286. package/src/scripts/codex-native-hook.ts +156 -10
  287. package/src/scripts/codex-native-pre-post.ts +16 -1
  288. package/src/scripts/notify-hook/process-runner.ts +40 -16
  289. package/src/scripts/notify-hook/team-dispatch.ts +9 -5
  290. package/src/scripts/notify-hook/team-tmux-guard.ts +7 -0
  291. package/src/scripts/smoke-packed-install.ts +105 -0
  292. package/src/scripts/sync-plugin-mirror.ts +3 -3
  293. package/src/scripts/verify-native-agents.ts +2 -2
@@ -17,6 +17,7 @@ import { OMX_TMUX_HUD_OWNER_ENV } from "../../hud/reconcile.js";
17
17
  import { readAllState } from "../../hud/state.js";
18
18
  import { getLegacyWikiDir, serializePage, writePage } from "../../wiki/storage.js";
19
19
  import { WIKI_SCHEMA_VERSION } from "../../wiki/types.js";
20
+ import { createUltragoalPlan, readUltragoalPlan } from "../../ultragoal/artifacts.js";
20
21
  function nativeHookScriptPath() {
21
22
  return join(process.cwd(), "dist", "scripts", "codex-native-hook.js");
22
23
  }
@@ -39,6 +40,38 @@ async function writeJson(path, value) {
39
40
  await mkdir(dirname(path), { recursive: true }).catch(() => { });
40
41
  await writeFile(path, JSON.stringify(value, null, 2));
41
42
  }
43
+ async function withLoreGuardConfig(value, prefix, run) {
44
+ const cwd = await mkdtemp(join(tmpdir(), `omx-native-hook-pretool-git-commit-lore-${prefix}-`));
45
+ const codexHome = await mkdtemp(join(tmpdir(), `omx-native-hook-codex-home-lore-${prefix}-`));
46
+ const defaultHome = await mkdtemp(join(tmpdir(), `omx-native-hook-home-lore-${prefix}-`));
47
+ const originalGuard = process.env.OMX_LORE_COMMIT_GUARD;
48
+ const originalCodexHome = process.env.CODEX_HOME;
49
+ const originalHome = process.env.HOME;
50
+ try {
51
+ delete process.env.OMX_LORE_COMMIT_GUARD;
52
+ process.env.CODEX_HOME = codexHome;
53
+ process.env.HOME = defaultHome;
54
+ await writeFile(join(codexHome, "config.toml"), `[shell_environment_policy.set]\nOMX_LORE_COMMIT_GUARD = "${value}"\n`, "utf-8");
55
+ return await run(cwd);
56
+ }
57
+ finally {
58
+ if (originalGuard === undefined)
59
+ delete process.env.OMX_LORE_COMMIT_GUARD;
60
+ else
61
+ process.env.OMX_LORE_COMMIT_GUARD = originalGuard;
62
+ if (originalCodexHome === undefined)
63
+ delete process.env.CODEX_HOME;
64
+ else
65
+ process.env.CODEX_HOME = originalCodexHome;
66
+ if (originalHome === undefined)
67
+ delete process.env.HOME;
68
+ else
69
+ process.env.HOME = originalHome;
70
+ await rm(cwd, { recursive: true, force: true });
71
+ await rm(codexHome, { recursive: true, force: true });
72
+ await rm(defaultHome, { recursive: true, force: true });
73
+ }
74
+ }
42
75
  function buildWorkerStopFakeTmux(tmuxLogPath, options = {}) {
43
76
  return `#!/usr/bin/env bash
44
77
  set -eu
@@ -567,7 +600,16 @@ describe("codex native hook dispatch", () => {
567
600
  });
568
601
  it("keeps subagent SessionStart from replacing the canonical leader session", async () => {
569
602
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-start-"));
570
- try {
603
+ const originalCodexHome = process.env.CODEX_HOME;
604
+ try {
605
+ process.env.CODEX_HOME = join(cwd, "codex-home");
606
+ await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
607
+ notifications: {
608
+ enabled: true,
609
+ verbosity: "session",
610
+ telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
611
+ },
612
+ });
571
613
  const stateDir = join(cwd, ".omx", "state");
572
614
  const canonicalSessionId = "omx-leader-session";
573
615
  const leaderNativeSessionId = "codex-leader-thread";
@@ -583,6 +625,13 @@ describe("codex native hook dispatch", () => {
583
625
  iteration: 1,
584
626
  max_iterations: 5,
585
627
  });
628
+ await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
629
+ await writeFile(join(cwd, ".omx", "hooks", "record-lifecycle.mjs"), [
630
+ "import { appendFileSync } from 'node:fs';",
631
+ "export async function onHookEvent(event) {",
632
+ " appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event, context: event.context })}\\n`);",
633
+ "}",
634
+ ].join("\n"));
586
635
  const transcriptPath = join(cwd, "subagent-rollout.jsonl");
587
636
  await writeFile(transcriptPath, `${JSON.stringify({
588
637
  type: "session_meta",
@@ -616,6 +665,7 @@ describe("codex native hook dispatch", () => {
616
665
  const leaderRalph = JSON.parse(await readFile(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), "utf-8"));
617
666
  assert.equal(leaderRalph.active, true);
618
667
  assert.equal(leaderRalph.current_phase, "executing");
668
+ assert.equal(existsSync(join(cwd, "hook-events.jsonl")), false, "subagent SessionStart must not independently dispatch session-start hook notifications");
619
669
  const tracking = JSON.parse(await readFile(join(stateDir, "subagent-tracking.json"), "utf-8"));
620
670
  assert.equal(tracking.sessions?.[canonicalSessionId]?.leader_thread_id, leaderNativeSessionId);
621
671
  assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[childNativeSessionId]?.kind, "subagent");
@@ -623,8 +673,213 @@ describe("codex native hook dispatch", () => {
623
673
  assert.equal(tracking.sessions?.[leaderNativeSessionId]?.leader_thread_id, leaderNativeSessionId);
624
674
  assert.equal(tracking.sessions?.[leaderNativeSessionId]?.threads?.[childNativeSessionId]?.kind, "subagent");
625
675
  assert.equal(tracking.sessions?.[leaderNativeSessionId]?.threads?.[childNativeSessionId]?.mode, "critic");
676
+ await dispatchCodexNativeHook({
677
+ hook_event_name: "Stop",
678
+ cwd,
679
+ session_id: childNativeSessionId,
680
+ thread_id: childNativeSessionId,
681
+ turn_id: "child-stop-turn",
682
+ }, { cwd });
683
+ assert.equal(existsSync(join(cwd, "hook-events.jsonl")), false, "subagent Stop must not independently dispatch stop hook notifications");
684
+ }
685
+ finally {
686
+ if (originalCodexHome === undefined) {
687
+ delete process.env.CODEX_HOME;
688
+ }
689
+ else {
690
+ process.env.CODEX_HOME = originalCodexHome;
691
+ }
692
+ await rm(cwd, { recursive: true, force: true });
693
+ }
694
+ });
695
+ it("suppresses child-agent SessionStart hook dispatch at minimal verbosity", async () => {
696
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-minimal-"));
697
+ const originalCodexHome = process.env.CODEX_HOME;
698
+ try {
699
+ process.env.CODEX_HOME = join(cwd, "codex-home");
700
+ await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
701
+ notifications: {
702
+ enabled: true,
703
+ verbosity: "minimal",
704
+ telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
705
+ },
706
+ });
707
+ const stateDir = join(cwd, ".omx", "state");
708
+ const canonicalSessionId = "omx-leader-session-minimal";
709
+ const leaderNativeSessionId = "codex-leader-thread-minimal";
710
+ const childNativeSessionId = "codex-child-thread-minimal";
711
+ await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
712
+ await writeSessionStart(cwd, canonicalSessionId, {
713
+ nativeSessionId: leaderNativeSessionId,
714
+ });
715
+ await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
716
+ await writeFile(join(cwd, ".omx", "hooks", "record-lifecycle.mjs"), [
717
+ "import { appendFileSync } from 'node:fs';",
718
+ "export async function onHookEvent(event) {",
719
+ " appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event })}\\n`);",
720
+ "}",
721
+ ].join("\n"));
722
+ const transcriptPath = join(cwd, "minimal-subagent-rollout.jsonl");
723
+ await writeFile(transcriptPath, `${JSON.stringify({
724
+ type: "session_meta",
725
+ payload: {
726
+ id: childNativeSessionId,
727
+ source: {
728
+ subagent: {
729
+ thread_spawn: {
730
+ parent_thread_id: leaderNativeSessionId,
731
+ agent_role: "verifier",
732
+ },
733
+ },
734
+ },
735
+ },
736
+ })}\n`);
737
+ await dispatchCodexNativeHook({
738
+ hook_event_name: "SessionStart",
739
+ cwd,
740
+ session_id: childNativeSessionId,
741
+ transcript_path: transcriptPath,
742
+ }, { cwd, sessionOwnerPid: process.pid });
743
+ assert.equal(existsSync(join(cwd, "hook-events.jsonl")), false, "subagent SessionStart must be suppressed at minimal verbosity");
744
+ }
745
+ finally {
746
+ if (originalCodexHome === undefined) {
747
+ delete process.env.CODEX_HOME;
748
+ }
749
+ else {
750
+ process.env.CODEX_HOME = originalCodexHome;
751
+ }
752
+ await rm(cwd, { recursive: true, force: true });
753
+ }
754
+ });
755
+ it("allows explicit child-agent lifecycle hook dispatch when includeChildAgents is enabled", async () => {
756
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-include-"));
757
+ const originalCodexHome = process.env.CODEX_HOME;
758
+ try {
759
+ process.env.CODEX_HOME = join(cwd, "codex-home");
760
+ await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
761
+ notifications: {
762
+ enabled: true,
763
+ verbosity: "session",
764
+ includeChildAgents: true,
765
+ telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
766
+ },
767
+ });
768
+ const stateDir = join(cwd, ".omx", "state");
769
+ const canonicalSessionId = "omx-leader-session-include";
770
+ const leaderNativeSessionId = "codex-leader-thread-include";
771
+ const childNativeSessionId = "codex-child-thread-include";
772
+ await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
773
+ await writeSessionStart(cwd, canonicalSessionId, {
774
+ nativeSessionId: leaderNativeSessionId,
775
+ });
776
+ await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
777
+ await writeFile(join(cwd, ".omx", "hooks", "record-lifecycle.mjs"), [
778
+ "import { appendFileSync } from 'node:fs';",
779
+ "export async function onHookEvent(event) {",
780
+ " appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event })}\\n`);",
781
+ "}",
782
+ ].join("\n"));
783
+ const transcriptPath = join(cwd, "included-subagent-rollout.jsonl");
784
+ await writeFile(transcriptPath, `${JSON.stringify({
785
+ type: "session_meta",
786
+ payload: {
787
+ id: childNativeSessionId,
788
+ source: {
789
+ subagent: {
790
+ thread_spawn: {
791
+ parent_thread_id: leaderNativeSessionId,
792
+ agent_role: "verifier",
793
+ },
794
+ },
795
+ },
796
+ },
797
+ })}\n`);
798
+ await dispatchCodexNativeHook({
799
+ hook_event_name: "SessionStart",
800
+ cwd,
801
+ session_id: childNativeSessionId,
802
+ transcript_path: transcriptPath,
803
+ }, { cwd, sessionOwnerPid: process.pid });
804
+ await dispatchCodexNativeHook({
805
+ hook_event_name: "Stop",
806
+ cwd,
807
+ session_id: childNativeSessionId,
808
+ thread_id: childNativeSessionId,
809
+ turn_id: "included-child-stop-turn",
810
+ }, { cwd });
811
+ const hookEvents = await readFile(join(cwd, "hook-events.jsonl"), "utf-8");
812
+ assert.match(hookEvents, /"event":"session-start"/);
813
+ assert.match(hookEvents, /"event":"stop"/);
814
+ }
815
+ finally {
816
+ if (originalCodexHome === undefined) {
817
+ delete process.env.CODEX_HOME;
818
+ }
819
+ else {
820
+ process.env.CODEX_HOME = originalCodexHome;
821
+ }
822
+ await rm(cwd, { recursive: true, force: true });
823
+ }
824
+ });
825
+ it("allows child-agent lifecycle hook dispatch at agent verbosity", async () => {
826
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-agent-"));
827
+ const originalCodexHome = process.env.CODEX_HOME;
828
+ try {
829
+ process.env.CODEX_HOME = join(cwd, "codex-home");
830
+ await writeJson(join(process.env.CODEX_HOME, ".omx-config.json"), {
831
+ notifications: {
832
+ enabled: true,
833
+ verbosity: "agent",
834
+ telegram: { enabled: true, botToken: "123:abc", chatId: "456" },
835
+ },
836
+ });
837
+ const stateDir = join(cwd, ".omx", "state");
838
+ const canonicalSessionId = "omx-leader-session-agent";
839
+ const leaderNativeSessionId = "codex-leader-thread-agent";
840
+ const childNativeSessionId = "codex-child-thread-agent";
841
+ await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
842
+ await writeSessionStart(cwd, canonicalSessionId, {
843
+ nativeSessionId: leaderNativeSessionId,
844
+ });
845
+ await mkdir(join(cwd, ".omx", "hooks"), { recursive: true });
846
+ await writeFile(join(cwd, ".omx", "hooks", "record-lifecycle.mjs"), [
847
+ "import { appendFileSync } from 'node:fs';",
848
+ "export async function onHookEvent(event) {",
849
+ " appendFileSync('hook-events.jsonl', `${JSON.stringify({ event: event.event })}\\n`);",
850
+ "}",
851
+ ].join("\n"));
852
+ const transcriptPath = join(cwd, "agent-verbosity-subagent-rollout.jsonl");
853
+ await writeFile(transcriptPath, `${JSON.stringify({
854
+ type: "session_meta",
855
+ payload: {
856
+ id: childNativeSessionId,
857
+ source: {
858
+ subagent: {
859
+ thread_spawn: {
860
+ parent_thread_id: leaderNativeSessionId,
861
+ agent_role: "verifier",
862
+ },
863
+ },
864
+ },
865
+ },
866
+ })}\n`);
867
+ await dispatchCodexNativeHook({
868
+ hook_event_name: "SessionStart",
869
+ cwd,
870
+ session_id: childNativeSessionId,
871
+ transcript_path: transcriptPath,
872
+ }, { cwd, sessionOwnerPid: process.pid });
873
+ const hookEvents = await readFile(join(cwd, "hook-events.jsonl"), "utf-8");
874
+ assert.match(hookEvents, /"event":"session-start"/);
626
875
  }
627
876
  finally {
877
+ if (originalCodexHome === undefined) {
878
+ delete process.env.CODEX_HOME;
879
+ }
880
+ else {
881
+ process.env.CODEX_HOME = originalCodexHome;
882
+ }
628
883
  await rm(cwd, { recursive: true, force: true });
629
884
  }
630
885
  });
@@ -1218,6 +1473,62 @@ describe("codex native hook dispatch", () => {
1218
1473
  await rm(cwd, { recursive: true, force: true });
1219
1474
  }
1220
1475
  });
1476
+ it("does not repeat performance-goal reconciliation after a recorded objective mismatch blocker", async () => {
1477
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-performance-mismatch-blocked-stop-"));
1478
+ try {
1479
+ await writeJson(join(cwd, ".omx", "goals", "performance", "latency", "state.json"), {
1480
+ version: 1,
1481
+ workflow: "performance-goal",
1482
+ slug: "latency",
1483
+ objective: "Reduce latency",
1484
+ status: "blocked",
1485
+ lastValidation: {
1486
+ status: "blocked",
1487
+ evidence: "omx performance-goal complete rejected the fresh get_goal snapshot: Codex goal objective mismatch: expected \"reduce latency\", got \"legacy objective\".",
1488
+ recordedAt: "2026-05-20T00:00:00.000Z",
1489
+ },
1490
+ });
1491
+ const result = await dispatchCodexNativeHook({
1492
+ hook_event_name: "Stop",
1493
+ cwd,
1494
+ session_id: "sess-performance-mismatch-blocked-stop",
1495
+ thread_id: "thread-performance-mismatch-blocked-stop",
1496
+ last_assistant_message: "Performance goal complete; next call update_goal({status: \"complete\"}).",
1497
+ }, { cwd });
1498
+ assert.notEqual(result.outputJson?.decision, "block");
1499
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /omx performance-goal complete --slug latency/);
1500
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
1501
+ }
1502
+ finally {
1503
+ await rm(cwd, { recursive: true, force: true });
1504
+ }
1505
+ });
1506
+ it("does not block Stop for an already complete performance-goal state", async () => {
1507
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-performance-complete-stop-"));
1508
+ try {
1509
+ await writeJson(join(cwd, ".omx", "goals", "performance", "latency", "state.json"), {
1510
+ version: 1,
1511
+ workflow: "performance-goal",
1512
+ slug: "latency",
1513
+ objective: "Reduce latency",
1514
+ status: "complete",
1515
+ completedAt: "2026-05-20T00:00:00.000Z",
1516
+ });
1517
+ const result = await dispatchCodexNativeHook({
1518
+ hook_event_name: "Stop",
1519
+ cwd,
1520
+ session_id: "sess-performance-complete-stop",
1521
+ thread_id: "thread-performance-complete-stop",
1522
+ last_assistant_message: "Performance goal complete; next call update_goal({status: \"complete\"}).",
1523
+ }, { cwd });
1524
+ assert.notEqual(result.outputJson?.decision, "block");
1525
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /omx performance-goal complete --slug latency/);
1526
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
1527
+ }
1528
+ finally {
1529
+ await rm(cwd, { recursive: true, force: true });
1530
+ }
1531
+ });
1221
1532
  it("blocks ultragoal Stop for concise generic goal completion claims", async () => {
1222
1533
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-generic-complete-stop-"));
1223
1534
  try {
@@ -1262,7 +1573,7 @@ describe("codex native hook dispatch", () => {
1262
1573
  await rm(cwd, { recursive: true, force: true });
1263
1574
  }
1264
1575
  });
1265
- it("blocks ultragoal Stop with blocked checkpoint and fresh-thread remediation for completed legacy snapshots", async () => {
1576
+ it("blocks ultragoal Stop with blocked checkpoint and available-goal-context remediation for completed legacy snapshots", async () => {
1266
1577
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-legacy-stop-"));
1267
1578
  try {
1268
1579
  await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
@@ -1281,7 +1592,8 @@ describe("codex native hook dispatch", () => {
1281
1592
  assert.equal(result.outputJson?.decision, "block");
1282
1593
  assert.match(output, /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
1283
1594
  assert.match(output, /--status blocked/);
1284
- assert.match(output, /fresh Codex thread/);
1595
+ assert.match(output, /Codex goal context/);
1596
+ assert.doesNotMatch(output, /fresh (?:Codex )?(?:thread|session)s?/i);
1285
1597
  assert.match(output, /Hooks must not mutate Codex goal state/);
1286
1598
  }
1287
1599
  finally {
@@ -1294,7 +1606,7 @@ describe("codex native hook dispatch", () => {
1294
1606
  await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
1295
1607
  version: 1,
1296
1608
  codexGoalMode: "aggregate",
1297
- codexObjective: "Complete all ultragoal stories in .omx/ultragoal/goals.json: many micro goals",
1609
+ codexObjective: "Complete the durable ultragoal plan in .omx/ultragoal/goals.json, including later accepted/appended stories, under the original brief constraints; use .omx/ultragoal/ledger.jsonl as the audit trail.",
1298
1610
  activeGoalId: "G001-micro",
1299
1611
  aggregateCompletion: {
1300
1612
  status: "complete",
@@ -1573,12 +1885,168 @@ describe("codex native hook dispatch", () => {
1573
1885
  assert.match(message, /get_goal/);
1574
1886
  assert.match(message, /create_goal/);
1575
1887
  assert.match(message, /update_goal/);
1888
+ assert.match(message, /does not call `\/goal clear`/);
1889
+ assert.match(message, /multiple sequential ultragoal runs/);
1576
1890
  assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-ultragoal-1", "ultragoal-state.json")), false);
1577
1891
  }
1578
1892
  finally {
1579
1893
  await rm(cwd, { recursive: true, force: true });
1580
1894
  }
1581
1895
  });
1896
+ it("applies only explicit structured UserPromptSubmit ultragoal steering directives", async () => {
1897
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-steer-"));
1898
+ try {
1899
+ await createUltragoalPlan(cwd, {
1900
+ brief: "G002-cli-and-prompt-submit-bridge .omx/ultragoal hook steering fixture",
1901
+ goals: [{ title: "First", objective: "Complete first milestone with tests." }],
1902
+ });
1903
+ const prose = await dispatchCodexNativeHook({
1904
+ hook_event_name: "UserPromptSubmit",
1905
+ cwd,
1906
+ session_id: "sess-ultragoal-steer-1",
1907
+ prompt: "Please add a subgoal for docs later; this is normal prose, not a directive.",
1908
+ }, { cwd });
1909
+ assert.equal(prose.outputJson, null);
1910
+ assert.equal((await readUltragoalPlan(cwd)).goals.length, 1);
1911
+ const jsonExample = await dispatchCodexNativeHook({
1912
+ hook_event_name: "UserPromptSubmit",
1913
+ cwd,
1914
+ session_id: "sess-ultragoal-steer-1",
1915
+ prompt: `Here is an inert example:\n\`\`\`json\n${JSON.stringify({
1916
+ kind: "add_subgoal",
1917
+ source: "user_prompt_submit",
1918
+ evidence: "Example JSON should not mutate .omx/ultragoal.",
1919
+ rationale: "Only explicit steering fences or labels are executable.",
1920
+ title: "Inert JSON example",
1921
+ objective: "This example must not be added.",
1922
+ })}\n\`\`\``,
1923
+ }, { cwd });
1924
+ assert.equal(jsonExample.outputJson, null);
1925
+ assert.equal((await readUltragoalPlan(cwd)).goals.length, 1);
1926
+ const result = await dispatchCodexNativeHook({
1927
+ hook_event_name: "UserPromptSubmit",
1928
+ cwd,
1929
+ session_id: "sess-ultragoal-steer-1",
1930
+ prompt: `OMX_ULTRAGOAL_STEER: ${JSON.stringify({
1931
+ kind: "add_subgoal",
1932
+ source: "user_prompt_submit",
1933
+ evidence: "Prompt-submit supplied a structured .omx/ultragoal directive for G002-cli-and-prompt-submit-bridge.",
1934
+ rationale: "Add bounded hook regression work while preserving all completion gates.",
1935
+ title: "Prompt bridge regression",
1936
+ objective: "Verify UserPromptSubmit bounded steering bridge with tests.",
1937
+ })}`,
1938
+ }, { cwd });
1939
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
1940
+ assert.match(message, /bounded \.omx\/ultragoal steering/);
1941
+ assert.match(message, /G002-cli-and-prompt-submit-bridge/);
1942
+ assert.match(message, /accepted/);
1943
+ const plan = await readUltragoalPlan(cwd);
1944
+ assert.equal(plan.goals.length, 2);
1945
+ assert.equal(plan.goals[1]?.title, "Prompt bridge regression");
1946
+ }
1947
+ finally {
1948
+ await rm(cwd, { recursive: true, force: true });
1949
+ }
1950
+ });
1951
+ it("does not apply UserPromptSubmit ultragoal steering from native subagent prompts", async () => {
1952
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-steer-subagent-"));
1953
+ try {
1954
+ await createUltragoalPlan(cwd, {
1955
+ brief: "G002-cli-and-prompt-submit-bridge .omx/ultragoal subagent steering fixture",
1956
+ goals: [{ title: "First", objective: "Complete first milestone with tests." }],
1957
+ });
1958
+ const stateDir = join(cwd, ".omx", "state");
1959
+ const canonicalSessionId = "sess-ultragoal-parent";
1960
+ const leaderNativeSessionId = "native-ultragoal-parent";
1961
+ const childNativeSessionId = "native-ultragoal-child";
1962
+ const nowIso = new Date().toISOString();
1963
+ await writeJson(join(stateDir, "session.json"), {
1964
+ session_id: canonicalSessionId,
1965
+ native_session_id: leaderNativeSessionId,
1966
+ });
1967
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
1968
+ schemaVersion: 1,
1969
+ sessions: {
1970
+ [canonicalSessionId]: {
1971
+ session_id: canonicalSessionId,
1972
+ leader_thread_id: leaderNativeSessionId,
1973
+ updated_at: nowIso,
1974
+ threads: {
1975
+ [leaderNativeSessionId]: {
1976
+ thread_id: leaderNativeSessionId,
1977
+ kind: "leader",
1978
+ first_seen_at: nowIso,
1979
+ last_seen_at: nowIso,
1980
+ turn_count: 1,
1981
+ },
1982
+ [childNativeSessionId]: {
1983
+ thread_id: childNativeSessionId,
1984
+ kind: "subagent",
1985
+ first_seen_at: nowIso,
1986
+ last_seen_at: nowIso,
1987
+ turn_count: 1,
1988
+ mode: "architect",
1989
+ },
1990
+ },
1991
+ },
1992
+ },
1993
+ });
1994
+ const result = await dispatchCodexNativeHook({
1995
+ hook_event_name: "UserPromptSubmit",
1996
+ cwd,
1997
+ session_id: childNativeSessionId,
1998
+ thread_id: childNativeSessionId,
1999
+ turn_id: "turn-ultragoal-child-1",
2000
+ prompt: `OMX_ULTRAGOAL_STEER: ${JSON.stringify({
2001
+ kind: "add_subgoal",
2002
+ source: "user_prompt_submit",
2003
+ evidence: "Subagent prompt text must be literal delegated context.",
2004
+ rationale: "Subagent prompts should not mutate the parent .omx/ultragoal ledger.",
2005
+ title: "Subagent should not add this",
2006
+ objective: "This must remain literal prompt text.",
2007
+ })}`,
2008
+ }, { cwd });
2009
+ assert.equal(result.outputJson, null);
2010
+ const plan = await readUltragoalPlan(cwd);
2011
+ assert.equal(plan.goals.length, 1);
2012
+ const ledger = await readFile(join(cwd, ".omx/ultragoal/ledger.jsonl"), "utf-8");
2013
+ assert.equal((ledger.match(/"event":"steering_accepted"/g) ?? []).length, 0);
2014
+ assert.equal((ledger.match(/"event":"steering_rejected"/g) ?? []).length, 0);
2015
+ }
2016
+ finally {
2017
+ await rm(cwd, { recursive: true, force: true });
2018
+ }
2019
+ });
2020
+ it("dedupes repeated UserPromptSubmit ultragoal steering directives by prompt signature", async () => {
2021
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-steer-dedupe-"));
2022
+ try {
2023
+ await createUltragoalPlan(cwd, {
2024
+ brief: "G002-cli-and-prompt-submit-bridge .omx/ultragoal dedupe fixture",
2025
+ goals: [{ title: "First", objective: "Complete first milestone with tests." }],
2026
+ });
2027
+ const prompt = `\`\`\`omx-ultragoal-steer
2028
+ ${JSON.stringify({
2029
+ kind: "add_subgoal",
2030
+ source: "user_prompt_submit",
2031
+ evidence: "Structured prompt-submit directive adds exactly one deduped goal.",
2032
+ rationale: "Use idempotent bridge semantics for repeated hook delivery.",
2033
+ title: "Deduped bridge regression",
2034
+ objective: "Verify repeated UserPromptSubmit steering does not duplicate goals.",
2035
+ })}
2036
+ \`\`\``;
2037
+ await dispatchCodexNativeHook({ hook_event_name: "UserPromptSubmit", cwd, session_id: "sess-dedupe", prompt }, { cwd });
2038
+ const second = await dispatchCodexNativeHook({ hook_event_name: "UserPromptSubmit", cwd, session_id: "sess-dedupe", prompt }, { cwd });
2039
+ const message = String(second.outputJson?.hookSpecificOutput?.additionalContext || "");
2040
+ assert.match(message, /deduped/);
2041
+ const plan = await readUltragoalPlan(cwd);
2042
+ assert.equal(plan.goals.filter((goal) => goal.title === "Deduped bridge regression").length, 1);
2043
+ const ledger = await readFile(join(cwd, ".omx/ultragoal/ledger.jsonl"), "utf-8");
2044
+ assert.equal((ledger.match(/"event":"steering_accepted"/g) ?? []).length, 1);
2045
+ }
2046
+ finally {
2047
+ await rm(cwd, { recursive: true, force: true });
2048
+ }
2049
+ });
1582
2050
  it("normalizes the Korean keyboard typo for ulw during UserPromptSubmit activation", async () => {
1583
2051
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ulw-ko-"));
1584
2052
  try {
@@ -3109,7 +3577,7 @@ exit 0
3109
3577
  cwd,
3110
3578
  tool_name: "Bash",
3111
3579
  tool_use_id: "tool-slop-git-priority",
3112
- tool_input: { command: 'git commit -m "quick hack fallback if it fails"' },
3580
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git commit -m "quick hack fallback if it fails"' },
3113
3581
  }, { cwd });
3114
3582
  assert.equal(result.omxEventName, "pre-tool-use");
3115
3583
  assert.equal(result.outputJson?.decision, "block");
@@ -3128,7 +3596,7 @@ exit 0
3128
3596
  cwd,
3129
3597
  tool_name: "Bash",
3130
3598
  tool_use_id: "tool-git-commit-invalid",
3131
- tool_input: { command: 'git commit -m "fix tests"' },
3599
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git commit -m "fix tests"' },
3132
3600
  }, { cwd });
3133
3601
  assert.equal(result.omxEventName, "pre-tool-use");
3134
3602
  assert.deepEqual(result.outputJson, {
@@ -3153,11 +3621,35 @@ exit 0
3153
3621
  await rm(cwd, { recursive: true, force: true });
3154
3622
  }
3155
3623
  });
3156
- it("allows non-Lore git commit messages when the Lore commit guard is explicitly disabled", async () => {
3624
+ it("blocks PreToolUse git commit when process env explicitly enables the Lore commit guard", async () => {
3625
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-env-enabled-"));
3626
+ const original = process.env.OMX_LORE_COMMIT_GUARD;
3627
+ try {
3628
+ process.env.OMX_LORE_COMMIT_GUARD = "1";
3629
+ const result = await dispatchCodexNativeHook({
3630
+ hook_event_name: "PreToolUse",
3631
+ cwd,
3632
+ tool_name: "Bash",
3633
+ tool_use_id: "tool-git-commit-lore-env-enabled",
3634
+ tool_input: { command: 'git commit -m "fix tests"' },
3635
+ }, { cwd });
3636
+ assert.equal(result.omxEventName, "pre-tool-use");
3637
+ assert.equal(result.outputJson?.decision, "block");
3638
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
3639
+ }
3640
+ finally {
3641
+ if (original === undefined)
3642
+ delete process.env.OMX_LORE_COMMIT_GUARD;
3643
+ else
3644
+ process.env.OMX_LORE_COMMIT_GUARD = original;
3645
+ await rm(cwd, { recursive: true, force: true });
3646
+ }
3647
+ });
3648
+ it("allows non-Lore git commit messages when the Lore commit guard is disabled by default", async () => {
3157
3649
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-disabled-"));
3158
3650
  const original = process.env.OMX_LORE_COMMIT_GUARD;
3159
3651
  try {
3160
- process.env.OMX_LORE_COMMIT_GUARD = "0";
3652
+ delete process.env.OMX_LORE_COMMIT_GUARD;
3161
3653
  const result = await dispatchCodexNativeHook({
3162
3654
  hook_event_name: "PreToolUse",
3163
3655
  cwd,
@@ -3176,6 +3668,60 @@ exit 0
3176
3668
  await rm(cwd, { recursive: true, force: true });
3177
3669
  }
3178
3670
  });
3671
+ it("blocks non-Lore git commit messages when the Lore commit guard is enabled in CODEX_HOME config.toml", async () => {
3672
+ await withLoreGuardConfig("1", "config-enabled", async (cwd) => {
3673
+ const result = await dispatchCodexNativeHook({
3674
+ hook_event_name: "PreToolUse",
3675
+ cwd,
3676
+ tool_name: "Bash",
3677
+ tool_use_id: "tool-git-commit-lore-config-enabled",
3678
+ tool_input: { command: 'git commit -m "fix: conventional"' },
3679
+ }, { cwd });
3680
+ assert.equal(result.omxEventName, "pre-tool-use");
3681
+ assert.equal(result.outputJson?.decision, "block");
3682
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
3683
+ });
3684
+ });
3685
+ it("allows non-Lore git commit messages when the Lore commit guard is disabled in CODEX_HOME config.toml", async () => {
3686
+ await withLoreGuardConfig("0", "config-disabled", async (cwd) => {
3687
+ const result = await dispatchCodexNativeHook({
3688
+ hook_event_name: "PreToolUse",
3689
+ cwd,
3690
+ tool_name: "Bash",
3691
+ tool_use_id: "tool-git-commit-lore-config-disabled",
3692
+ tool_input: { command: 'git commit -m "fix: use conventional commit"' },
3693
+ }, { cwd });
3694
+ assert.equal(result.omxEventName, "pre-tool-use");
3695
+ assert.equal(result.outputJson, null);
3696
+ });
3697
+ });
3698
+ it("lets inline Lore commit guard values override a disabled CODEX_HOME config.toml", async () => {
3699
+ await withLoreGuardConfig("0", "config-inline-enabled", async (cwd) => {
3700
+ const result = await dispatchCodexNativeHook({
3701
+ hook_event_name: "PreToolUse",
3702
+ cwd,
3703
+ tool_name: "Bash",
3704
+ tool_use_id: "tool-git-commit-lore-config-inline-enabled",
3705
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git commit -m "fix: conventional"' },
3706
+ }, { cwd });
3707
+ assert.equal(result.omxEventName, "pre-tool-use");
3708
+ assert.equal(result.outputJson?.decision, "block");
3709
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
3710
+ });
3711
+ });
3712
+ it("restores default-off Lore guard when env -u removes a disabled CODEX_HOME config source", async () => {
3713
+ await withLoreGuardConfig("0", "config-codex-home-unset", async (cwd) => {
3714
+ const result = await dispatchCodexNativeHook({
3715
+ hook_event_name: "PreToolUse",
3716
+ cwd,
3717
+ tool_name: "Bash",
3718
+ tool_use_id: "tool-git-commit-lore-config-codex-home-unset",
3719
+ tool_input: { command: 'env -u CODEX_HOME git commit -m "fix: conventional"' },
3720
+ }, { cwd });
3721
+ assert.equal(result.omxEventName, "pre-tool-use");
3722
+ assert.equal(result.outputJson, null);
3723
+ });
3724
+ });
3179
3725
  it("allows non-Lore git commit messages when the Lore commit guard is disabled inline", async () => {
3180
3726
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-inline-disabled-"));
3181
3727
  try {
@@ -3193,7 +3739,30 @@ exit 0
3193
3739
  await rm(cwd, { recursive: true, force: true });
3194
3740
  }
3195
3741
  });
3196
- it("does not treat newline-separated Lore guard assignment as inline git commit env", async () => {
3742
+ it("allows inline disabled guard to override an enabled process env", async () => {
3743
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-inline-override-disabled-"));
3744
+ const original = process.env.OMX_LORE_COMMIT_GUARD;
3745
+ try {
3746
+ process.env.OMX_LORE_COMMIT_GUARD = "1";
3747
+ const result = await dispatchCodexNativeHook({
3748
+ hook_event_name: "PreToolUse",
3749
+ cwd,
3750
+ tool_name: "Bash",
3751
+ tool_use_id: "tool-git-commit-lore-inline-override-disabled",
3752
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=0 git commit -m "fix: conventional"' },
3753
+ }, { cwd });
3754
+ assert.equal(result.omxEventName, "pre-tool-use");
3755
+ assert.equal(result.outputJson, null);
3756
+ }
3757
+ finally {
3758
+ if (original === undefined)
3759
+ delete process.env.OMX_LORE_COMMIT_GUARD;
3760
+ else
3761
+ process.env.OMX_LORE_COMMIT_GUARD = original;
3762
+ await rm(cwd, { recursive: true, force: true });
3763
+ }
3764
+ });
3765
+ it("does not treat newline-separated Lore guard assignment as inline git commit opt-in", async () => {
3197
3766
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-newline-assignment-"));
3198
3767
  try {
3199
3768
  const result = await dispatchCodexNativeHook({
@@ -3201,21 +3770,33 @@ exit 0
3201
3770
  cwd,
3202
3771
  tool_name: "Bash",
3203
3772
  tool_use_id: "tool-git-commit-lore-newline-assignment",
3204
- tool_input: { command: 'OMX_LORE_COMMIT_GUARD=0\ngit commit -m "fix: conventional"' },
3773
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1\ngit commit -m "fix: conventional"' },
3205
3774
  }, { cwd });
3206
3775
  assert.equal(result.omxEventName, "pre-tool-use");
3207
- assert.equal(result.outputJson?.decision, "block");
3208
- assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
3776
+ assert.equal(result.outputJson, null);
3209
3777
  }
3210
3778
  finally {
3211
3779
  await rm(cwd, { recursive: true, force: true });
3212
3780
  }
3213
3781
  });
3214
- it("restores default-on Lore guard when env -u unsets a disabled process env", async () => {
3782
+ it("restores default-off Lore guard when env -u unsets a config.toml fallback", async () => {
3783
+ await withLoreGuardConfig("1", "config-env-unset", async (cwd) => {
3784
+ const result = await dispatchCodexNativeHook({
3785
+ hook_event_name: "PreToolUse",
3786
+ cwd,
3787
+ tool_name: "Bash",
3788
+ tool_use_id: "tool-git-commit-lore-config-env-unset",
3789
+ tool_input: { command: 'env -u OMX_LORE_COMMIT_GUARD git commit -m "fix: conventional"' },
3790
+ }, { cwd });
3791
+ assert.equal(result.omxEventName, "pre-tool-use");
3792
+ assert.equal(result.outputJson, null);
3793
+ });
3794
+ });
3795
+ it("restores default-off Lore guard when env -u unsets an enabled process env", async () => {
3215
3796
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-env-unset-"));
3216
3797
  const original = process.env.OMX_LORE_COMMIT_GUARD;
3217
3798
  try {
3218
- process.env.OMX_LORE_COMMIT_GUARD = "0";
3799
+ process.env.OMX_LORE_COMMIT_GUARD = "1";
3219
3800
  const result = await dispatchCodexNativeHook({
3220
3801
  hook_event_name: "PreToolUse",
3221
3802
  cwd,
@@ -3224,8 +3805,7 @@ exit 0
3224
3805
  tool_input: { command: 'env -u OMX_LORE_COMMIT_GUARD git commit -m "fix: conventional"' },
3225
3806
  }, { cwd });
3226
3807
  assert.equal(result.omxEventName, "pre-tool-use");
3227
- assert.equal(result.outputJson?.decision, "block");
3228
- assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
3808
+ assert.equal(result.outputJson, null);
3229
3809
  }
3230
3810
  finally {
3231
3811
  if (original === undefined)
@@ -3235,11 +3815,11 @@ exit 0
3235
3815
  await rm(cwd, { recursive: true, force: true });
3236
3816
  }
3237
3817
  });
3238
- it("restores default-on Lore guard when env -i clears a disabled process env", async () => {
3818
+ it("restores default-off Lore guard when env -i clears an enabled process env", async () => {
3239
3819
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-env-ignore-"));
3240
3820
  const original = process.env.OMX_LORE_COMMIT_GUARD;
3241
3821
  try {
3242
- process.env.OMX_LORE_COMMIT_GUARD = "0";
3822
+ process.env.OMX_LORE_COMMIT_GUARD = "1";
3243
3823
  const result = await dispatchCodexNativeHook({
3244
3824
  hook_event_name: "PreToolUse",
3245
3825
  cwd,
@@ -3248,8 +3828,7 @@ exit 0
3248
3828
  tool_input: { command: 'env -i PATH=/usr/bin git commit -m "fix: conventional"' },
3249
3829
  }, { cwd });
3250
3830
  assert.equal(result.omxEventName, "pre-tool-use");
3251
- assert.equal(result.outputJson?.decision, "block");
3252
- assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
3831
+ assert.equal(result.outputJson, null);
3253
3832
  }
3254
3833
  finally {
3255
3834
  if (original === undefined)
@@ -3259,7 +3838,7 @@ exit 0
3259
3838
  await rm(cwd, { recursive: true, force: true });
3260
3839
  }
3261
3840
  });
3262
- it("keeps Lore commit enforcement enabled for unknown inline guard values", async () => {
3841
+ it("keeps Lore commit enforcement disabled for unknown inline guard values", async () => {
3263
3842
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-inline-unknown-"));
3264
3843
  try {
3265
3844
  const result = await dispatchCodexNativeHook({
@@ -3270,8 +3849,7 @@ exit 0
3270
3849
  tool_input: { command: 'OMX_LORE_COMMIT_GUARD=maybe git commit -m "fix: conventional"' },
3271
3850
  }, { cwd });
3272
3851
  assert.equal(result.omxEventName, "pre-tool-use");
3273
- assert.equal(result.outputJson?.decision, "block");
3274
- assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
3852
+ assert.equal(result.outputJson, null);
3275
3853
  }
3276
3854
  finally {
3277
3855
  await rm(cwd, { recursive: true, force: true });
@@ -3300,7 +3878,7 @@ exit 0
3300
3878
  await rm(cwd, { recursive: true, force: true });
3301
3879
  }
3302
3880
  });
3303
- it("keeps Lore commit enforcement enabled for unknown guard values", async () => {
3881
+ it("keeps Lore commit enforcement disabled for unknown guard values", async () => {
3304
3882
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-lore-unknown-"));
3305
3883
  const original = process.env.OMX_LORE_COMMIT_GUARD;
3306
3884
  try {
@@ -3313,8 +3891,7 @@ exit 0
3313
3891
  tool_input: { command: 'git commit -m "fix tests"' },
3314
3892
  }, { cwd });
3315
3893
  assert.equal(result.omxEventName, "pre-tool-use");
3316
- assert.equal(result.outputJson?.decision, "block");
3317
- assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
3894
+ assert.equal(result.outputJson, null);
3318
3895
  }
3319
3896
  finally {
3320
3897
  if (original === undefined)
@@ -3407,7 +3984,7 @@ exit 0
3407
3984
  cwd,
3408
3985
  tool_name: "Bash",
3409
3986
  tool_use_id: "tool-git-commit-env-invalid",
3410
- tool_input: { command: 'HUSKY=0 git commit -m "fix tests"' },
3987
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 HUSKY=0 git commit -m "fix tests"' },
3411
3988
  }, { cwd });
3412
3989
  assert.equal(result.omxEventName, "pre-tool-use");
3413
3990
  assert.deepEqual(result.outputJson, {
@@ -3437,7 +4014,7 @@ exit 0
3437
4014
  cwd,
3438
4015
  tool_name: "Bash",
3439
4016
  tool_use_id: "tool-git-commit-option-invalid",
3440
- tool_input: { command: 'git -c core.editor=true commit -m "fix tests"' },
4017
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git -c core.editor=true commit -m "fix tests"' },
3441
4018
  }, { cwd });
3442
4019
  assert.equal(result.omxEventName, "pre-tool-use");
3443
4020
  assert.deepEqual(result.outputJson, {
@@ -3467,7 +4044,7 @@ exit 0
3467
4044
  cwd,
3468
4045
  tool_name: "Bash",
3469
4046
  tool_use_id: "tool-git-exe-commit-env-wrapper-invalid",
3470
- tool_input: { command: 'env git.exe commit -m "fix tests"' },
4047
+ tool_input: { command: 'env OMX_LORE_COMMIT_GUARD=1 git.exe commit -m "fix tests"' },
3471
4048
  }, { cwd });
3472
4049
  assert.equal(result.omxEventName, "pre-tool-use");
3473
4050
  assert.deepEqual(result.outputJson, {
@@ -3497,7 +4074,7 @@ exit 0
3497
4074
  cwd,
3498
4075
  tool_name: "Bash",
3499
4076
  tool_use_id: "tool-git-exe-commit-invalid",
3500
- tool_input: { command: 'git.exe commit -m "fix tests"' },
4077
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 git.exe commit -m "fix tests"' },
3501
4078
  }, { cwd });
3502
4079
  assert.equal(result.omxEventName, "pre-tool-use");
3503
4080
  assert.deepEqual(result.outputJson, {
@@ -3527,7 +4104,7 @@ exit 0
3527
4104
  cwd,
3528
4105
  tool_name: "Bash",
3529
4106
  tool_use_id: "tool-git-exe-commit-env-flag-wrapper-invalid",
3530
- tool_input: { command: 'env -i PATH=/usr/bin git.exe commit -m "fix tests"' },
4107
+ tool_input: { command: 'env -i PATH=/usr/bin OMX_LORE_COMMIT_GUARD=1 git.exe commit -m "fix tests"' },
3531
4108
  }, { cwd });
3532
4109
  assert.equal(result.omxEventName, "pre-tool-use");
3533
4110
  assert.deepEqual(result.outputJson, {
@@ -3557,7 +4134,7 @@ exit 0
3557
4134
  cwd,
3558
4135
  tool_name: "Bash",
3559
4136
  tool_use_id: "tool-git-exe-commit-env-value-wrapper-invalid",
3560
- tool_input: { command: 'env -u FOO git.exe commit -m "fix tests"' },
4137
+ tool_input: { command: 'env -u FOO OMX_LORE_COMMIT_GUARD=1 git.exe commit -m "fix tests"' },
3561
4138
  }, { cwd });
3562
4139
  assert.equal(result.omxEventName, "pre-tool-use");
3563
4140
  assert.deepEqual(result.outputJson, {
@@ -3587,7 +4164,7 @@ exit 0
3587
4164
  cwd,
3588
4165
  tool_name: "Bash",
3589
4166
  tool_use_id: "tool-git-exe-commit-windows-path-invalid",
3590
- tool_input: { command: '"C:/Program Files/Git/cmd/git.exe" commit -m "fix tests"' },
4167
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 "C:/Program Files/Git/cmd/git.exe" commit -m "fix tests"' },
3591
4168
  }, { cwd });
3592
4169
  assert.equal(result.omxEventName, "pre-tool-use");
3593
4170
  assert.deepEqual(result.outputJson, {
@@ -3617,7 +4194,7 @@ exit 0
3617
4194
  cwd,
3618
4195
  tool_name: "Bash",
3619
4196
  tool_use_id: "tool-git-exe-commit-windows-backslash-path-invalid",
3620
- tool_input: { command: '"C:\\Program Files\\Git\\cmd\\git.exe" commit -m "fix tests"' },
4197
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 "C:\\Program Files\\Git\\cmd\\git.exe" commit -m "fix tests"' },
3621
4198
  }, { cwd });
3622
4199
  assert.equal(result.omxEventName, "pre-tool-use");
3623
4200
  assert.deepEqual(result.outputJson, {
@@ -3647,7 +4224,7 @@ exit 0
3647
4224
  cwd,
3648
4225
  tool_name: "Bash",
3649
4226
  tool_use_id: "tool-git-commit-path-invalid",
3650
- tool_input: { command: '/usr/bin/git commit -m "fix tests"' },
4227
+ tool_input: { command: 'OMX_LORE_COMMIT_GUARD=1 /usr/bin/git commit -m "fix tests"' },
3651
4228
  }, { cwd });
3652
4229
  assert.equal(result.omxEventName, "pre-tool-use");
3653
4230
  assert.deepEqual(result.outputJson, {
@@ -3677,7 +4254,7 @@ exit 0
3677
4254
  cwd,
3678
4255
  tool_name: "Bash",
3679
4256
  tool_use_id: "tool-git-commit-file",
3680
- tool_input: { command: "git commit -F .git/COMMIT_EDITMSG" },
4257
+ tool_input: { command: "OMX_LORE_COMMIT_GUARD=1 git commit -F .git/COMMIT_EDITMSG" },
3681
4258
  }, { cwd });
3682
4259
  assert.equal(result.omxEventName, "pre-tool-use");
3683
4260
  assert.deepEqual(result.outputJson, {
@@ -3706,7 +4283,7 @@ exit 0
3706
4283
  tool_use_id: "tool-git-commit-missing-omx-coauthor",
3707
4284
  tool_input: {
3708
4285
  command: [
3709
- 'git commit',
4286
+ 'OMX_LORE_COMMIT_GUARD=1 git commit',
3710
4287
  '-m "Prevent invalid history from bypassing Lore enforcement"',
3711
4288
  '-m "The native pre-tool-use hook now blocks inline git commit messages that skip Lore trailers or the required OmX co-author trailer."',
3712
4289
  '-m "Constraint: Native PreToolUse can only inspect the Bash command text"',
@@ -3741,7 +4318,7 @@ exit 0
3741
4318
  tool_use_id: "tool-git-commit-valid",
3742
4319
  tool_input: {
3743
4320
  command: [
3744
- 'git commit',
4321
+ 'OMX_LORE_COMMIT_GUARD=1 git commit',
3745
4322
  '-m "Prevent invalid history from bypassing Lore enforcement"',
3746
4323
  '-m "The native pre-tool-use hook now blocks inline git commit messages that skip Lore trailers or the required OmX co-author trailer."',
3747
4324
  '-m "Constraint: Native PreToolUse can only inspect the Bash command text"',
@@ -3767,7 +4344,7 @@ exit 0
3767
4344
  tool_use_id: "tool-git-commit-compact-coauthor",
3768
4345
  tool_input: {
3769
4346
  command: [
3770
- 'git commit',
4347
+ 'OMX_LORE_COMMIT_GUARD=1 git commit',
3771
4348
  '-m "Launch lvisai.xyz intro site"',
3772
4349
  '-m "Co-authored-by: OmX <omx@oh-my-codex.dev>"',
3773
4350
  ].join(" "),
@@ -3790,7 +4367,7 @@ exit 0
3790
4367
  tool_use_id: "tool-git-commit-compact-trailers",
3791
4368
  tool_input: {
3792
4369
  command: [
3793
- 'git commit',
4370
+ 'OMX_LORE_COMMIT_GUARD=1 git commit',
3794
4371
  '-m "Launch lvisai.xyz intro site"',
3795
4372
  '-m "Constraint: Native PreToolUse can only inspect inline Bash command text\nTested: node --test dist/scripts/__tests__/codex-native-hook.test.js\n\nCo-authored-by: OmX <omx@oh-my-codex.dev>"',
3796
4373
  ].join(" "),
@@ -3813,7 +4390,7 @@ exit 0
3813
4390
  tool_use_id: "tool-git-commit-compact-no-separator",
3814
4391
  tool_input: {
3815
4392
  command: [
3816
- 'git commit',
4393
+ 'OMX_LORE_COMMIT_GUARD=1 git commit',
3817
4394
  '--message="Launch lvisai.xyz intro site\nCo-authored-by: OmX <omx@oh-my-codex.dev>"',
3818
4395
  ].join(" "),
3819
4396
  },
@@ -4622,6 +5199,80 @@ exit 0
4622
5199
  await rm(cwd, { recursive: true, force: true });
4623
5200
  }
4624
5201
  });
5202
+ it("allows Stop when terminal Autopilot run-state shadows stale session ralplan state", async () => {
5203
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-terminal-run-state-"));
5204
+ try {
5205
+ const stateDir = join(cwd, ".omx", "state");
5206
+ const sessionId = "sess-stop-autopilot-terminal-run-state";
5207
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
5208
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
5209
+ active: true,
5210
+ mode: "autopilot",
5211
+ current_phase: "ralplan",
5212
+ });
5213
+ await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
5214
+ version: 1,
5215
+ active: false,
5216
+ mode: "autopilot",
5217
+ outcome: "finish",
5218
+ lifecycle_outcome: "finished",
5219
+ current_phase: "complete",
5220
+ completed_at: "2026-05-20T11:00:00.000Z",
5221
+ updated_at: "2026-05-20T11:00:00.000Z",
5222
+ });
5223
+ const result = await dispatchCodexNativeHook({
5224
+ hook_event_name: "Stop",
5225
+ cwd,
5226
+ session_id: sessionId,
5227
+ thread_id: "thread-stop-autopilot-terminal-run-state",
5228
+ turn_id: "turn-stop-autopilot-terminal-run-state-1",
5229
+ last_assistant_message: "Done. Verification passed.",
5230
+ }, { cwd });
5231
+ assert.equal(result.omxEventName, "stop");
5232
+ assert.equal(result.outputJson, null);
5233
+ }
5234
+ finally {
5235
+ await rm(cwd, { recursive: true, force: true });
5236
+ }
5237
+ });
5238
+ it("still blocks Stop while Autopilot ralplan state is genuinely non-terminal", async () => {
5239
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-active-ralplan-"));
5240
+ try {
5241
+ const stateDir = join(cwd, ".omx", "state");
5242
+ const sessionId = "sess-stop-autopilot-active-ralplan";
5243
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
5244
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
5245
+ active: true,
5246
+ mode: "autopilot",
5247
+ current_phase: "ralplan",
5248
+ });
5249
+ await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
5250
+ version: 1,
5251
+ active: true,
5252
+ mode: "autopilot",
5253
+ outcome: "continue",
5254
+ current_phase: "ralplan",
5255
+ updated_at: "2026-05-20T11:00:00.000Z",
5256
+ });
5257
+ const result = await dispatchCodexNativeHook({
5258
+ hook_event_name: "Stop",
5259
+ cwd,
5260
+ session_id: sessionId,
5261
+ thread_id: "thread-stop-autopilot-active-ralplan",
5262
+ turn_id: "turn-stop-autopilot-active-ralplan-1",
5263
+ }, { cwd });
5264
+ assert.equal(result.omxEventName, "stop");
5265
+ assert.deepEqual(result.outputJson, {
5266
+ decision: "block",
5267
+ reason: "OMX autopilot is still active (phase: ralplan); continue the task and gather fresh verification evidence before stopping.",
5268
+ stopReason: "autopilot_ralplan",
5269
+ systemMessage: "OMX autopilot is still active (phase: ralplan).",
5270
+ });
5271
+ }
5272
+ finally {
5273
+ await rm(cwd, { recursive: true, force: true });
5274
+ }
5275
+ });
4625
5276
  it("does not block Stop from stale root Autopilot planning state when the explicit session has no scoped state", async () => {
4626
5277
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-autopilot-planning-"));
4627
5278
  try {
@@ -6961,16 +7612,25 @@ exit 0
6961
7612
  assert.equal(result.omxEventName, "stop");
6962
7613
  const reason = String(result.outputJson?.reason);
6963
7614
  assert.match(reason, /Ralph completion audit is missing required evidence/);
6964
- assert.match(reason, /state\.completion_audit = \{ passed: true, prompt_to_artifact_checklist: \[\.\.\.\], verification_evidence: \[\.\.\.\] \}/);
7615
+ assert.match(reason, /set "completion_audit" on the Ralph state object/);
7616
+ assert.doesNotMatch(reason, /state\.completion_audit/);
6965
7617
  assert.match(reason, /repo-relative JSON file/);
6966
7618
  assert.match(reason, /Markdown artifacts and flat top-level checklist\/evidence fields are not accepted/);
6967
7619
  assert.equal(result.outputJson?.stopReason, "ralph_completion_audit_missing_completion_audit");
6968
7620
  const reopened = JSON.parse(await readFile(statePath, "utf-8"));
6969
- assert.equal(reopened.active, true);
6970
- assert.equal(reopened.current_phase, "verifying");
7621
+ assert.equal(reopened.active, false);
7622
+ assert.equal(reopened.current_phase, "complete");
6971
7623
  assert.equal(reopened.completion_audit_gate, "blocked");
6972
7624
  assert.equal(reopened.completion_audit_missing_reason, "missing_completion_audit");
6973
- assert.equal(typeof reopened.completed_at, "undefined");
7625
+ assert.equal(reopened.completed_at, "2026-05-10T12:00:00.000Z");
7626
+ const repeat = await dispatchCodexNativeHook({
7627
+ hook_event_name: "Stop",
7628
+ cwd,
7629
+ session_id: sessionId,
7630
+ last_assistant_message: "Done. Ralph complete.",
7631
+ }, { cwd });
7632
+ assert.equal(repeat.outputJson?.stopReason, "ralph_completion_audit_missing_completion_audit");
7633
+ assert.doesNotMatch(String(repeat.outputJson?.reason), /Ralph is still active/);
6974
7634
  }
6975
7635
  finally {
6976
7636
  await rm(cwd, { recursive: true, force: true });