oh-my-codex 0.16.3 → 0.16.4

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 (278) hide show
  1. package/Cargo.lock +5 -5
  2. package/Cargo.toml +1 -1
  3. package/README.md +3 -3
  4. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js +9 -0
  5. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js.map +1 -1
  6. package/dist/cli/__tests__/cleanup.test.js +27 -0
  7. package/dist/cli/__tests__/cleanup.test.js.map +1 -1
  8. package/dist/cli/__tests__/codex-plugin-layout.test.js +7 -5
  9. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  10. package/dist/cli/__tests__/doctor-warning-copy.test.js +101 -6
  11. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  12. package/dist/cli/__tests__/index.test.js +131 -2
  13. package/dist/cli/__tests__/index.test.js.map +1 -1
  14. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +2 -0
  15. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  16. package/dist/cli/__tests__/ralph.test.js +47 -0
  17. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  18. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +2 -2
  19. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
  20. package/dist/cli/__tests__/setup-install-mode.test.js +272 -26
  21. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  22. package/dist/cli/__tests__/setup-refresh.test.js +85 -3
  23. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  24. package/dist/cli/__tests__/setup-scope.test.js +1 -1
  25. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  26. package/dist/cli/__tests__/setup-skills-overwrite.test.js +2 -1
  27. package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
  28. package/dist/cli/__tests__/team.test.js +108 -0
  29. package/dist/cli/__tests__/team.test.js.map +1 -1
  30. package/dist/cli/__tests__/ultragoal.test.js +69 -0
  31. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  32. package/dist/cli/__tests__/uninstall.test.js +54 -8
  33. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  34. package/dist/cli/cleanup.d.ts.map +1 -1
  35. package/dist/cli/cleanup.js +8 -4
  36. package/dist/cli/cleanup.js.map +1 -1
  37. package/dist/cli/codex-feature-probe.d.ts +9 -0
  38. package/dist/cli/codex-feature-probe.d.ts.map +1 -0
  39. package/dist/cli/codex-feature-probe.js +28 -0
  40. package/dist/cli/codex-feature-probe.js.map +1 -0
  41. package/dist/cli/doctor.d.ts +1 -0
  42. package/dist/cli/doctor.d.ts.map +1 -1
  43. package/dist/cli/doctor.js +152 -17
  44. package/dist/cli/doctor.js.map +1 -1
  45. package/dist/cli/index.d.ts +9 -2
  46. package/dist/cli/index.d.ts.map +1 -1
  47. package/dist/cli/index.js +135 -17
  48. package/dist/cli/index.js.map +1 -1
  49. package/dist/cli/mcp-parity.js +8 -8
  50. package/dist/cli/mcp-parity.js.map +1 -1
  51. package/dist/cli/plugin-marketplace.d.ts +3 -0
  52. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  53. package/dist/cli/plugin-marketplace.js +88 -0
  54. package/dist/cli/plugin-marketplace.js.map +1 -1
  55. package/dist/cli/ralph.d.ts.map +1 -1
  56. package/dist/cli/ralph.js +21 -0
  57. package/dist/cli/ralph.js.map +1 -1
  58. package/dist/cli/setup-preferences.d.ts +4 -0
  59. package/dist/cli/setup-preferences.d.ts.map +1 -1
  60. package/dist/cli/setup-preferences.js +7 -0
  61. package/dist/cli/setup-preferences.js.map +1 -1
  62. package/dist/cli/setup.d.ts +5 -3
  63. package/dist/cli/setup.d.ts.map +1 -1
  64. package/dist/cli/setup.js +114 -44
  65. package/dist/cli/setup.js.map +1 -1
  66. package/dist/cli/ultragoal.d.ts +1 -1
  67. package/dist/cli/ultragoal.d.ts.map +1 -1
  68. package/dist/cli/ultragoal.js +64 -5
  69. package/dist/cli/ultragoal.js.map +1 -1
  70. package/dist/cli/uninstall.d.ts +2 -0
  71. package/dist/cli/uninstall.d.ts.map +1 -1
  72. package/dist/cli/uninstall.js +12 -3
  73. package/dist/cli/uninstall.js.map +1 -1
  74. package/dist/config/__tests__/codex-feature-flags.test.d.ts +2 -0
  75. package/dist/config/__tests__/codex-feature-flags.test.d.ts.map +1 -0
  76. package/dist/config/__tests__/codex-feature-flags.test.js +35 -0
  77. package/dist/config/__tests__/codex-feature-flags.test.js.map +1 -0
  78. package/dist/config/__tests__/codex-hooks.test.js +7 -0
  79. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  80. package/dist/config/__tests__/generator-idempotent.test.js +70 -9
  81. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  82. package/dist/config/__tests__/generator-notify.test.js +116 -11
  83. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  84. package/dist/config/__tests__/wiki-config-contract.test.js +6 -3
  85. package/dist/config/__tests__/wiki-config-contract.test.js.map +1 -1
  86. package/dist/config/codex-feature-flags.d.ts +21 -0
  87. package/dist/config/codex-feature-flags.d.ts.map +1 -0
  88. package/dist/config/codex-feature-flags.js +56 -0
  89. package/dist/config/codex-feature-flags.js.map +1 -0
  90. package/dist/config/codex-hooks.d.ts +2 -0
  91. package/dist/config/codex-hooks.d.ts.map +1 -1
  92. package/dist/config/codex-hooks.js +25 -3
  93. package/dist/config/codex-hooks.js.map +1 -1
  94. package/dist/config/generator.d.ts +11 -2
  95. package/dist/config/generator.d.ts.map +1 -1
  96. package/dist/config/generator.js +221 -123
  97. package/dist/config/generator.js.map +1 -1
  98. package/dist/config/omx-first-party-mcp.d.ts +3 -1
  99. package/dist/config/omx-first-party-mcp.d.ts.map +1 -1
  100. package/dist/config/omx-first-party-mcp.js +2 -2
  101. package/dist/config/omx-first-party-mcp.js.map +1 -1
  102. package/dist/hooks/__tests__/keyword-detector.test.js +92 -2
  103. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  104. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js +125 -1
  105. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js.map +1 -1
  106. package/dist/hooks/__tests__/skill-catalog-hygiene.test.d.ts +2 -0
  107. package/dist/hooks/__tests__/skill-catalog-hygiene.test.d.ts.map +1 -0
  108. package/dist/hooks/__tests__/skill-catalog-hygiene.test.js +84 -0
  109. package/dist/hooks/__tests__/skill-catalog-hygiene.test.js.map +1 -0
  110. package/dist/hooks/agents-overlay.js +2 -2
  111. package/dist/hooks/agents-overlay.js.map +1 -1
  112. package/dist/hooks/keyword-detector.d.ts +1 -0
  113. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  114. package/dist/hooks/keyword-detector.js +7 -5
  115. package/dist/hooks/keyword-detector.js.map +1 -1
  116. package/dist/hud/__tests__/state.test.js +164 -0
  117. package/dist/hud/__tests__/state.test.js.map +1 -1
  118. package/dist/hud/state.d.ts.map +1 -1
  119. package/dist/hud/state.js +4 -5
  120. package/dist/hud/state.js.map +1 -1
  121. package/dist/mcp/__tests__/state-paths.test.js +61 -0
  122. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  123. package/dist/mcp/__tests__/state-server.test.js +166 -0
  124. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  125. package/dist/mcp/state-paths.d.ts.map +1 -1
  126. package/dist/mcp/state-paths.js +23 -2
  127. package/dist/mcp/state-paths.js.map +1 -1
  128. package/dist/modes/__tests__/base-session-scope.test.js +22 -0
  129. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  130. package/dist/modes/__tests__/base-tmux-pane.test.js +57 -26
  131. package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
  132. package/dist/modes/base.d.ts.map +1 -1
  133. package/dist/modes/base.js +5 -0
  134. package/dist/modes/base.js.map +1 -1
  135. package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.d.ts +2 -0
  136. package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.d.ts.map +1 -0
  137. package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js +316 -0
  138. package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js.map +1 -0
  139. package/dist/planning/__tests__/approved-launch-hint-lineage-matrix.test.d.ts +2 -0
  140. package/dist/planning/__tests__/approved-launch-hint-lineage-matrix.test.d.ts.map +1 -0
  141. package/dist/planning/__tests__/approved-launch-hint-lineage-matrix.test.js +481 -0
  142. package/dist/planning/__tests__/approved-launch-hint-lineage-matrix.test.js.map +1 -0
  143. package/dist/planning/__tests__/artifacts.test.js +533 -4
  144. package/dist/planning/__tests__/artifacts.test.js.map +1 -1
  145. package/dist/planning/__tests__/context-pack-status.test.js +524 -0
  146. package/dist/planning/__tests__/context-pack-status.test.js.map +1 -1
  147. package/dist/planning/__tests__/markdown-structure.test.d.ts +2 -0
  148. package/dist/planning/__tests__/markdown-structure.test.d.ts.map +1 -0
  149. package/dist/planning/__tests__/markdown-structure.test.js +459 -0
  150. package/dist/planning/__tests__/markdown-structure.test.js.map +1 -0
  151. package/dist/planning/__tests__/ready-context-pack-role-refs.test.js +523 -1
  152. package/dist/planning/__tests__/ready-context-pack-role-refs.test.js.map +1 -1
  153. package/dist/planning/artifacts.d.ts +1 -1
  154. package/dist/planning/artifacts.d.ts.map +1 -1
  155. package/dist/planning/artifacts.js +227 -28
  156. package/dist/planning/artifacts.js.map +1 -1
  157. package/dist/planning/context-pack-status.d.ts +25 -0
  158. package/dist/planning/context-pack-status.d.ts.map +1 -1
  159. package/dist/planning/context-pack-status.js +272 -31
  160. package/dist/planning/context-pack-status.js.map +1 -1
  161. package/dist/planning/markdown-structure.d.ts +20 -0
  162. package/dist/planning/markdown-structure.d.ts.map +1 -0
  163. package/dist/planning/markdown-structure.js +137 -0
  164. package/dist/planning/markdown-structure.js.map +1 -0
  165. package/dist/ralph/__tests__/completion-audit.test.d.ts +2 -0
  166. package/dist/ralph/__tests__/completion-audit.test.d.ts.map +1 -0
  167. package/dist/ralph/__tests__/completion-audit.test.js +121 -0
  168. package/dist/ralph/__tests__/completion-audit.test.js.map +1 -0
  169. package/dist/ralph/completion-audit.d.ts +8 -0
  170. package/dist/ralph/completion-audit.d.ts.map +1 -0
  171. package/dist/ralph/completion-audit.js +99 -0
  172. package/dist/ralph/completion-audit.js.map +1 -0
  173. package/dist/scripts/__tests__/codex-native-hook.test.js +220 -13
  174. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  175. package/dist/scripts/__tests__/notify-dispatcher.test.d.ts +2 -0
  176. package/dist/scripts/__tests__/notify-dispatcher.test.d.ts.map +1 -0
  177. package/dist/scripts/__tests__/notify-dispatcher.test.js +126 -0
  178. package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -0
  179. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  180. package/dist/scripts/codex-native-hook.js +133 -54
  181. package/dist/scripts/codex-native-hook.js.map +1 -1
  182. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  183. package/dist/scripts/codex-native-pre-post.js +4 -2
  184. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  185. package/dist/scripts/notify-dispatcher.js +30 -1
  186. package/dist/scripts/notify-dispatcher.js.map +1 -1
  187. package/dist/scripts/notify-hook.js +3 -1
  188. package/dist/scripts/notify-hook.js.map +1 -1
  189. package/dist/state/__tests__/workflow-transition.test.js +102 -27
  190. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  191. package/dist/state/operations.d.ts.map +1 -1
  192. package/dist/state/operations.js +9 -3
  193. package/dist/state/operations.js.map +1 -1
  194. package/dist/state/skill-active.d.ts +7 -0
  195. package/dist/state/skill-active.d.ts.map +1 -1
  196. package/dist/state/skill-active.js +25 -8
  197. package/dist/state/skill-active.js.map +1 -1
  198. package/dist/state/workflow-transition-reconcile.d.ts +1 -0
  199. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  200. package/dist/state/workflow-transition-reconcile.js +22 -15
  201. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  202. package/dist/state/workflow-transition.js +3 -3
  203. package/dist/state/workflow-transition.js.map +1 -1
  204. package/dist/team/__tests__/approved-execution.test.js +39 -0
  205. package/dist/team/__tests__/approved-execution.test.js.map +1 -1
  206. package/dist/team/__tests__/runtime.test.js +5 -0
  207. package/dist/team/__tests__/runtime.test.js.map +1 -1
  208. package/dist/team/__tests__/scaling.test.js +497 -2
  209. package/dist/team/__tests__/scaling.test.js.map +1 -1
  210. package/dist/team/__tests__/state-root.test.js +1 -1
  211. package/dist/team/__tests__/state-root.test.js.map +1 -1
  212. package/dist/team/__tests__/worker-bootstrap.test.js +8 -0
  213. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  214. package/dist/team/approved-execution.d.ts.map +1 -1
  215. package/dist/team/approved-execution.js +3 -0
  216. package/dist/team/approved-execution.js.map +1 -1
  217. package/dist/team/scaling.d.ts.map +1 -1
  218. package/dist/team/scaling.js +43 -0
  219. package/dist/team/scaling.js.map +1 -1
  220. package/dist/team/state-root.d.ts.map +1 -1
  221. package/dist/team/state-root.js +4 -0
  222. package/dist/team/state-root.js.map +1 -1
  223. package/dist/team/state.d.ts.map +1 -1
  224. package/dist/team/state.js +2 -6
  225. package/dist/team/state.js.map +1 -1
  226. package/dist/ultragoal/__tests__/artifacts.test.js +124 -1
  227. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  228. package/dist/ultragoal/__tests__/docs-contract.test.js +21 -0
  229. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  230. package/dist/ultragoal/artifacts.d.ts +44 -2
  231. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  232. package/dist/ultragoal/artifacts.js +197 -13
  233. package/dist/ultragoal/artifacts.js.map +1 -1
  234. package/dist/wiki/lifecycle.js +1 -1
  235. package/dist/wiki/lifecycle.js.map +1 -1
  236. package/package.json +1 -1
  237. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  238. package/plugins/oh-my-codex/.mcp.json +5 -5
  239. package/plugins/oh-my-codex/skills/analyze/SKILL.md +0 -2
  240. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +2 -2
  241. package/plugins/oh-my-codex/skills/code-review/SKILL.md +1 -3
  242. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +5 -7
  243. package/plugins/oh-my-codex/skills/doctor/SKILL.md +2 -2
  244. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +3 -3
  245. package/plugins/oh-my-codex/skills/pipeline/SKILL.md +3 -3
  246. package/plugins/oh-my-codex/skills/plan/SKILL.md +3 -6
  247. package/plugins/oh-my-codex/skills/ralph/SKILL.md +9 -10
  248. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +36 -3
  249. package/plugins/oh-my-codex/skills/ultraqa/SKILL.md +21 -24
  250. package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +8 -8
  251. package/plugins/oh-my-codex/skills/wiki/SKILL.md +13 -13
  252. package/skills/analyze/SKILL.md +0 -2
  253. package/skills/ask-claude/SKILL.md +5 -3
  254. package/skills/ask-gemini/SKILL.md +5 -3
  255. package/skills/autopilot/SKILL.md +2 -2
  256. package/skills/code-review/SKILL.md +1 -3
  257. package/skills/deep-interview/SKILL.md +5 -7
  258. package/skills/doctor/SKILL.md +2 -2
  259. package/skills/ecomode/SKILL.md +105 -1
  260. package/skills/frontend-ui-ux/SKILL.md +4 -26
  261. package/skills/git-master/SKILL.md +2 -4
  262. package/skills/omx-setup/SKILL.md +3 -3
  263. package/skills/pipeline/SKILL.md +3 -3
  264. package/skills/plan/SKILL.md +3 -6
  265. package/skills/ralph/SKILL.md +9 -10
  266. package/skills/swarm/SKILL.md +5 -3
  267. package/skills/tdd/SKILL.md +95 -1
  268. package/skills/ultragoal/SKILL.md +36 -3
  269. package/skills/ultraqa/SKILL.md +21 -24
  270. package/skills/ultrawork/SKILL.md +8 -8
  271. package/skills/web-clone/SKILL.md +348 -1
  272. package/skills/wiki/SKILL.md +13 -13
  273. package/src/scripts/__tests__/codex-native-hook.test.ts +231 -13
  274. package/src/scripts/__tests__/notify-dispatcher.test.ts +153 -0
  275. package/src/scripts/codex-native-hook.ts +160 -43
  276. package/src/scripts/codex-native-pre-post.ts +4 -1
  277. package/src/scripts/notify-dispatcher.ts +40 -1
  278. package/src/scripts/notify-hook.ts +3 -1
@@ -24,6 +24,7 @@ import { writeSessionStart } from "../../hooks/session.js";
24
24
  import { resetTriageConfigCache } from "../../hooks/triage-config.js";
25
25
  import { executeStateOperation } from "../../state/operations.js";
26
26
  import { OMX_TMUX_HUD_OWNER_ENV } from "../../hud/reconcile.js";
27
+ import { readAllState } from "../../hud/state.js";
27
28
  import { writePage } from "../../wiki/storage.js";
28
29
  import { WIKI_SCHEMA_VERSION } from "../../wiki/types.js";
29
30
 
@@ -236,7 +237,7 @@ describe("codex native hook config", () => {
236
237
  matcher?: string;
237
238
  hooks?: Array<Record<string, unknown>>;
238
239
  };
239
- assert.equal(sessionStart.matcher, "startup|resume");
240
+ assert.equal(sessionStart.matcher, "startup|resume|clear");
240
241
  assert.equal(sessionStart.hooks?.[0]?.statusMessage, undefined);
241
242
 
242
243
  const preToolUse = config.hooks.PreToolUse[0] as {
@@ -1217,7 +1218,7 @@ describe("codex native hook dispatch", () => {
1217
1218
  assert.equal(result.omxEventName, "keyword-detector");
1218
1219
  assert.equal(result.skillState?.skill, "ralplan");
1219
1220
  assert.ok(result.outputJson, "UserPromptSubmit should emit developer context");
1220
- assert.match(JSON.stringify(result.outputJson), /skill: ralplan activated and initial state initialized at \.omx\/state\/sessions\/sess-1\/ralplan-state\.json; write subsequent updates via omx_state MCP\./);
1221
+ assert.match(JSON.stringify(result.outputJson), /use CLI-first state updates via `omx state write\/read\/clear --input '<json>' --json`/);
1221
1222
 
1222
1223
  assert.equal(
1223
1224
  existsSync(join(cwd, ".omx", "state", "skill-active-state.json")),
@@ -1240,6 +1241,112 @@ describe("codex native hook dispatch", () => {
1240
1241
  }
1241
1242
  });
1242
1243
 
1244
+ it("records boxed keyword activation mode detail and skill state under OMX_ROOT", async () => {
1245
+ const root = await mkdtemp(join(tmpdir(), "omx-native-hook-boxed-"));
1246
+ const cwd = join(root, "source");
1247
+ const omxRoot = join(root, "box");
1248
+ const sessionId = "sess-boxed-ralplan";
1249
+ const previousOmxRoot = process.env.OMX_ROOT;
1250
+ const previousOmxStateRoot = process.env.OMX_STATE_ROOT;
1251
+ const previousTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
1252
+ const previousOmxSessionId = process.env.OMX_SESSION_ID;
1253
+ try {
1254
+ await mkdir(cwd, { recursive: true });
1255
+ process.env.OMX_ROOT = omxRoot;
1256
+ delete process.env.OMX_STATE_ROOT;
1257
+ delete process.env.OMX_TEAM_STATE_ROOT;
1258
+ process.env.OMX_SESSION_ID = sessionId;
1259
+
1260
+ const result = await dispatchCodexNativeHook(
1261
+ {
1262
+ hook_event_name: "UserPromptSubmit",
1263
+ cwd,
1264
+ session_id: sessionId,
1265
+ thread_id: "thread-boxed",
1266
+ turn_id: "turn-boxed",
1267
+ prompt: "$ralplan implement issue #1307",
1268
+ },
1269
+ { cwd },
1270
+ );
1271
+
1272
+ assert.equal(result.omxEventName, "keyword-detector");
1273
+ assert.equal(result.skillState?.skill, "ralplan");
1274
+
1275
+ const boxedSessionDir = join(omxRoot, ".omx", "state", "sessions", sessionId);
1276
+ assert.equal(existsSync(join(boxedSessionDir, "skill-active-state.json")), true);
1277
+ assert.equal(existsSync(join(boxedSessionDir, "ralplan-state.json")), true);
1278
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", sessionId, "skill-active-state.json")), false);
1279
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", sessionId, "ralplan-state.json")), false);
1280
+
1281
+ const hudState = await readAllState(cwd);
1282
+ assert.equal(hudState.ralplan?.active, true);
1283
+ assert.equal(hudState.ralplan?.current_phase, "planning");
1284
+ } finally {
1285
+ if (typeof previousOmxRoot === "string") process.env.OMX_ROOT = previousOmxRoot;
1286
+ else delete process.env.OMX_ROOT;
1287
+ if (typeof previousOmxStateRoot === "string") process.env.OMX_STATE_ROOT = previousOmxStateRoot;
1288
+ else delete process.env.OMX_STATE_ROOT;
1289
+ if (typeof previousTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = previousTeamStateRoot;
1290
+ else delete process.env.OMX_TEAM_STATE_ROOT;
1291
+ if (typeof previousOmxSessionId === "string") process.env.OMX_SESSION_ID = previousOmxSessionId;
1292
+ else delete process.env.OMX_SESSION_ID;
1293
+ await rm(root, { recursive: true, force: true });
1294
+ }
1295
+ });
1296
+
1297
+ it("records native keyword activation mode detail and skill state under OMX_TEAM_STATE_ROOT", async () => {
1298
+ const root = await mkdtemp(join(tmpdir(), "omx-native-hook-team-root-"));
1299
+ const cwd = join(root, "source");
1300
+ const teamStateRoot = join(root, "team-state");
1301
+ const sessionId = "sess-team-root-ralplan";
1302
+ const previousOmxRoot = process.env.OMX_ROOT;
1303
+ const previousOmxStateRoot = process.env.OMX_STATE_ROOT;
1304
+ const previousTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
1305
+ const previousOmxSessionId = process.env.OMX_SESSION_ID;
1306
+ try {
1307
+ await mkdir(cwd, { recursive: true });
1308
+ delete process.env.OMX_ROOT;
1309
+ delete process.env.OMX_STATE_ROOT;
1310
+ process.env.OMX_TEAM_STATE_ROOT = teamStateRoot;
1311
+ process.env.OMX_SESSION_ID = sessionId;
1312
+
1313
+ const result = await dispatchCodexNativeHook(
1314
+ {
1315
+ hook_event_name: "UserPromptSubmit",
1316
+ cwd,
1317
+ session_id: sessionId,
1318
+ thread_id: "thread-team-root",
1319
+ turn_id: "turn-team-root",
1320
+ prompt: "$ralplan implement issue #1307",
1321
+ },
1322
+ { cwd },
1323
+ );
1324
+
1325
+ assert.equal(result.omxEventName, "keyword-detector");
1326
+ assert.equal(result.skillState?.skill, "ralplan");
1327
+
1328
+ const teamSessionDir = join(teamStateRoot, "sessions", sessionId);
1329
+ assert.equal(existsSync(join(teamSessionDir, "skill-active-state.json")), true);
1330
+ assert.equal(existsSync(join(teamSessionDir, "ralplan-state.json")), true);
1331
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", sessionId, "skill-active-state.json")), false);
1332
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", sessionId, "ralplan-state.json")), false);
1333
+
1334
+ const hudState = await readAllState(cwd);
1335
+ assert.equal(hudState.ralplan?.active, true);
1336
+ assert.equal(hudState.ralplan?.current_phase, "planning");
1337
+ } finally {
1338
+ if (typeof previousOmxRoot === "string") process.env.OMX_ROOT = previousOmxRoot;
1339
+ else delete process.env.OMX_ROOT;
1340
+ if (typeof previousOmxStateRoot === "string") process.env.OMX_STATE_ROOT = previousOmxStateRoot;
1341
+ else delete process.env.OMX_STATE_ROOT;
1342
+ if (typeof previousTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = previousTeamStateRoot;
1343
+ else delete process.env.OMX_TEAM_STATE_ROOT;
1344
+ if (typeof previousOmxSessionId === "string") process.env.OMX_SESSION_ID = previousOmxSessionId;
1345
+ else delete process.env.OMX_SESSION_ID;
1346
+ await rm(root, { recursive: true, force: true });
1347
+ }
1348
+ });
1349
+
1243
1350
  it("warns completion-like prompts when active goal workflows need Codex snapshot reconciliation", async () => {
1244
1351
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-goal-warning-"));
1245
1352
  try {
@@ -1474,7 +1581,7 @@ describe("codex native hook dispatch", () => {
1474
1581
  (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
1475
1582
  );
1476
1583
  assert.match(message, /\$oh-my-codex:ralplan" -> ralplan/);
1477
- assert.match(message, /skill: ralplan activated and initial state initialized at \.omx\/state\/sessions\/sess-plugin-1\/ralplan-state\.json; write subsequent updates via omx_state MCP\./);
1584
+ assert.match(message, /use CLI-first state updates via `omx state write\/read\/clear --input '<json>' --json`/);
1478
1585
  assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-plugin-1", "ralplan-state.json")), true);
1479
1586
  } finally {
1480
1587
  await rm(cwd, { recursive: true, force: true });
@@ -1691,7 +1798,7 @@ describe("codex native hook dispatch", () => {
1691
1798
  (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
1692
1799
  );
1693
1800
  assert.match(message, /\$ralph" -> ralph/);
1694
- assert.match(message, /skill: ralph activated and initial state initialized at \.omx\/state\/sessions\/sess-ralph-msg\/ralph-state\.json; write subsequent updates via omx_state MCP\./);
1801
+ assert.match(message, /use CLI-first state updates via `omx state write\/read\/clear --input '<json>' --json`/);
1695
1802
  assert.match(message, /Prompt-side `\$ralph` activation seeds Ralph workflow state only; it does not invoke `omx ralph`\./);
1696
1803
  assert.match(message, /Use `omx ralph --prd \.\.\.` only when you explicitly want the PRD-gated CLI startup path\./);
1697
1804
  } finally {
@@ -1721,7 +1828,7 @@ describe("codex native hook dispatch", () => {
1721
1828
  (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
1722
1829
  );
1723
1830
  assert.match(message, /\$oh-my-codex:ralph" -> ralph/);
1724
- assert.match(message, /skill: ralph activated and initial state initialized at \.omx\/state\/sessions\/sess-plugin-ralph-msg\/ralph-state\.json; write subsequent updates via omx_state MCP\./);
1831
+ assert.match(message, /use CLI-first state updates via `omx state write\/read\/clear --input '<json>' --json`/);
1725
1832
  assert.match(message, /Prompt-side `\$ralph` activation seeds Ralph workflow state only; it does not invoke `omx ralph`\./);
1726
1833
  } finally {
1727
1834
  await rm(cwd, { recursive: true, force: true });
@@ -1803,7 +1910,7 @@ describe("codex native hook dispatch", () => {
1803
1910
  (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
1804
1911
  );
1805
1912
  assert.match(message, /\$deep-interview" -> deep-interview/);
1806
- assert.match(message, /skill: deep-interview activated and initial state initialized at \.omx\/state\/sessions\/sess-deep-interview-msg\/deep-interview-state\.json; write subsequent updates via omx_state MCP\./);
1913
+ assert.match(message, /use CLI-first state updates via `omx state write\/read\/clear --input '<json>' --json`/);
1807
1914
  assert.match(message, /Deep-interview is active, but this session is not attached to tmux/);
1808
1915
  assert.match(message, /Do not invoke `omx question`, `omx hud`, or `omx team`/);
1809
1916
  assert.match(message, /native structured question tool when available/);
@@ -2279,8 +2386,11 @@ export async function onHookEvent(event) {
2279
2386
 
2280
2387
  assert.match(JSON.stringify(denied.outputJson), /denied workflow keyword/i);
2281
2388
  assert.match(JSON.stringify(denied.outputJson), /Unsupported workflow overlap: team \+ autopilot\./);
2282
- assert.match(JSON.stringify(denied.outputJson), /`omx state clear --mode <mode>`/);
2283
- assert.match(JSON.stringify(denied.outputJson), /`omx_state\.\*` MCP tools/);
2389
+ assert.match(JSON.stringify(denied.outputJson), /omx state clear --input/);
2390
+ assert.match(JSON.stringify(denied.outputJson), /mode\\":\\"<mode>/);
2391
+ assert.match(JSON.stringify(denied.outputJson), /--json/);
2392
+ assert.match(JSON.stringify(denied.outputJson), /explicit MCP compatibility is enabled/);
2393
+ assert.match(JSON.stringify(denied.outputJson), /`omx_state\.\*` tools/);
2284
2394
  assert.equal(
2285
2395
  existsSync(join(cwd, ".omx", "state", "sessions", "sess-deny-1", "autopilot-state.json")),
2286
2396
  false,
@@ -2357,7 +2467,7 @@ export async function onHookEvent(event) {
2357
2467
  assert.match(message, /\$ralph" -> ralph/);
2358
2468
  assert.doesNotMatch(message, /mode transiting:/);
2359
2469
  assert.match(message, /planning preserved over simultaneous execution follow-up; deferred skills: team, ralph\./);
2360
- assert.match(message, /skill: ralplan activated and initial state initialized at \.omx\/state\/sessions\/sess-multi-1\/ralplan-state\.json; write subsequent updates via omx_state MCP\./);
2470
+ assert.match(message, /use CLI-first state updates via `omx state write\/read\/clear --input '<json>' --json`/);
2361
2471
  assert.doesNotMatch(message, /Use the durable OMX team runtime via `omx team \.\.\.`/);
2362
2472
  } finally {
2363
2473
  await rm(cwd, { recursive: true, force: true });
@@ -2389,7 +2499,7 @@ export async function onHookEvent(event) {
2389
2499
  assert.match(message, /\$oh-my-codex:ralph" -> ralph/);
2390
2500
  assert.doesNotMatch(message, /mode transiting:/);
2391
2501
  assert.match(message, /planning preserved over simultaneous execution follow-up; deferred skills: team, ralph\./);
2392
- assert.match(message, /skill: ralplan activated and initial state initialized at \.omx\/state\/sessions\/sess-plugin-multi-1\/ralplan-state\.json; write subsequent updates via omx_state MCP\./);
2502
+ assert.match(message, /use CLI-first state updates via `omx state write\/read\/clear --input '<json>' --json`/);
2393
2503
  } finally {
2394
2504
  await rm(cwd, { recursive: true, force: true });
2395
2505
  }
@@ -4499,7 +4609,7 @@ exit 0
4499
4609
  );
4500
4610
  assert.match(
4501
4611
  additionalContext,
4502
- /omx state state_write --input/,
4612
+ /omx state write --input/,
4503
4613
  );
4504
4614
  assert.match(
4505
4615
  additionalContext,
@@ -4747,7 +4857,7 @@ exit 0
4747
4857
  assert.equal(hookSpecificOutput?.hookEventName, "PostToolUse");
4748
4858
  assert.match(
4749
4859
  String(hookSpecificOutput?.additionalContext || ""),
4750
- /Retry via CLI parity with `omx state state_write --input '\{\}' --json`\./,
4860
+ /Retry via CLI parity with `omx state write --input '\{\}' --json`\./,
4751
4861
  );
4752
4862
  assert.match(
4753
4863
  String(hookSpecificOutput?.additionalContext || ""),
@@ -4833,7 +4943,7 @@ exit 0
4833
4943
  hookSpecificOutput: {
4834
4944
  hookEventName: "PostToolUse",
4835
4945
  additionalContext:
4836
- "Clear MCP transport-death signal detected. Preserve current team/runtime state. Retry via CLI parity with `omx state state_write --input '{\"mode\":\"team\",\"active\":true}' --json`. OMX MCP servers are plain Node stdio processes, so they still shut down when stdin/transport closes. If this happened during team runtime, inspect first with `omx team status <team>` or `omx team api read-stall-state --input '{\"team_name\":\"<team>\"}' --json`, and only force cleanup after capturing needed state. For root-cause debugging, rerun with `OMX_MCP_TRANSPORT_DEBUG=1` to log why the stdio transport closed.",
4946
+ "Clear MCP transport-death signal detected. Preserve current team/runtime state. Retry via CLI parity with `omx state write --input '{\"mode\":\"team\",\"active\":true}' --json`. OMX MCP servers are plain Node stdio processes, so they still shut down when stdin/transport closes. If this happened during team runtime, inspect first with `omx team status <team>` or `omx team api read-stall-state --input '{\"team_name\":\"<team>\"}' --json`, and only force cleanup after capturing needed state. For root-cause debugging, rerun with `OMX_MCP_TRANSPORT_DEBUG=1` to log why the stdio transport closed.",
4837
4947
  },
4838
4948
  });
4839
4949
 
@@ -6253,6 +6363,41 @@ exit 0
6253
6363
  }
6254
6364
  });
6255
6365
 
6366
+ it("ignores stale source-root team Stop fallback when OMX_TEAM_STATE_ROOT is authoritative", async () => {
6367
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-stale-source-root-"));
6368
+ const teamStateRoot = join(cwd, "shared-team-state");
6369
+ const priorTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
6370
+ try {
6371
+ process.env.OMX_TEAM_STATE_ROOT = teamStateRoot;
6372
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
6373
+ await mkdir(join(teamStateRoot, "team", "stale-source-team"), { recursive: true });
6374
+ await writeJson(join(cwd, ".omx", "state", "team-state.json"), {
6375
+ active: true,
6376
+ team_name: "stale-source-team",
6377
+ current_phase: "team-exec",
6378
+ });
6379
+ await writeJson(join(teamStateRoot, "team", "stale-source-team", "phase.json"), {
6380
+ current_phase: "team-exec",
6381
+ });
6382
+
6383
+ const result = await dispatchCodexNativeHook(
6384
+ {
6385
+ hook_event_name: "Stop",
6386
+ cwd,
6387
+ session_id: "sess-stale-source-team",
6388
+ },
6389
+ { cwd },
6390
+ );
6391
+
6392
+ assert.equal(result.omxEventName, "stop");
6393
+ assert.equal(result.outputJson, null);
6394
+ } finally {
6395
+ if (typeof priorTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = priorTeamStateRoot;
6396
+ else delete process.env.OMX_TEAM_STATE_ROOT;
6397
+ await rm(cwd, { recursive: true, force: true });
6398
+ }
6399
+ });
6400
+
6256
6401
  it("returns Stop continuation output from canonical team state rooted via OMX_TEAM_STATE_ROOT", async () => {
6257
6402
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-env-root-"));
6258
6403
  const previousTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
@@ -7288,6 +7433,79 @@ exit 0
7288
7433
  }
7289
7434
  });
7290
7435
 
7436
+ it("blocks Codex App Stop when Ralph is marked complete without completion-audit evidence", async () => {
7437
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-complete-audit-missing-"));
7438
+ try {
7439
+ const sessionId = "sess-ralph-complete-missing";
7440
+ const statePath = join(cwd, ".omx", "state", "sessions", sessionId, "ralph-state.json");
7441
+ await writeJson(join(cwd, ".omx", "state", "session.json"), { session_id: sessionId, native_session_id: sessionId, cwd });
7442
+ await writeJson(statePath, {
7443
+ active: false,
7444
+ mode: "ralph",
7445
+ current_phase: "complete",
7446
+ session_id: sessionId,
7447
+ completed_at: "2026-05-10T12:00:00.000Z",
7448
+ });
7449
+
7450
+ const result = await dispatchCodexNativeHook(
7451
+ {
7452
+ hook_event_name: "Stop",
7453
+ cwd,
7454
+ session_id: sessionId,
7455
+ last_assistant_message: "Done. Ralph complete.",
7456
+ },
7457
+ { cwd },
7458
+ );
7459
+
7460
+ assert.equal(result.omxEventName, "stop");
7461
+ assert.match(String(result.outputJson?.reason), /Ralph completion audit is missing required evidence/);
7462
+ assert.equal(result.outputJson?.stopReason, "ralph_completion_audit_missing_completion_audit");
7463
+ const reopened = JSON.parse(await readFile(statePath, "utf-8")) as Record<string, unknown>;
7464
+ assert.equal(reopened.active, true);
7465
+ assert.equal(reopened.current_phase, "verifying");
7466
+ assert.equal(reopened.completion_audit_gate, "blocked");
7467
+ assert.equal(reopened.completion_audit_missing_reason, "missing_completion_audit");
7468
+ assert.equal(typeof reopened.completed_at, "undefined");
7469
+ } finally {
7470
+ await rm(cwd, { recursive: true, force: true });
7471
+ }
7472
+ });
7473
+
7474
+ it("allows Codex App Stop when complete Ralph state carries checklist and verification evidence", async () => {
7475
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralph-complete-audit-present-"));
7476
+ try {
7477
+ const sessionId = "sess-ralph-complete-present";
7478
+ await writeJson(join(cwd, ".omx", "state", "session.json"), { session_id: sessionId, native_session_id: sessionId, cwd });
7479
+ await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "ralph-state.json"), {
7480
+ active: false,
7481
+ mode: "ralph",
7482
+ current_phase: "complete",
7483
+ session_id: sessionId,
7484
+ completed_at: "2026-05-10T12:00:00.000Z",
7485
+ completion_audit: {
7486
+ passed: true,
7487
+ prompt_to_artifact_checklist: ["issue #2260 fixed", "tests added"],
7488
+ verification_evidence: ["node --test dist/scripts/__tests__/codex-native-hook.test.js"],
7489
+ },
7490
+ });
7491
+
7492
+ const result = await dispatchCodexNativeHook(
7493
+ {
7494
+ hook_event_name: "Stop",
7495
+ cwd,
7496
+ session_id: sessionId,
7497
+ last_assistant_message: "Done with completion audit evidence recorded.",
7498
+ },
7499
+ { cwd },
7500
+ );
7501
+
7502
+ assert.equal(result.omxEventName, "stop");
7503
+ assert.equal(result.outputJson, null);
7504
+ } finally {
7505
+ await rm(cwd, { recursive: true, force: true });
7506
+ }
7507
+ });
7508
+
7291
7509
  it("returns Stop continuation output while Ralph is active without an explicit session pin", async () => {
7292
7510
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-"));
7293
7511
  try {
@@ -0,0 +1,153 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { existsSync } from "node:fs";
4
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { spawnSync } from "node:child_process";
8
+
9
+ function runDispatcher(metadataPath: string): void {
10
+ const dispatcherScript = join(process.cwd(), "dist", "scripts", "notify-dispatcher.js");
11
+ const result = spawnSync(
12
+ process.execPath,
13
+ [dispatcherScript, "--metadata", metadataPath, JSON.stringify({ type: "test" })],
14
+ { encoding: "utf-8", windowsHide: true },
15
+ );
16
+ assert.equal(result.status, 0, result.stderr || result.stdout);
17
+ }
18
+
19
+ describe("notify dispatcher previousNotify guard", () => {
20
+ it("skips stale OMX-managed previousNotify dispatcher entries", () => {
21
+ const wd = mkdtempSync(join(tmpdir(), "omx-notify-dispatcher-stale-"));
22
+ try {
23
+ const oldPkgScripts = join(wd, "global", "oh-my-codex", "dist", "scripts");
24
+ mkdirSync(oldPkgScripts, { recursive: true });
25
+ const stalePreviousMarker = join(wd, "stale-previous-ran");
26
+ const omxMarker = join(wd, "omx-ran");
27
+ const staleDispatcher = join(oldPkgScripts, "notify-dispatcher.js");
28
+ const omxHook = join(wd, "current-notify-hook.js");
29
+ writeFileSync(staleDispatcher, `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(stalePreviousMarker)}, "ran");\n`);
30
+ writeFileSync(omxHook, `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(omxMarker)}, "ran");\n`);
31
+ const metadataPath = join(wd, "notify-dispatch.json");
32
+ writeFileSync(
33
+ metadataPath,
34
+ JSON.stringify({
35
+ managedBy: "oh-my-codex",
36
+ version: 1,
37
+ previousNotify: [process.execPath, staleDispatcher, "--metadata", metadataPath],
38
+ omxNotify: [process.execPath, omxHook],
39
+ dispatcherNotify: [
40
+ process.execPath,
41
+ join(process.cwd(), "dist", "scripts", "notify-dispatcher.js"),
42
+ "--metadata",
43
+ metadataPath,
44
+ ],
45
+ }),
46
+ );
47
+
48
+ runDispatcher(metadataPath);
49
+
50
+ assert.equal(existsSync(stalePreviousMarker), false);
51
+ assert.equal(readFileSync(omxMarker, "utf-8"), "ran");
52
+ } finally {
53
+ rmSync(wd, { recursive: true, force: true });
54
+ }
55
+ });
56
+
57
+ it("skips stale OMX-managed previousNotify dispatcher entries behind node flags", () => {
58
+ const wd = mkdtempSync(join(tmpdir(), "omx-notify-dispatcher-flagged-stale-"));
59
+ try {
60
+ const oldPkgScripts = join(wd, "global", "oh-my-codex", "dist", "scripts");
61
+ mkdirSync(oldPkgScripts, { recursive: true });
62
+ const stalePreviousMarker = join(wd, "stale-previous-ran");
63
+ const omxMarker = join(wd, "omx-ran");
64
+ const staleDispatcher = join(oldPkgScripts, "notify-dispatcher.js");
65
+ const omxHook = join(wd, "current-notify-hook.js");
66
+ writeFileSync(staleDispatcher, `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(stalePreviousMarker)}, "ran");\n`);
67
+ writeFileSync(omxHook, `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(omxMarker)}, "ran");\n`);
68
+ const metadataPath = join(wd, "notify-dispatch.json");
69
+ writeFileSync(
70
+ metadataPath,
71
+ JSON.stringify({
72
+ managedBy: "oh-my-codex",
73
+ version: 1,
74
+ previousNotify: [
75
+ process.execPath,
76
+ "--no-warnings",
77
+ staleDispatcher,
78
+ "--metadata",
79
+ metadataPath,
80
+ ],
81
+ omxNotify: [process.execPath, omxHook],
82
+ }),
83
+ );
84
+
85
+ runDispatcher(metadataPath);
86
+
87
+ assert.equal(existsSync(stalePreviousMarker), false);
88
+ assert.equal(readFileSync(omxMarker, "utf-8"), "ran");
89
+ } finally {
90
+ rmSync(wd, { recursive: true, force: true });
91
+ }
92
+ });
93
+
94
+ it("preserves and runs real user previousNotify entries", () => {
95
+ const wd = mkdtempSync(join(tmpdir(), "omx-notify-dispatcher-user-"));
96
+ try {
97
+ const userMarker = join(wd, "user-ran");
98
+ const omxMarker = join(wd, "omx-ran");
99
+ const userScript = join(wd, "user-notify.js");
100
+ const omxHook = join(wd, "current-notify-hook.js");
101
+ writeFileSync(userScript, `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(userMarker)}, "ran");\n`);
102
+ writeFileSync(omxHook, `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(omxMarker)}, "ran");\n`);
103
+ const metadataPath = join(wd, "notify-dispatch.json");
104
+ writeFileSync(
105
+ metadataPath,
106
+ JSON.stringify({
107
+ managedBy: "oh-my-codex",
108
+ version: 1,
109
+ previousNotify: [process.execPath, userScript],
110
+ omxNotify: [process.execPath, omxHook],
111
+ }),
112
+ );
113
+
114
+ runDispatcher(metadataPath);
115
+
116
+ assert.equal(readFileSync(userMarker, "utf-8"), "ran");
117
+ assert.equal(readFileSync(omxMarker, "utf-8"), "ran");
118
+ } finally {
119
+ rmSync(wd, { recursive: true, force: true });
120
+ }
121
+ });
122
+
123
+ it("does not mistake real user notify arguments for managed entrypoints", () => {
124
+ const wd = mkdtempSync(join(tmpdir(), "omx-notify-dispatcher-user-arg-"));
125
+ try {
126
+ const userMarker = join(wd, "user-ran");
127
+ const userScript = join(wd, "user-notify.js");
128
+ writeFileSync(
129
+ userScript,
130
+ `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(userMarker)}, process.argv.slice(2).join("\\n"));\n`,
131
+ );
132
+ const metadataPath = join(wd, "notify-dispatch.json");
133
+ writeFileSync(
134
+ metadataPath,
135
+ JSON.stringify({
136
+ managedBy: "oh-my-codex",
137
+ version: 1,
138
+ previousNotify: [
139
+ process.execPath,
140
+ userScript,
141
+ "/opt/homebrew/lib/node_modules/oh-my-codex/dist/scripts/notify-hook.js",
142
+ ],
143
+ }),
144
+ );
145
+
146
+ runDispatcher(metadataPath);
147
+
148
+ assert.match(readFileSync(userMarker, "utf-8"), /notify-hook\.js/);
149
+ } finally {
150
+ rmSync(wd, { recursive: true, force: true });
151
+ }
152
+ });
153
+ });