oh-my-codex 0.18.6 → 0.18.7

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 +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +56 -7
  4. package/dist/agents/__tests__/definitions.test.js +11 -0
  5. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  6. package/dist/agents/__tests__/native-config.test.js +14 -5
  7. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  8. package/dist/agents/definitions.d.ts +2 -0
  9. package/dist/agents/definitions.d.ts.map +1 -1
  10. package/dist/agents/definitions.js +4 -1
  11. package/dist/agents/definitions.js.map +1 -1
  12. package/dist/agents/native-config.js +2 -2
  13. package/dist/agents/native-config.js.map +1 -1
  14. package/dist/autopilot/__tests__/fsm.test.d.ts +2 -0
  15. package/dist/autopilot/__tests__/fsm.test.d.ts.map +1 -0
  16. package/dist/autopilot/__tests__/fsm.test.js +75 -0
  17. package/dist/autopilot/__tests__/fsm.test.js.map +1 -0
  18. package/dist/autopilot/__tests__/ralplan-gate.test.d.ts +2 -0
  19. package/dist/autopilot/__tests__/ralplan-gate.test.d.ts.map +1 -0
  20. package/dist/autopilot/__tests__/ralplan-gate.test.js +79 -0
  21. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -0
  22. package/dist/autopilot/deep-interview-gate.d.ts +18 -0
  23. package/dist/autopilot/deep-interview-gate.d.ts.map +1 -0
  24. package/dist/autopilot/deep-interview-gate.js +256 -0
  25. package/dist/autopilot/deep-interview-gate.js.map +1 -0
  26. package/dist/autopilot/fsm.d.ts +13 -0
  27. package/dist/autopilot/fsm.d.ts.map +1 -0
  28. package/dist/autopilot/fsm.js +70 -0
  29. package/dist/autopilot/fsm.js.map +1 -0
  30. package/dist/autopilot/ralplan-gate.d.ts +17 -0
  31. package/dist/autopilot/ralplan-gate.d.ts.map +1 -0
  32. package/dist/autopilot/ralplan-gate.js +61 -0
  33. package/dist/autopilot/ralplan-gate.js.map +1 -0
  34. package/dist/cli/__tests__/index.test.js +24 -4
  35. package/dist/cli/__tests__/index.test.js.map +1 -1
  36. package/dist/cli/__tests__/launch-fallback.test.js +175 -6
  37. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  38. package/dist/cli/__tests__/question.test.js +100 -0
  39. package/dist/cli/__tests__/question.test.js.map +1 -1
  40. package/dist/cli/__tests__/setup-refresh.test.js +18 -0
  41. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  42. package/dist/cli/__tests__/team.test.js +2 -2
  43. package/dist/cli/__tests__/team.test.js.map +1 -1
  44. package/dist/cli/index.d.ts +3 -1
  45. package/dist/cli/index.d.ts.map +1 -1
  46. package/dist/cli/index.js +191 -36
  47. package/dist/cli/index.js.map +1 -1
  48. package/dist/cli/question.d.ts.map +1 -1
  49. package/dist/cli/question.js +36 -5
  50. package/dist/cli/question.js.map +1 -1
  51. package/dist/config/__tests__/deep-interview.test.js +7 -6
  52. package/dist/config/__tests__/deep-interview.test.js.map +1 -1
  53. package/dist/config/deep-interview.d.ts.map +1 -1
  54. package/dist/config/deep-interview.js +14 -4
  55. package/dist/config/deep-interview.js.map +1 -1
  56. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +8 -0
  57. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  58. package/dist/hooks/__tests__/deep-interview-contract.test.js +10 -0
  59. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  60. package/dist/hooks/__tests__/keyword-detector.test.js +649 -11
  61. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  62. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +63 -0
  63. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  64. package/dist/hooks/__tests__/session.test.js +25 -0
  65. package/dist/hooks/__tests__/session.test.js.map +1 -1
  66. package/dist/hooks/deep-interview-config-instruction.js +1 -1
  67. package/dist/hooks/deep-interview-config-instruction.js.map +1 -1
  68. package/dist/hooks/keyword-detector.d.ts +1 -0
  69. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  70. package/dist/hooks/keyword-detector.js +171 -21
  71. package/dist/hooks/keyword-detector.js.map +1 -1
  72. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  73. package/dist/hooks/keyword-registry.js +1 -0
  74. package/dist/hooks/keyword-registry.js.map +1 -1
  75. package/dist/hooks/session.d.ts +2 -0
  76. package/dist/hooks/session.d.ts.map +1 -1
  77. package/dist/hooks/session.js +13 -5
  78. package/dist/hooks/session.js.map +1 -1
  79. package/dist/hud/__tests__/authority.test.js +35 -0
  80. package/dist/hud/__tests__/authority.test.js.map +1 -1
  81. package/dist/hud/__tests__/index.test.js +168 -2
  82. package/dist/hud/__tests__/index.test.js.map +1 -1
  83. package/dist/hud/__tests__/reconcile.test.js +67 -13
  84. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  85. package/dist/hud/__tests__/state.test.js +80 -0
  86. package/dist/hud/__tests__/state.test.js.map +1 -1
  87. package/dist/hud/__tests__/tmux.test.js +134 -1
  88. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  89. package/dist/hud/authority.d.ts.map +1 -1
  90. package/dist/hud/authority.js +13 -2
  91. package/dist/hud/authority.js.map +1 -1
  92. package/dist/hud/index.d.ts +17 -0
  93. package/dist/hud/index.d.ts.map +1 -1
  94. package/dist/hud/index.js +64 -10
  95. package/dist/hud/index.js.map +1 -1
  96. package/dist/hud/reconcile.js +1 -1
  97. package/dist/hud/reconcile.js.map +1 -1
  98. package/dist/hud/state.d.ts.map +1 -1
  99. package/dist/hud/state.js +16 -1
  100. package/dist/hud/state.js.map +1 -1
  101. package/dist/hud/tmux.d.ts +2 -0
  102. package/dist/hud/tmux.d.ts.map +1 -1
  103. package/dist/hud/tmux.js +39 -2
  104. package/dist/hud/tmux.js.map +1 -1
  105. package/dist/mcp/__tests__/hermes-bridge.test.js +203 -7
  106. package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
  107. package/dist/mcp/__tests__/state-server.test.js +13 -1
  108. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  109. package/dist/mcp/hermes-bridge.d.ts +12 -2
  110. package/dist/mcp/hermes-bridge.d.ts.map +1 -1
  111. package/dist/mcp/hermes-bridge.js +83 -9
  112. package/dist/mcp/hermes-bridge.js.map +1 -1
  113. package/dist/modes/__tests__/base-autoresearch-contract.test.js +7 -1
  114. package/dist/modes/__tests__/base-autoresearch-contract.test.js.map +1 -1
  115. package/dist/pipeline/__tests__/stages.test.js +130 -0
  116. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  117. package/dist/pipeline/orchestrator.js +1 -1
  118. package/dist/pipeline/orchestrator.js.map +1 -1
  119. package/dist/pipeline/stages/ralplan.d.ts +1 -0
  120. package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
  121. package/dist/pipeline/stages/ralplan.js +14 -5
  122. package/dist/pipeline/stages/ralplan.js.map +1 -1
  123. package/dist/question/__tests__/deep-interview.test.js +160 -2
  124. package/dist/question/__tests__/deep-interview.test.js.map +1 -1
  125. package/dist/question/__tests__/policy.test.js +63 -3
  126. package/dist/question/__tests__/policy.test.js.map +1 -1
  127. package/dist/question/__tests__/renderer.test.js +191 -2
  128. package/dist/question/__tests__/renderer.test.js.map +1 -1
  129. package/dist/question/__tests__/state.test.js +94 -3
  130. package/dist/question/__tests__/state.test.js.map +1 -1
  131. package/dist/question/__tests__/ui.test.js +4 -0
  132. package/dist/question/__tests__/ui.test.js.map +1 -1
  133. package/dist/question/autopilot-wait.d.ts +12 -2
  134. package/dist/question/autopilot-wait.d.ts.map +1 -1
  135. package/dist/question/autopilot-wait.js +158 -47
  136. package/dist/question/autopilot-wait.js.map +1 -1
  137. package/dist/question/deep-interview.d.ts.map +1 -1
  138. package/dist/question/deep-interview.js +22 -6
  139. package/dist/question/deep-interview.js.map +1 -1
  140. package/dist/question/policy.d.ts.map +1 -1
  141. package/dist/question/policy.js +2 -5
  142. package/dist/question/policy.js.map +1 -1
  143. package/dist/question/renderer.d.ts +12 -0
  144. package/dist/question/renderer.d.ts.map +1 -1
  145. package/dist/question/renderer.js +87 -3
  146. package/dist/question/renderer.js.map +1 -1
  147. package/dist/question/state.d.ts +8 -1
  148. package/dist/question/state.d.ts.map +1 -1
  149. package/dist/question/state.js +54 -14
  150. package/dist/question/state.js.map +1 -1
  151. package/dist/question/types.d.ts +1 -1
  152. package/dist/question/types.d.ts.map +1 -1
  153. package/dist/question/ui.d.ts +1 -0
  154. package/dist/question/ui.d.ts.map +1 -1
  155. package/dist/question/ui.js +1 -0
  156. package/dist/question/ui.js.map +1 -1
  157. package/dist/ralplan/__tests__/runtime.test.js +191 -0
  158. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  159. package/dist/ralplan/consensus-gate.d.ts +9 -1
  160. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  161. package/dist/ralplan/consensus-gate.js +84 -2
  162. package/dist/ralplan/consensus-gate.js.map +1 -1
  163. package/dist/ralplan/runtime.d.ts +9 -0
  164. package/dist/ralplan/runtime.d.ts.map +1 -1
  165. package/dist/ralplan/runtime.js +32 -11
  166. package/dist/ralplan/runtime.js.map +1 -1
  167. package/dist/scripts/__tests__/codex-native-hook.test.js +1487 -34
  168. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  169. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  170. package/dist/scripts/codex-native-hook.js +356 -38
  171. package/dist/scripts/codex-native-hook.js.map +1 -1
  172. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  173. package/dist/scripts/codex-native-pre-post.js +79 -1
  174. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  175. package/dist/scripts/hook-payload-guard.d.ts +9 -0
  176. package/dist/scripts/hook-payload-guard.d.ts.map +1 -0
  177. package/dist/scripts/hook-payload-guard.js +111 -0
  178. package/dist/scripts/hook-payload-guard.js.map +1 -0
  179. package/dist/scripts/notify-fallback-watcher.js +8 -1
  180. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  181. package/dist/scripts/notify-hook/__tests__/payload-guard.test.d.ts +2 -0
  182. package/dist/scripts/notify-hook/__tests__/payload-guard.test.d.ts.map +1 -0
  183. package/dist/scripts/notify-hook/__tests__/payload-guard.test.js +39 -0
  184. package/dist/scripts/notify-hook/__tests__/payload-guard.test.js.map +1 -0
  185. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  186. package/dist/scripts/notify-hook/team-worker-stop.js +234 -86
  187. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  188. package/dist/scripts/notify-hook.js +11 -2
  189. package/dist/scripts/notify-hook.js.map +1 -1
  190. package/dist/state/__tests__/operations.test.js +1012 -1
  191. package/dist/state/__tests__/operations.test.js.map +1 -1
  192. package/dist/state/__tests__/skill-active.test.js +59 -1
  193. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  194. package/dist/state/__tests__/workflow-transition.test.js +73 -7
  195. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  196. package/dist/state/operations.d.ts.map +1 -1
  197. package/dist/state/operations.js +102 -0
  198. package/dist/state/operations.js.map +1 -1
  199. package/dist/state/skill-active.d.ts.map +1 -1
  200. package/dist/state/skill-active.js +33 -3
  201. package/dist/state/skill-active.js.map +1 -1
  202. package/dist/state/workflow-transition-reconcile.d.ts +6 -0
  203. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  204. package/dist/state/workflow-transition-reconcile.js +28 -1
  205. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  206. package/dist/state/workflow-transition.d.ts.map +1 -1
  207. package/dist/state/workflow-transition.js +10 -3
  208. package/dist/state/workflow-transition.js.map +1 -1
  209. package/dist/subagents/__tests__/tracker.test.js +139 -0
  210. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  211. package/dist/subagents/tracker.d.ts +3 -0
  212. package/dist/subagents/tracker.d.ts.map +1 -1
  213. package/dist/subagents/tracker.js +41 -4
  214. package/dist/subagents/tracker.js.map +1 -1
  215. package/dist/team/__tests__/coordination-protocol.test.d.ts +2 -0
  216. package/dist/team/__tests__/coordination-protocol.test.d.ts.map +1 -0
  217. package/dist/team/__tests__/coordination-protocol.test.js +173 -0
  218. package/dist/team/__tests__/coordination-protocol.test.js.map +1 -0
  219. package/dist/team/__tests__/runtime.test.js +51 -2
  220. package/dist/team/__tests__/runtime.test.js.map +1 -1
  221. package/dist/team/__tests__/state.test.js +83 -0
  222. package/dist/team/__tests__/state.test.js.map +1 -1
  223. package/dist/team/__tests__/tmux-session.test.js +45 -0
  224. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  225. package/dist/team/__tests__/worker-bootstrap.test.js +84 -0
  226. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  227. package/dist/team/coordination-protocol.d.ts +14 -0
  228. package/dist/team/coordination-protocol.d.ts.map +1 -0
  229. package/dist/team/coordination-protocol.js +244 -0
  230. package/dist/team/coordination-protocol.js.map +1 -0
  231. package/dist/team/runtime.d.ts +1 -0
  232. package/dist/team/runtime.d.ts.map +1 -1
  233. package/dist/team/runtime.js +19 -3
  234. package/dist/team/runtime.js.map +1 -1
  235. package/dist/team/state/tasks.d.ts.map +1 -1
  236. package/dist/team/state/tasks.js +24 -0
  237. package/dist/team/state/tasks.js.map +1 -1
  238. package/dist/team/state/types.d.ts +21 -1
  239. package/dist/team/state/types.d.ts.map +1 -1
  240. package/dist/team/state/types.js.map +1 -1
  241. package/dist/team/state.d.ts +17 -1
  242. package/dist/team/state.d.ts.map +1 -1
  243. package/dist/team/state.js +12 -5
  244. package/dist/team/state.js.map +1 -1
  245. package/dist/team/team-ops.d.ts +1 -1
  246. package/dist/team/team-ops.d.ts.map +1 -1
  247. package/dist/team/team-ops.js.map +1 -1
  248. package/dist/team/tmux-session.d.ts.map +1 -1
  249. package/dist/team/tmux-session.js +19 -1
  250. package/dist/team/tmux-session.js.map +1 -1
  251. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  252. package/dist/team/worker-bootstrap.js +63 -0
  253. package/dist/team/worker-bootstrap.js.map +1 -1
  254. package/dist/utils/__tests__/agents-model-table.test.js +4 -2
  255. package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
  256. package/dist/utils/agents-model-table.d.ts.map +1 -1
  257. package/dist/utils/agents-model-table.js +3 -0
  258. package/dist/utils/agents-model-table.js.map +1 -1
  259. package/package.json +1 -1
  260. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  261. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +10 -5
  262. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +9 -4
  263. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +12 -0
  264. package/plugins/oh-my-codex/skills/team/SKILL.md +16 -0
  265. package/plugins/oh-my-codex/skills/worker/SKILL.md +14 -0
  266. package/skills/autopilot/SKILL.md +10 -5
  267. package/skills/deep-interview/SKILL.md +9 -4
  268. package/skills/ralplan/SKILL.md +12 -0
  269. package/skills/team/SKILL.md +16 -0
  270. package/skills/worker/SKILL.md +14 -0
  271. package/src/scripts/__tests__/codex-native-hook.test.ts +2202 -523
  272. package/src/scripts/codex-native-hook.ts +444 -36
  273. package/src/scripts/codex-native-pre-post.ts +80 -0
  274. package/src/scripts/hook-payload-guard.ts +113 -0
  275. package/src/scripts/notify-fallback-watcher.ts +8 -1
  276. package/src/scripts/notify-hook/__tests__/payload-guard.test.ts +41 -0
  277. package/src/scripts/notify-hook/team-worker-stop.ts +193 -52
  278. package/src/scripts/notify-hook.ts +14 -2
@@ -22,6 +22,8 @@ import { getLegacyWikiDir, serializePage, writePage } from "../../wiki/storage.j
22
22
  import { WIKI_SCHEMA_VERSION } from "../../wiki/types.js";
23
23
  import { createUltragoalPlan, readUltragoalPlan } from "../../ultragoal/artifacts.js";
24
24
  import { getBaseStateDir } from "../../state/paths.js";
25
+ import { maybeNudgeLeaderForAllowedWorkerStop } from "../notify-hook/team-worker-stop.js";
26
+ import { MAX_NATIVE_STDIN_JSON_BYTES } from "../hook-payload-guard.js";
25
27
  function nativeHookScriptPath() {
26
28
  return join(process.cwd(), "dist", "scripts", "codex-native-hook.js");
27
29
  }
@@ -104,6 +106,12 @@ async function withLoreGuardConfig(value, prefix, run) {
104
106
  }
105
107
  }
106
108
  function buildWorkerStopFakeTmux(tmuxLogPath, options = {}) {
109
+ const rawCaptureText = options.captureText ?? (options.busyLeader ? "• Working… (esc to interrupt)" : "› ready");
110
+ const captureText = `'${rawCaptureText.replace(/'/g, "'\"'\"'")}'`;
111
+ const currentCommand = `'${(options.currentCommand ?? "codex").replace(/'/g, "'\"'\"'")}'`;
112
+ const sendDelaySeconds = Math.max(0, options.sendDelayMs ?? 0) / 1000;
113
+ const removePathOnSend = options.removePathOnSend ? `'${options.removePathOnSend.replace(/'/g, "'\"'\"'")}'` : "";
114
+ const removePathOnCapture = options.removePathOnCapture ? `'${options.removePathOnCapture.replace(/'/g, "'\"'\"'")}'` : "";
107
115
  return `#!/usr/bin/env bash
108
116
  set -eu
109
117
  echo "$@" >> "${tmuxLogPath}"
@@ -124,17 +132,20 @@ if [[ "$cmd" == "display-message" ]]; then
124
132
  "#{pane_id}") echo "%42" ;;
125
133
  "#{pane_current_path}") pwd ;;
126
134
  "#{pane_start_command}") echo "codex" ;;
127
- "#{pane_current_command}") echo "codex" ;;
135
+ "#{pane_current_command}") printf '%s\\n' ${currentCommand} ;;
128
136
  "#S") echo "omx-team-worker-stop" ;;
129
137
  *) ;;
130
138
  esac
131
139
  exit 0
132
140
  fi
133
141
  if [[ "$cmd" == "capture-pane" ]]; then
134
- ${options.busyLeader ? 'echo "• Working… (esc to interrupt)"' : 'echo "› ready"'}
142
+ ${removePathOnCapture ? `rm -rf ${removePathOnCapture}` : ""}
143
+ printf '%s\\n' ${captureText}
135
144
  exit 0
136
145
  fi
137
146
  if [[ "$cmd" == "send-keys" ]]; then
147
+ ${sendDelaySeconds > 0 ? `sleep ${sendDelaySeconds}` : ""}
148
+ ${removePathOnSend ? `rm -rf ${removePathOnSend}` : ""}
138
149
  ${options.failSend ? "exit 1" : "exit 0"}
139
150
  fi
140
151
  exit 0
@@ -281,13 +292,78 @@ describe("codex native hook dispatch", () => {
281
292
  it("does not treat a different module url as the main module", () => {
282
293
  assert.equal(isCodexNativeHookMainModule(pathToFileURL("/tmp/omx native/other-script.js").href, "/tmp/omx native/codex-native-hook.js"), false);
283
294
  });
284
- it("emits deterministic JSON stdout when CLI stdin is malformed", () => {
295
+ it("emits schema-safe JSON stdout when CLI stdin is malformed", () => {
285
296
  const stdout = runNativeHookCli("{");
286
297
  const output = parseSingleJsonStdout(stdout);
298
+ assert.equal(output.continue, false);
299
+ assert.equal(output.stopReason, "native_hook_stdin_parse_error");
300
+ assert.equal(output.hookSpecificOutput, undefined);
301
+ assert.match(String(output.systemMessage ?? ""), /stdin JSON parsing failed inside codex-native-hook:/);
302
+ });
303
+ it("redacts unterminated prompt-like malformed stdin fields", async () => {
304
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-malformed-unterminated-"));
305
+ try {
306
+ const privatePrompt = "PRIVATE_UNTERMINATED_PROMPT";
307
+ const malformed = `{hook_event_name:"PostToolUse", prompt:"${privatePrompt}`;
308
+ const result = spawnSync(process.execPath, [nativeHookScriptPath()], {
309
+ cwd,
310
+ input: malformed,
311
+ encoding: "utf-8",
312
+ stdio: ["pipe", "pipe", "pipe"],
313
+ });
314
+ assert.equal(result.status, 0, result.stderr || result.stdout);
315
+ assert.equal(result.stderr, "");
316
+ const output = parseSingleJsonStdout(result.stdout);
317
+ assert.equal(output.stopReason, "native_hook_stdin_parse_error");
318
+ const log = await readFile(join(cwd, ".omx", "logs", `native-hook-${new Date().toISOString().split("T")[0]}.jsonl`), "utf-8");
319
+ const entry = JSON.parse(log.trim());
320
+ const prefix = String(entry.raw_input_prefix ?? "");
321
+ assert.doesNotMatch(prefix, new RegExp(privatePrompt));
322
+ assert.match(prefix, /prompt:"\[REDACTED\]"/);
323
+ }
324
+ finally {
325
+ await rm(cwd, { recursive: true, force: true });
326
+ }
327
+ });
328
+ it("logs a bounded redacted raw stdin prefix when CLI stdin is malformed", async () => {
329
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-malformed-log-prefix-"));
330
+ try {
331
+ const secret = "sk-test-secret123456";
332
+ const promptText = "summarize private launch notes";
333
+ const malformed = `{hook_event_name:"PostToolUse", access_token:"${secret}", prompt:"${promptText}", text:"${promptText}", bad:"${"x".repeat(400)}"}${String.fromCharCode(10, 0, 7)}`;
334
+ const result = spawnSync(process.execPath, [nativeHookScriptPath()], {
335
+ cwd,
336
+ input: malformed,
337
+ encoding: "utf-8",
338
+ stdio: ["pipe", "pipe", "pipe"],
339
+ });
340
+ assert.equal(result.status, 0, result.stderr || result.stdout);
341
+ assert.equal(result.stderr, "");
342
+ const output = parseSingleJsonStdout(result.stdout);
343
+ assert.equal(output.stopReason, "native_hook_stdin_parse_error");
344
+ const log = await readFile(join(cwd, ".omx", "logs", `native-hook-${new Date().toISOString().split("T")[0]}.jsonl`), "utf-8");
345
+ const entry = JSON.parse(log.trim());
346
+ const prefix = String(entry.raw_input_prefix ?? "");
347
+ assert.equal(entry.type, "native_hook_stdin_parse_error");
348
+ assert.equal(entry.raw_input_length, Buffer.byteLength(malformed, "utf-8"));
349
+ assert.ok(prefix.length <= 240, `prefix should be bounded, got ${prefix.length}`);
350
+ assert.doesNotMatch(prefix, /[\u0000-\u001f\u007f-\u009f]/);
351
+ assert.doesNotMatch(prefix, new RegExp(secret));
352
+ assert.doesNotMatch(prefix, new RegExp(promptText));
353
+ assert.match(prefix, /\[REDACTED\]/);
354
+ }
355
+ finally {
356
+ await rm(cwd, { recursive: true, force: true });
357
+ }
358
+ });
359
+ it("emits Stop-schema-safe block JSON when malformed stdin still identifies Stop", () => {
360
+ const stdout = runNativeHookCli('{hook_event_name:"Stop",');
361
+ const output = parseSingleJsonStdout(stdout);
287
362
  assert.equal(output.decision, "block");
288
363
  assert.equal(output.reason, "OMX native hook received malformed JSON input. Preserve runtime state, inspect the emitting hook payload yourself, and retry with valid JSON.");
289
- assert.equal(output.hookSpecificOutput?.hookEventName, "Unknown");
290
- assert.match(String(output.hookSpecificOutput?.additionalContext ?? ""), /stdin JSON parsing failed inside codex-native-hook:/);
364
+ assert.equal(output.stopReason, "native_hook_stdin_parse_error");
365
+ assert.equal(output.hookSpecificOutput, undefined);
366
+ assert.match(String(output.systemMessage ?? ""), /stdin JSON parsing failed inside codex-native-hook:/);
291
367
  });
292
368
  it("emits parseable no-op JSON stdout for inactive Stop CLI runs", async () => {
293
369
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-noop-json-"));
@@ -301,6 +377,112 @@ describe("codex native hook dispatch", () => {
301
377
  }, { cwd });
302
378
  const output = parseSingleJsonStdout(stdout);
303
379
  assert.deepEqual(output, {});
380
+ assert.equal(existsSync(join(cwd, ".omx", "state")), false);
381
+ }
382
+ finally {
383
+ await rm(cwd, { recursive: true, force: true });
384
+ }
385
+ });
386
+ it("returns empty JSON for oversized Stop stdin without parsing or creating inactive state", async () => {
387
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-"));
388
+ try {
389
+ const oversizedStop = JSON.stringify({
390
+ hook_event_name: "Stop",
391
+ cwd,
392
+ session_id: "sess-cli-stop-oversized",
393
+ transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
394
+ });
395
+ const stdout = runNativeHookCli(oversizedStop, { cwd });
396
+ assert.deepEqual(parseSingleJsonStdout(stdout), {});
397
+ assert.equal(existsSync(join(cwd, ".omx", "state")), false);
398
+ }
399
+ finally {
400
+ await rm(cwd, { recursive: true, force: true });
401
+ }
402
+ });
403
+ it("blocks oversized Stop stdin when current session autopilot is active", async () => {
404
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-active-"));
405
+ try {
406
+ await writeActiveAutopilotSession(cwd, "sess-cli-stop-oversized-active");
407
+ const oversizedStop = JSON.stringify({
408
+ hook_event_name: "Stop",
409
+ cwd,
410
+ session_id: "native-session-hidden-by-oversized-payload",
411
+ transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
412
+ });
413
+ const output = parseSingleJsonStdout(runNativeHookCli(oversizedStop, { cwd }));
414
+ assert.equal(output.decision, "block");
415
+ assert.equal(output.stopReason, "native_stop_stdin_oversized_active_workflow");
416
+ assert.match(String(output.systemMessage ?? ""), /active current-session workflow state/);
417
+ assert.equal(existsSync(join(cwd, ".omx", "logs")), false);
418
+ }
419
+ finally {
420
+ await rm(cwd, { recursive: true, force: true });
421
+ }
422
+ });
423
+ it("does not block oversized Stop stdin for unrelated root autopilot state", async () => {
424
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-stale-root-"));
425
+ try {
426
+ await writeJson(join(cwd, ".omx", "state", "session.json"), {
427
+ session_id: "sess-current-without-active-autopilot",
428
+ cwd,
429
+ });
430
+ await writeJson(join(cwd, ".omx", "state", "autopilot-state.json"), {
431
+ active: true,
432
+ current_phase: "execution",
433
+ });
434
+ const oversizedStop = JSON.stringify({
435
+ hook_event_name: "Stop",
436
+ cwd,
437
+ transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
438
+ });
439
+ assert.deepEqual(parseSingleJsonStdout(runNativeHookCli(oversizedStop, { cwd })), {});
440
+ assert.equal(existsSync(join(cwd, ".omx", "logs")), false);
441
+ }
442
+ finally {
443
+ await rm(cwd, { recursive: true, force: true });
444
+ }
445
+ });
446
+ it("does not block oversized Stop stdin when terminal run-state shadows stale autopilot state", async () => {
447
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-terminal-run-"));
448
+ try {
449
+ const sessionId = "sess-cli-stop-oversized-terminal-run";
450
+ await writeActiveAutopilotSession(cwd, sessionId);
451
+ await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "run-state.json"), {
452
+ version: 1,
453
+ active: false,
454
+ mode: "autopilot",
455
+ outcome: "finish",
456
+ lifecycle_outcome: "finished",
457
+ current_phase: "complete",
458
+ completed_at: "2026-05-20T11:00:00.000Z",
459
+ updated_at: "2026-05-20T11:00:00.000Z",
460
+ });
461
+ const oversizedStop = JSON.stringify({
462
+ hook_event_name: "Stop",
463
+ cwd,
464
+ transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
465
+ });
466
+ assert.deepEqual(parseSingleJsonStdout(runNativeHookCli(oversizedStop, { cwd })), {});
467
+ assert.equal(existsSync(join(cwd, ".omx", "logs")), false);
468
+ }
469
+ finally {
470
+ await rm(cwd, { recursive: true, force: true });
471
+ }
472
+ });
473
+ it("fails closed for oversized non-Stop stdin before parsing", async () => {
474
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-nonstop-oversized-"));
475
+ try {
476
+ const oversizedPrompt = JSON.stringify({
477
+ hook_event_name: "UserPromptSubmit",
478
+ cwd,
479
+ session_id: "sess-cli-prompt-oversized",
480
+ prompt: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
481
+ });
482
+ const output = parseSingleJsonStdout(runNativeHookCli(oversizedPrompt, { cwd }));
483
+ assert.equal(output.continue, false);
484
+ assert.equal(output.stopReason, "native_hook_stdin_oversized");
485
+ assert.match(String(output.systemMessage ?? ""), /rejected oversized stdin JSON before parsing/);
304
486
  }
305
487
  finally {
306
488
  await rm(cwd, { recursive: true, force: true });
@@ -915,6 +1097,50 @@ describe("codex native hook dispatch", () => {
915
1097
  await rm(cwd, { recursive: true, force: true });
916
1098
  }
917
1099
  });
1100
+ it("keeps a self-parented native role thread as subagent evidence", async () => {
1101
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-self-parented-subagent-"));
1102
+ try {
1103
+ const stateDir = join(cwd, ".omx", "state");
1104
+ const canonicalSessionId = "omx-autopilot-session";
1105
+ const nativeRoleThreadId = "codex-architect-thread";
1106
+ await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
1107
+ await writeSessionStart(cwd, canonicalSessionId, {
1108
+ nativeSessionId: nativeRoleThreadId,
1109
+ });
1110
+ const transcriptPath = join(cwd, "architect-subagent-rollout.jsonl");
1111
+ await writeFile(transcriptPath, `${JSON.stringify({
1112
+ type: "session_meta",
1113
+ payload: {
1114
+ id: nativeRoleThreadId,
1115
+ source: {
1116
+ subagent: {
1117
+ thread_spawn: {
1118
+ parent_thread_id: nativeRoleThreadId,
1119
+ depth: 1,
1120
+ agent_nickname: "Architect",
1121
+ agent_role: "architect",
1122
+ },
1123
+ },
1124
+ },
1125
+ agent_nickname: "Architect",
1126
+ agent_role: "architect",
1127
+ },
1128
+ })}\n`);
1129
+ await dispatchCodexNativeHook({
1130
+ hook_event_name: "SessionStart",
1131
+ cwd,
1132
+ session_id: nativeRoleThreadId,
1133
+ transcript_path: transcriptPath,
1134
+ }, { cwd, sessionOwnerPid: process.pid });
1135
+ const tracking = JSON.parse(await readFile(join(stateDir, "subagent-tracking.json"), "utf-8"));
1136
+ assert.equal(tracking.sessions?.[canonicalSessionId]?.leader_thread_id, undefined);
1137
+ assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[nativeRoleThreadId]?.kind, "subagent");
1138
+ assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[nativeRoleThreadId]?.mode, "architect");
1139
+ }
1140
+ finally {
1141
+ await rm(cwd, { recursive: true, force: true });
1142
+ }
1143
+ });
918
1144
  it("does not attach a subagent SessionStart to an unrelated canonical leader", async () => {
919
1145
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-start-mismatch-"));
920
1146
  try {
@@ -1018,6 +1244,89 @@ describe("codex native hook dispatch", () => {
1018
1244
  await rm(cwd, { recursive: true, force: true });
1019
1245
  }
1020
1246
  });
1247
+ it("prefers the OMX owner session id when a native new session revives HUD", async () => {
1248
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-owner-session-revive-"));
1249
+ try {
1250
+ const stateDir = join(cwd, ".omx", "state");
1251
+ const ownerSessionId = "omx-launch-owner-hud";
1252
+ const oldNativeSessionId = "codex-native-hud-old";
1253
+ const nativeSessionId = "codex-native-hud-new";
1254
+ await mkdir(stateDir, { recursive: true });
1255
+ await writeSessionStart(cwd, ownerSessionId, {
1256
+ nativeSessionId: oldNativeSessionId,
1257
+ pid: process.pid,
1258
+ });
1259
+ await dispatchCodexNativeHook({
1260
+ hook_event_name: "SessionStart",
1261
+ cwd,
1262
+ session_id: nativeSessionId,
1263
+ }, {
1264
+ cwd,
1265
+ sessionOwnerPid: process.pid,
1266
+ });
1267
+ const sessionState = JSON.parse(await readFile(join(stateDir, "session.json"), "utf-8"));
1268
+ assert.equal(sessionState.session_id, nativeSessionId);
1269
+ assert.equal(sessionState.native_session_id, nativeSessionId);
1270
+ assert.equal(sessionState.previous_native_session_id, oldNativeSessionId);
1271
+ assert.equal(sessionState.owner_omx_session_id, ownerSessionId);
1272
+ let reconcileCall = null;
1273
+ const promptResult = await dispatchCodexNativeHook({
1274
+ hook_event_name: "UserPromptSubmit",
1275
+ cwd,
1276
+ session_id: nativeSessionId,
1277
+ thread_id: "thread-hud-owner",
1278
+ turn_id: "turn-hud-owner",
1279
+ prompt: "$ralplan fix native new hud owner handoff",
1280
+ }, {
1281
+ cwd,
1282
+ reconcileHudForPromptSubmitFn: async (hookCwd, deps = {}) => {
1283
+ reconcileCall = { cwd: hookCwd, sessionId: deps.sessionId };
1284
+ return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
1285
+ },
1286
+ });
1287
+ assert.equal(promptResult.omxEventName, "keyword-detector");
1288
+ assert.deepEqual(reconcileCall, { cwd, sessionId: ownerSessionId });
1289
+ }
1290
+ finally {
1291
+ await rm(cwd, { recursive: true, force: true });
1292
+ }
1293
+ });
1294
+ it("falls back to the canonical session id for malformed HUD owner ids", async () => {
1295
+ for (const [index, invalidOwnerSessionId] of ["codex-native-hud-owner", "omx-../../stale"].entries()) {
1296
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-invalid-owner-revive-"));
1297
+ try {
1298
+ const stateDir = join(cwd, ".omx", "state");
1299
+ const canonicalSessionId = "omx-launch-hud-safe";
1300
+ const nativeSessionId = "codex-native-hud-safe";
1301
+ await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
1302
+ await writeSessionStart(cwd, canonicalSessionId);
1303
+ const sessionStatePath = join(stateDir, "session.json");
1304
+ const sessionState = JSON.parse(await readFile(sessionStatePath, "utf-8"));
1305
+ sessionState.owner_omx_session_id = invalidOwnerSessionId;
1306
+ await writeJson(sessionStatePath, sessionState);
1307
+ let reconcileCall = null;
1308
+ const promptResult = await dispatchCodexNativeHook({
1309
+ hook_event_name: "UserPromptSubmit",
1310
+ cwd,
1311
+ session_id: nativeSessionId,
1312
+ thread_id: `thread-hud-invalid-owner-${index}`,
1313
+ turn_id: "turn-hud-invalid-owner",
1314
+ prompt: "$ralplan fix malformed hud owner handoff",
1315
+ }, {
1316
+ cwd,
1317
+ reconcileHudForPromptSubmitFn: async (hookCwd, deps = {}) => {
1318
+ reconcileCall = { cwd: hookCwd, sessionId: deps.sessionId };
1319
+ return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
1320
+ },
1321
+ });
1322
+ assert.equal(promptResult.omxEventName, "keyword-detector");
1323
+ assert.deepEqual(reconcileCall, { cwd, sessionId: canonicalSessionId });
1324
+ }
1325
+ finally {
1326
+ await rm(cwd, { recursive: true, force: true });
1327
+ }
1328
+ }
1329
+ });
1021
1330
  it("passes the canonical OMX session id when UserPromptSubmit revives HUD", async () => {
1022
1331
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-session-revive-"));
1023
1332
  try {
@@ -2208,46 +2517,340 @@ standardMaxRounds = 15
2208
2517
  await rm(cwd, { recursive: true, force: true });
2209
2518
  }
2210
2519
  });
2211
- it("records plugin-prefixed keyword activation from UserPromptSubmit payloads", async () => {
2212
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-plugin-prefixed-"));
2520
+ it("does not treat a corrupt leader kind=subagent tracker entry as native subagent prompt scope", async () => {
2521
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-corrupt-leader-subagent-"));
2213
2522
  try {
2214
- await mkdir(join(cwd, ".omx", "state"), { recursive: true });
2523
+ const stateDir = join(cwd, ".omx", "state");
2524
+ const canonicalSessionId = "sess-corrupt-leader";
2525
+ const leaderNativeSessionId = "native-corrupt-leader";
2526
+ const nowIso = new Date().toISOString();
2527
+ await writeJson(join(stateDir, "session.json"), {
2528
+ session_id: canonicalSessionId,
2529
+ native_session_id: leaderNativeSessionId,
2530
+ });
2531
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
2532
+ schemaVersion: 1,
2533
+ sessions: {
2534
+ [canonicalSessionId]: {
2535
+ session_id: canonicalSessionId,
2536
+ leader_thread_id: leaderNativeSessionId,
2537
+ updated_at: nowIso,
2538
+ threads: {
2539
+ [leaderNativeSessionId]: {
2540
+ thread_id: leaderNativeSessionId,
2541
+ kind: "subagent",
2542
+ first_seen_at: nowIso,
2543
+ last_seen_at: nowIso,
2544
+ turn_count: 2,
2545
+ },
2546
+ },
2547
+ },
2548
+ },
2549
+ });
2215
2550
  const result = await dispatchCodexNativeHook({
2216
2551
  hook_event_name: "UserPromptSubmit",
2217
2552
  cwd,
2218
- session_id: "sess-plugin-1",
2219
- thread_id: "thread-plugin-1",
2220
- turn_id: "turn-plugin-1",
2221
- prompt: "$oh-my-codex:ralplan implement issue #1307",
2553
+ session_id: leaderNativeSessionId,
2554
+ thread_id: leaderNativeSessionId,
2555
+ turn_id: "turn-corrupt-leader",
2556
+ prompt: "$autopilot continue this review blocker fix",
2222
2557
  }, { cwd });
2223
2558
  assert.equal(result.omxEventName, "keyword-detector");
2224
- assert.equal(result.skillState?.skill, "ralplan");
2225
- const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
2226
- assert.match(message, /\$oh-my-codex:ralplan" -> ralplan/);
2227
- assert.match(message, /use CLI-first state updates via `omx state write\/read\/clear --input '<json>' --json`/);
2228
- assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-plugin-1", "ralplan-state.json")), true);
2559
+ assert.equal(result.skillState?.skill, "autopilot");
2560
+ assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")), true);
2229
2561
  }
2230
2562
  finally {
2231
2563
  await rm(cwd, { recursive: true, force: true });
2232
2564
  }
2233
2565
  });
2234
- it("injects autopilot ralplan consensus gate guidance on prompt activation", async () => {
2235
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-gate-"));
2566
+ it("lets the current canonical leader boundary beat stale global subagent tracking with a distinct prompt thread id", async () => {
2567
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-current-leader-stale-global-"));
2236
2568
  try {
2237
- await mkdir(join(cwd, ".omx", "state"), { recursive: true });
2569
+ const stateDir = join(cwd, ".omx", "state");
2570
+ const canonicalSessionId = "sess-current-leader";
2571
+ const leaderNativeSessionId = "native-current-leader";
2572
+ const staleSessionId = "sess-stale-subagent";
2573
+ const staleLeaderNativeSessionId = "native-stale-leader";
2574
+ const nowIso = new Date().toISOString();
2575
+ await writeJson(join(stateDir, "session.json"), {
2576
+ session_id: canonicalSessionId,
2577
+ native_session_id: leaderNativeSessionId,
2578
+ });
2579
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
2580
+ schemaVersion: 1,
2581
+ sessions: {
2582
+ [canonicalSessionId]: {
2583
+ session_id: canonicalSessionId,
2584
+ leader_thread_id: leaderNativeSessionId,
2585
+ updated_at: nowIso,
2586
+ threads: {
2587
+ [leaderNativeSessionId]: {
2588
+ thread_id: leaderNativeSessionId,
2589
+ kind: "leader",
2590
+ first_seen_at: nowIso,
2591
+ last_seen_at: nowIso,
2592
+ turn_count: 1,
2593
+ },
2594
+ },
2595
+ },
2596
+ [staleSessionId]: {
2597
+ session_id: staleSessionId,
2598
+ leader_thread_id: staleLeaderNativeSessionId,
2599
+ updated_at: nowIso,
2600
+ threads: {
2601
+ [staleLeaderNativeSessionId]: {
2602
+ thread_id: staleLeaderNativeSessionId,
2603
+ kind: "leader",
2604
+ first_seen_at: nowIso,
2605
+ last_seen_at: nowIso,
2606
+ turn_count: 1,
2607
+ },
2608
+ [leaderNativeSessionId]: {
2609
+ thread_id: leaderNativeSessionId,
2610
+ kind: "subagent",
2611
+ first_seen_at: nowIso,
2612
+ last_seen_at: nowIso,
2613
+ turn_count: 1,
2614
+ mode: "architect",
2615
+ },
2616
+ },
2617
+ },
2618
+ },
2619
+ });
2238
2620
  const result = await dispatchCodexNativeHook({
2239
2621
  hook_event_name: "UserPromptSubmit",
2240
2622
  cwd,
2241
- session_id: "sess-autopilot-ralplan-gate",
2242
- thread_id: "thread-autopilot-ralplan-gate",
2243
- turn_id: "turn-autopilot-ralplan-gate",
2244
- prompt: "$autopilot implement issue #2430",
2623
+ session_id: leaderNativeSessionId,
2624
+ thread_id: "thread-current-turn-not-native-session",
2625
+ turn_id: "turn-current-leader",
2626
+ prompt: "$autopilot continue",
2245
2627
  }, { cwd });
2246
2628
  assert.equal(result.omxEventName, "keyword-detector");
2247
2629
  assert.equal(result.skillState?.skill, "autopilot");
2248
- const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
2249
- assert.match(message, /Autopilot protocol:/);
2250
- assert.match(message, /deep-interview -> ralplan -> ultragoal -> code-review -> ultraqa/);
2630
+ assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")), true);
2631
+ assert.equal(existsSync(join(stateDir, "sessions", staleSessionId, "autopilot-state.json")), false);
2632
+ }
2633
+ finally {
2634
+ await rm(cwd, { recursive: true, force: true });
2635
+ }
2636
+ });
2637
+ it("lets the current session native leader beat stale global subagent tracking without a canonical summary and with a distinct prompt thread id", async () => {
2638
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-current-native-leader-stale-global-"));
2639
+ try {
2640
+ const stateDir = join(cwd, ".omx", "state");
2641
+ const canonicalSessionId = "sess-current-native-leader";
2642
+ const leaderNativeSessionId = "native-current-leader-no-summary";
2643
+ const staleSessionId = "sess-stale-native-subagent";
2644
+ const staleLeaderNativeSessionId = "native-stale-parent";
2645
+ const nowIso = new Date().toISOString();
2646
+ await writeJson(join(stateDir, "session.json"), {
2647
+ session_id: canonicalSessionId,
2648
+ native_session_id: leaderNativeSessionId,
2649
+ });
2650
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
2651
+ schemaVersion: 1,
2652
+ sessions: {
2653
+ [staleSessionId]: {
2654
+ session_id: staleSessionId,
2655
+ leader_thread_id: staleLeaderNativeSessionId,
2656
+ updated_at: nowIso,
2657
+ threads: {
2658
+ [staleLeaderNativeSessionId]: {
2659
+ thread_id: staleLeaderNativeSessionId,
2660
+ kind: "leader",
2661
+ first_seen_at: nowIso,
2662
+ last_seen_at: nowIso,
2663
+ turn_count: 1,
2664
+ },
2665
+ [leaderNativeSessionId]: {
2666
+ thread_id: leaderNativeSessionId,
2667
+ kind: "subagent",
2668
+ first_seen_at: nowIso,
2669
+ last_seen_at: nowIso,
2670
+ turn_count: 1,
2671
+ mode: "critic",
2672
+ },
2673
+ },
2674
+ },
2675
+ },
2676
+ });
2677
+ const result = await dispatchCodexNativeHook({
2678
+ hook_event_name: "UserPromptSubmit",
2679
+ cwd,
2680
+ session_id: leaderNativeSessionId,
2681
+ thread_id: "thread-current-turn-not-native-session",
2682
+ turn_id: "turn-current-native-leader",
2683
+ prompt: "$autopilot continue",
2684
+ }, { cwd });
2685
+ assert.equal(result.omxEventName, "keyword-detector");
2686
+ assert.equal(result.skillState?.skill, "autopilot");
2687
+ assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")), true);
2688
+ assert.equal(existsSync(join(stateDir, "sessions", staleSessionId, "autopilot-state.json")), false);
2689
+ }
2690
+ finally {
2691
+ await rm(cwd, { recursive: true, force: true });
2692
+ }
2693
+ });
2694
+ it("lets the current session native leader beat a malformed canonical subagent entry", async () => {
2695
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-current-native-leader-malformed-canonical-"));
2696
+ try {
2697
+ const stateDir = join(cwd, ".omx", "state");
2698
+ const canonicalSessionId = "sess-current-native-leader-malformed";
2699
+ const leaderNativeSessionId = "native-current-leader-malformed";
2700
+ const nowIso = new Date().toISOString();
2701
+ await writeJson(join(stateDir, "session.json"), {
2702
+ session_id: canonicalSessionId,
2703
+ native_session_id: leaderNativeSessionId,
2704
+ });
2705
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
2706
+ schemaVersion: 1,
2707
+ sessions: {
2708
+ [canonicalSessionId]: {
2709
+ session_id: canonicalSessionId,
2710
+ updated_at: nowIso,
2711
+ threads: {
2712
+ [leaderNativeSessionId]: {
2713
+ thread_id: leaderNativeSessionId,
2714
+ kind: "subagent",
2715
+ first_seen_at: nowIso,
2716
+ last_seen_at: nowIso,
2717
+ turn_count: 1,
2718
+ mode: "architect",
2719
+ },
2720
+ },
2721
+ },
2722
+ },
2723
+ });
2724
+ const result = await dispatchCodexNativeHook({
2725
+ hook_event_name: "UserPromptSubmit",
2726
+ cwd,
2727
+ session_id: leaderNativeSessionId,
2728
+ thread_id: leaderNativeSessionId,
2729
+ turn_id: "turn-current-native-leader-malformed",
2730
+ prompt: "$autopilot continue",
2731
+ }, { cwd });
2732
+ assert.equal(result.omxEventName, "keyword-detector");
2733
+ assert.equal(result.skillState?.skill, "autopilot");
2734
+ assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")), true);
2735
+ }
2736
+ finally {
2737
+ await rm(cwd, { recursive: true, force: true });
2738
+ }
2739
+ });
2740
+ it("still treats mixed child and leader payload identities as native subagent scope", async () => {
2741
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-mixed-child-leader-identity-"));
2742
+ try {
2743
+ const stateDir = join(cwd, ".omx", "state");
2744
+ const canonicalSessionId = "sess-mixed-child-leader";
2745
+ const leaderNativeSessionId = "native-mixed-leader";
2746
+ const childNativeSessionId = "native-mixed-child";
2747
+ const nowIso = new Date().toISOString();
2748
+ await writeJson(join(stateDir, "session.json"), {
2749
+ session_id: canonicalSessionId,
2750
+ native_session_id: leaderNativeSessionId,
2751
+ });
2752
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
2753
+ schemaVersion: 1,
2754
+ sessions: {
2755
+ [canonicalSessionId]: {
2756
+ session_id: canonicalSessionId,
2757
+ leader_thread_id: leaderNativeSessionId,
2758
+ updated_at: nowIso,
2759
+ threads: {
2760
+ [leaderNativeSessionId]: {
2761
+ thread_id: leaderNativeSessionId,
2762
+ kind: "leader",
2763
+ first_seen_at: nowIso,
2764
+ last_seen_at: nowIso,
2765
+ turn_count: 1,
2766
+ },
2767
+ [childNativeSessionId]: {
2768
+ thread_id: childNativeSessionId,
2769
+ kind: "subagent",
2770
+ first_seen_at: nowIso,
2771
+ last_seen_at: nowIso,
2772
+ turn_count: 1,
2773
+ mode: "critic",
2774
+ },
2775
+ },
2776
+ },
2777
+ },
2778
+ });
2779
+ const result = await dispatchCodexNativeHook({
2780
+ hook_event_name: "UserPromptSubmit",
2781
+ cwd,
2782
+ session_id: childNativeSessionId,
2783
+ thread_id: leaderNativeSessionId,
2784
+ turn_id: "turn-mixed-child-leader",
2785
+ prompt: "$ralplan review this as delegated text",
2786
+ }, { cwd });
2787
+ assert.equal(result.omxEventName, "keyword-detector");
2788
+ assert.equal(result.skillState, null);
2789
+ assert.equal(result.outputJson, null);
2790
+ assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "ralplan-state.json")), false);
2791
+ assert.equal(existsSync(join(stateDir, "sessions", childNativeSessionId, "ralplan-state.json")), false);
2792
+ const reversedResult = await dispatchCodexNativeHook({
2793
+ hook_event_name: "UserPromptSubmit",
2794
+ cwd,
2795
+ session_id: leaderNativeSessionId,
2796
+ thread_id: childNativeSessionId,
2797
+ turn_id: "turn-mixed-leader-child",
2798
+ prompt: "$autopilot review this as delegated text",
2799
+ }, { cwd });
2800
+ assert.equal(reversedResult.omxEventName, "keyword-detector");
2801
+ assert.equal(reversedResult.skillState, null);
2802
+ assert.equal(reversedResult.outputJson, null);
2803
+ assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")), false);
2804
+ assert.equal(existsSync(join(stateDir, "sessions", childNativeSessionId, "autopilot-state.json")), false);
2805
+ }
2806
+ finally {
2807
+ await rm(cwd, { recursive: true, force: true });
2808
+ }
2809
+ });
2810
+ it("records plugin-prefixed keyword activation from UserPromptSubmit payloads", async () => {
2811
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-plugin-prefixed-"));
2812
+ try {
2813
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
2814
+ const result = await dispatchCodexNativeHook({
2815
+ hook_event_name: "UserPromptSubmit",
2816
+ cwd,
2817
+ session_id: "sess-plugin-1",
2818
+ thread_id: "thread-plugin-1",
2819
+ turn_id: "turn-plugin-1",
2820
+ prompt: "$oh-my-codex:ralplan implement issue #1307",
2821
+ }, { cwd });
2822
+ assert.equal(result.omxEventName, "keyword-detector");
2823
+ assert.equal(result.skillState?.skill, "ralplan");
2824
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
2825
+ assert.match(message, /\$oh-my-codex:ralplan" -> ralplan/);
2826
+ assert.match(message, /use CLI-first state updates via `omx state write\/read\/clear --input '<json>' --json`/);
2827
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-plugin-1", "ralplan-state.json")), true);
2828
+ }
2829
+ finally {
2830
+ await rm(cwd, { recursive: true, force: true });
2831
+ }
2832
+ });
2833
+ it("injects autopilot ralplan consensus gate guidance on prompt activation", async () => {
2834
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-gate-"));
2835
+ try {
2836
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
2837
+ const result = await dispatchCodexNativeHook({
2838
+ hook_event_name: "UserPromptSubmit",
2839
+ cwd,
2840
+ session_id: "sess-autopilot-ralplan-gate",
2841
+ thread_id: "thread-autopilot-ralplan-gate",
2842
+ turn_id: "turn-autopilot-ralplan-gate",
2843
+ prompt: "$autopilot implement issue #2430",
2844
+ }, { cwd });
2845
+ assert.equal(result.omxEventName, "keyword-detector");
2846
+ assert.equal(result.skillState?.skill, "autopilot");
2847
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
2848
+ assert.match(message, /Autopilot protocol:/);
2849
+ assert.match(message, /deep-interview -> ralplan -> ultragoal -> code-review -> ultraqa/);
2850
+ assert.match(message, /structured question chain, not a one-question gate/);
2851
+ assert.match(message, /re-score ambiguity against the active threshold/);
2852
+ assert.match(message, /max_rounds as a cap/);
2853
+ assert.match(message, /Do not advance from deep-interview to ralplan merely because the first question was answered/);
2251
2854
  assert.match(message, /Planner output has been reviewed sequentially by Architect and then Critic/);
2252
2855
  assert.match(message, /do not hand off to Ultragoal or implementation until .*ralplan_architect_review.*ralplan_critic_review/);
2253
2856
  }
@@ -2705,6 +3308,11 @@ ${JSON.stringify({
2705
3308
  assert.equal(result.skillState?.skill, "autopilot");
2706
3309
  const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
2707
3310
  assert.match(message, /"keep going" -> ralph/);
3311
+ assert.match(message, /Autopilot protocol:/);
3312
+ assert.match(message, /structured question chain, not a one-question gate/);
3313
+ assert.match(message, /re-score ambiguity against the active threshold/);
3314
+ assert.match(message, /max_rounds as a cap/);
3315
+ assert.match(message, /Do not advance from deep-interview to ralplan merely because the first question was answered/);
2708
3316
  assert.doesNotMatch(message, /denied workflow keyword/i);
2709
3317
  assert.doesNotMatch(message, /Unsupported workflow overlap: autopilot \+ ralph\./);
2710
3318
  assert.doesNotMatch(message, /Prompt-side `\$ralph` activation/);
@@ -2714,6 +3322,107 @@ ${JSON.stringify({
2714
3322
  await rm(cwd, { recursive: true, force: true });
2715
3323
  }
2716
3324
  });
3325
+ it("keeps omx question answers on the active autopilot skill so the interview chain guidance is injected", async () => {
3326
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-question-answer-continuation-"));
3327
+ try {
3328
+ const sessionId = "sess-autopilot-question-answer";
3329
+ const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
3330
+ await mkdir(sessionDir, { recursive: true });
3331
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
3332
+ version: 1,
3333
+ active: true,
3334
+ skill: "autopilot",
3335
+ keyword: "$autopilot",
3336
+ phase: "deep-interview",
3337
+ initialized_mode: "autopilot",
3338
+ initialized_state_path: `.omx/state/sessions/${sessionId}/autopilot-state.json`,
3339
+ session_id: sessionId,
3340
+ active_skills: [
3341
+ { skill: "autopilot", phase: "deep-interview", active: true, session_id: sessionId },
3342
+ ],
3343
+ });
3344
+ await writeJson(join(sessionDir, "autopilot-state.json"), {
3345
+ active: true,
3346
+ mode: "autopilot",
3347
+ current_phase: "deep-interview",
3348
+ started_at: "2026-04-19T00:00:00.000Z",
3349
+ updated_at: "2026-04-19T00:10:00.000Z",
3350
+ session_id: sessionId,
3351
+ });
3352
+ const result = await dispatchCodexNativeHook({
3353
+ hook_event_name: "UserPromptSubmit",
3354
+ cwd,
3355
+ session_id: sessionId,
3356
+ thread_id: "thread-autopilot-question-answer",
3357
+ turn_id: "turn-autopilot-question-answer",
3358
+ prompt: "[omx question answered] semantic_marker_expansion $ralplan",
3359
+ }, { cwd });
3360
+ assert.equal(result.omxEventName, "keyword-detector");
3361
+ assert.equal(result.skillState?.skill, "autopilot");
3362
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
3363
+ assert.match(message, /continued active workflow skill "autopilot"/);
3364
+ assert.match(message, /Autopilot protocol:/);
3365
+ assert.match(message, /structured question chain, not a one-question gate/);
3366
+ assert.match(message, /This turn is a marked omx question answer/);
3367
+ assert.match(message, /then re-score/);
3368
+ assert.match(message, /write interview_complete evidence and hand off/);
3369
+ assert.match(message, /readiness gate remains unresolved and the answer would materially change execution/);
3370
+ assert.match(message, /Do not advance from deep-interview to ralplan merely because the first question was answered/);
3371
+ assert.doesNotMatch(message, /denied workflow keyword/i);
3372
+ assert.equal(existsSync(join(sessionDir, "ralplan-state.json")), false);
3373
+ }
3374
+ finally {
3375
+ await rm(cwd, { recursive: true, force: true });
3376
+ }
3377
+ });
3378
+ it("keeps deep-interview bridge guidance on marked question answers with workflow-like tokens", async () => {
3379
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-question-answer-continuation-"));
3380
+ try {
3381
+ const sessionId = "sess-deep-interview-question-answer";
3382
+ const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
3383
+ await mkdir(sessionDir, { recursive: true });
3384
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
3385
+ version: 1,
3386
+ active: true,
3387
+ skill: "deep-interview",
3388
+ keyword: "$deep-interview",
3389
+ phase: "planning",
3390
+ initialized_mode: "deep-interview",
3391
+ initialized_state_path: `.omx/state/sessions/${sessionId}/deep-interview-state.json`,
3392
+ session_id: sessionId,
3393
+ active_skills: [
3394
+ { skill: "deep-interview", phase: "planning", active: true, session_id: sessionId },
3395
+ ],
3396
+ });
3397
+ await writeJson(join(sessionDir, "deep-interview-state.json"), {
3398
+ active: true,
3399
+ mode: "deep-interview",
3400
+ current_phase: "intent-first",
3401
+ started_at: "2026-04-21T10:00:00.000Z",
3402
+ updated_at: "2026-04-21T10:00:00.000Z",
3403
+ });
3404
+ const result = await dispatchCodexNativeHook({
3405
+ hook_event_name: "UserPromptSubmit",
3406
+ cwd,
3407
+ session_id: sessionId,
3408
+ thread_id: "thread-deep-interview-question-answer",
3409
+ turn_id: "turn-deep-interview-question-answer",
3410
+ prompt: "[omx question answered] answer text $ralplan",
3411
+ }, { cwd });
3412
+ assert.equal(result.omxEventName, "keyword-detector");
3413
+ assert.equal(result.skillState?.skill, "deep-interview");
3414
+ const message = String(result.outputJson?.hookSpecificOutput?.additionalContext || "");
3415
+ assert.match(message, /continued active workflow skill "deep-interview"/);
3416
+ assert.match(message, /workflow-like tokens inside the marked omx question answer are treated as answer text/);
3417
+ assert.match(message, /Deep-interview is active, but this session is not attached to tmux/);
3418
+ assert.match(message, /native structured question tool when available/);
3419
+ assert.doesNotMatch(message, /detected workflow keyword "\$ralplan" -> ralplan/);
3420
+ assert.equal(existsSync(join(sessionDir, "ralplan-state.json")), false);
3421
+ }
3422
+ finally {
3423
+ await rm(cwd, { recursive: true, force: true });
3424
+ }
3425
+ });
2717
3426
  it("clarifies outside-tmux prompt-side deep-interview activation without pretending omx question is directly available", async () => {
2718
3427
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-routing-"));
2719
3428
  try {
@@ -3140,6 +3849,10 @@ export async function onHookEvent(event) {
3140
3849
  active: true,
3141
3850
  mode: "deep-interview",
3142
3851
  current_phase: "intent-first",
3852
+ deep_interview_gate: {
3853
+ status: "complete",
3854
+ rationale: "Requirements are clarified and ready for ralplan consensus.",
3855
+ },
3143
3856
  });
3144
3857
  await writeJson(join(sessionDir, "skill-active-state.json"), {
3145
3858
  active: true,
@@ -5758,20 +6471,56 @@ exit 0
5758
6471
  await rm(cwd, { recursive: true, force: true });
5759
6472
  }
5760
6473
  });
5761
- it("treats stderr-only informative non-zero output as reviewable instead of a generic failure", async () => {
5762
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-informative-stderr-"));
6474
+ it("does not block ordinary non-zero grep output in PostToolUse", async () => {
6475
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-grep-nonzero-"));
5763
6476
  try {
5764
6477
  const result = await dispatchCodexNativeHook({
5765
6478
  hook_event_name: "PostToolUse",
5766
6479
  cwd,
5767
6480
  tool_name: "Bash",
5768
- tool_use_id: "tool-useful-stderr",
5769
- tool_input: { command: "gh pr checks" },
5770
- tool_response: "{\"exit_code\":8,\"stdout\":\"\",\"stderr\":\"build pending\\nlint pass\"}",
6481
+ tool_use_id: "tool-grep-nonzero",
6482
+ tool_input: { command: "grep -R missing-pattern src | head -20" },
6483
+ tool_response: "{\"exit_code\":1,\"stdout\":\"src/example.ts:TODO\",\"stderr\":\"\"}",
5771
6484
  }, { cwd });
5772
6485
  assert.equal(result.omxEventName, "post-tool-use");
5773
- assert.deepEqual(result.outputJson, {
5774
- decision: "block",
6486
+ assert.equal(result.outputJson, null);
6487
+ }
6488
+ finally {
6489
+ await rm(cwd, { recursive: true, force: true });
6490
+ }
6491
+ });
6492
+ it("does not block ordinary non-zero diagnostic output in PostToolUse", async () => {
6493
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-diagnostic-nonzero-"));
6494
+ try {
6495
+ const result = await dispatchCodexNativeHook({
6496
+ hook_event_name: "PostToolUse",
6497
+ cwd,
6498
+ tool_name: "Bash",
6499
+ tool_use_id: "tool-diagnostic-nonzero",
6500
+ tool_input: { command: "find src -name nope -print" },
6501
+ tool_response: "{\"exit_code\":1,\"stdout\":\"searched 10 files\",\"stderr\":\"\"}",
6502
+ }, { cwd });
6503
+ assert.equal(result.omxEventName, "post-tool-use");
6504
+ assert.equal(result.outputJson, null);
6505
+ }
6506
+ finally {
6507
+ await rm(cwd, { recursive: true, force: true });
6508
+ }
6509
+ });
6510
+ it("treats stderr-only informative non-zero output as reviewable instead of a generic failure", async () => {
6511
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-informative-stderr-"));
6512
+ try {
6513
+ const result = await dispatchCodexNativeHook({
6514
+ hook_event_name: "PostToolUse",
6515
+ cwd,
6516
+ tool_name: "Bash",
6517
+ tool_use_id: "tool-useful-stderr",
6518
+ tool_input: { command: "gh pr checks" },
6519
+ tool_response: "{\"exit_code\":8,\"stdout\":\"\",\"stderr\":\"build pending\\nlint pass\"}",
6520
+ }, { cwd });
6521
+ assert.equal(result.omxEventName, "post-tool-use");
6522
+ assert.deepEqual(result.outputJson, {
6523
+ decision: "block",
5775
6524
  reason: "The Bash command returned a non-zero exit code but produced useful output that should be reviewed before retrying.",
5776
6525
  hookSpecificOutput: {
5777
6526
  hookEventName: "PostToolUse",
@@ -5808,6 +6557,72 @@ exit 0
5808
6557
  await rm(cwd, { recursive: true, force: true });
5809
6558
  }
5810
6559
  });
6560
+ it("treats wrapped gh pr checks output as reviewable", async () => {
6561
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-gh-wrapped-"));
6562
+ try {
6563
+ for (const command of [
6564
+ "GH_PAGER=cat gh pr checks",
6565
+ "env GH_TOKEN=ghp_testtoken gh pr checks",
6566
+ "/usr/bin/env gh pr checks",
6567
+ "env -- gh pr checks",
6568
+ "env -C repo gh pr checks",
6569
+ "/usr/bin/gh pr checks",
6570
+ "gh --repo owner/repo pr checks",
6571
+ "echo a; gh pr checks",
6572
+ "cd repo && gh pr checks",
6573
+ ]) {
6574
+ const result = await dispatchCodexNativeHook({
6575
+ hook_event_name: "PostToolUse",
6576
+ cwd,
6577
+ tool_name: "Bash",
6578
+ tool_use_id: `tool-useful-${command}`,
6579
+ tool_input: { command },
6580
+ tool_response: "{\"exit_code\":8,\"stdout\":\"build pending\",\"stderr\":\"\"}",
6581
+ }, { cwd });
6582
+ assert.equal(result.omxEventName, "post-tool-use");
6583
+ assert.equal(result.outputJson?.decision, "block", command);
6584
+ }
6585
+ }
6586
+ finally {
6587
+ await rm(cwd, { recursive: true, force: true });
6588
+ }
6589
+ });
6590
+ it("does not treat heredoc gh pr checks text as a reviewable command", async () => {
6591
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-gh-heredoc-"));
6592
+ try {
6593
+ const result = await dispatchCodexNativeHook({
6594
+ hook_event_name: "PostToolUse",
6595
+ cwd,
6596
+ tool_name: "Bash",
6597
+ tool_use_id: "tool-heredoc-gh-checks",
6598
+ tool_input: { command: "cat <<'EOF'\ngh pr checks\nEOF\nfalse" },
6599
+ tool_response: "{\"exit_code\":1,\"stdout\":\"gh pr checks\",\"stderr\":\"\"}",
6600
+ }, { cwd });
6601
+ assert.equal(result.omxEventName, "post-tool-use");
6602
+ assert.equal(result.outputJson, null);
6603
+ }
6604
+ finally {
6605
+ await rm(cwd, { recursive: true, force: true });
6606
+ }
6607
+ });
6608
+ it("does not treat echoed gh pr checks text as a reviewable command", async () => {
6609
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-gh-echo-"));
6610
+ try {
6611
+ const result = await dispatchCodexNativeHook({
6612
+ hook_event_name: "PostToolUse",
6613
+ cwd,
6614
+ tool_name: "Bash",
6615
+ tool_use_id: "tool-echo-gh-checks",
6616
+ tool_input: { command: "echo gh pr checks" },
6617
+ tool_response: "{\"exit_code\":1,\"stdout\":\"gh pr checks\",\"stderr\":\"\"}",
6618
+ }, { cwd });
6619
+ assert.equal(result.omxEventName, "post-tool-use");
6620
+ assert.equal(result.outputJson, null);
6621
+ }
6622
+ finally {
6623
+ await rm(cwd, { recursive: true, force: true });
6624
+ }
6625
+ });
5811
6626
  it("returns MCP transport-death guidance and preserves failed team state", async () => {
5812
6627
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-dead-"));
5813
6628
  try {
@@ -6649,6 +7464,307 @@ exit 0
6649
7464
  await rm(cwd, { recursive: true, force: true });
6650
7465
  }
6651
7466
  });
7467
+ it("dedupes allowed worker Stop leader nudges across workers in the same team window", async () => {
7468
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-team-dedupe-"));
7469
+ const prevPath = process.env.PATH;
7470
+ try {
7471
+ const stateDir = join(cwd, ".omx", "state");
7472
+ const logsDir = join(cwd, ".omx", "logs");
7473
+ const teamName = "worker-stop-team-dedupe";
7474
+ const teamDir = join(stateDir, "team", teamName);
7475
+ const fakeBinDir = join(cwd, "fake-bin");
7476
+ const tmuxLogPath = join(cwd, "tmux.log");
7477
+ await mkdir(fakeBinDir, { recursive: true });
7478
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
7479
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
7480
+ await writeJson(join(teamDir, "manifest.v2.json"), {
7481
+ name: teamName,
7482
+ tmux_session: "omx-team-worker-stop",
7483
+ leader_pane_id: "%42",
7484
+ workers: [
7485
+ { name: "worker-1", index: 1, pane_id: "%10" },
7486
+ { name: "worker-2", index: 2, pane_id: "%11" },
7487
+ ],
7488
+ });
7489
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
7490
+ const first = await maybeNudgeLeaderForAllowedWorkerStop({
7491
+ stateDir,
7492
+ logsDir,
7493
+ workerContext: { teamName, workerName: "worker-1" },
7494
+ });
7495
+ const second = await maybeNudgeLeaderForAllowedWorkerStop({
7496
+ stateDir,
7497
+ logsDir,
7498
+ workerContext: { teamName, workerName: "worker-2" },
7499
+ });
7500
+ assert.equal(first.result, "sent");
7501
+ assert.equal(second.result, "suppressed_team_cooldown");
7502
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
7503
+ const stopNudges = tmuxLog.match(/send-keys -t %42 -l \[OMX\] worker-\d+ native Stop allowed/g) || [];
7504
+ assert.equal(stopNudges.length, 1, "same-team workers should share one leader nudge cooldown window");
7505
+ const teamNudgeState = JSON.parse(await readFile(join(teamDir, "worker-stop-nudge.json"), "utf-8"));
7506
+ assert.equal(teamNudgeState.worker, "worker-1");
7507
+ assert.equal(teamNudgeState.delivery, "sent");
7508
+ }
7509
+ finally {
7510
+ if (typeof prevPath === "string")
7511
+ process.env.PATH = prevPath;
7512
+ else
7513
+ delete process.env.PATH;
7514
+ await rm(cwd, { recursive: true, force: true });
7515
+ }
7516
+ });
7517
+ it("serializes concurrent allowed worker Stop leader nudges with a team lock", async () => {
7518
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-concurrent-dedupe-"));
7519
+ const prevPath = process.env.PATH;
7520
+ try {
7521
+ const stateDir = join(cwd, ".omx", "state");
7522
+ const logsDir = join(cwd, ".omx", "logs");
7523
+ const teamName = "worker-stop-concurrent";
7524
+ const teamDir = join(stateDir, "team", teamName);
7525
+ const fakeBinDir = join(cwd, "fake-bin");
7526
+ const tmuxLogPath = join(cwd, "tmux.log");
7527
+ await mkdir(fakeBinDir, { recursive: true });
7528
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, { sendDelayMs: 100 }));
7529
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
7530
+ await writeJson(join(teamDir, "manifest.v2.json"), {
7531
+ name: teamName,
7532
+ tmux_session: "omx-team-worker-stop",
7533
+ leader_pane_id: "%42",
7534
+ workers: [
7535
+ { name: "worker-1", index: 1, pane_id: "%10" },
7536
+ { name: "worker-2", index: 2, pane_id: "%11" },
7537
+ ],
7538
+ });
7539
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
7540
+ const results = await Promise.all([
7541
+ maybeNudgeLeaderForAllowedWorkerStop({
7542
+ stateDir,
7543
+ logsDir,
7544
+ workerContext: { teamName, workerName: "worker-1" },
7545
+ }),
7546
+ maybeNudgeLeaderForAllowedWorkerStop({
7547
+ stateDir,
7548
+ logsDir,
7549
+ workerContext: { teamName, workerName: "worker-2" },
7550
+ }),
7551
+ ]);
7552
+ assert.equal(results.filter((result) => result.result === "sent").length, 1);
7553
+ assert.equal(results.filter((result) => result.result === "suppressed_team_lock_held").length, 1);
7554
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
7555
+ const stopNudges = tmuxLog.match(/send-keys -t %42 -l \[OMX\] worker-\d+ native Stop allowed/g) || [];
7556
+ assert.equal(stopNudges.length, 1, "concurrent same-team workers should emit only one leader nudge");
7557
+ assert.equal(existsSync(join(teamDir, "worker-stop-nudge.lock")), false);
7558
+ }
7559
+ finally {
7560
+ if (typeof prevPath === "string")
7561
+ process.env.PATH = prevPath;
7562
+ else
7563
+ delete process.env.PATH;
7564
+ await rm(cwd, { recursive: true, force: true });
7565
+ }
7566
+ });
7567
+ it("skips worker Stop leader nudge when team state is missing or shut down", async () => {
7568
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-missing-team-"));
7569
+ try {
7570
+ const stateDir = join(cwd, ".omx", "state");
7571
+ const logsDir = join(cwd, ".omx", "logs");
7572
+ const result = await maybeNudgeLeaderForAllowedWorkerStop({
7573
+ stateDir,
7574
+ logsDir,
7575
+ workerContext: { teamName: "removed-team", workerName: "worker-1" },
7576
+ });
7577
+ assert.equal(result.result, "team_state_gone_or_shutdown");
7578
+ assert.equal(existsSync(join(stateDir, "team", "removed-team", "worker-stop-nudge.json")), false);
7579
+ await writeJson(join(stateDir, "team", "shutdown-team", "shutdown.json"), {
7580
+ started_at: new Date().toISOString(),
7581
+ });
7582
+ const shutdownResult = await maybeNudgeLeaderForAllowedWorkerStop({
7583
+ stateDir,
7584
+ logsDir,
7585
+ workerContext: { teamName: "shutdown-team", workerName: "worker-1" },
7586
+ });
7587
+ assert.equal(shutdownResult.result, "team_state_gone_or_shutdown");
7588
+ assert.equal(existsSync(join(stateDir, "team", "shutdown-team", "worker-stop-nudge.json")), false);
7589
+ }
7590
+ finally {
7591
+ await rm(cwd, { recursive: true, force: true });
7592
+ }
7593
+ });
7594
+ it("does not treat old visible worker Stop transcript as pending queue state", async () => {
7595
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-queue-dedupe-"));
7596
+ const prevPath = process.env.PATH;
7597
+ try {
7598
+ const stateDir = join(cwd, ".omx", "state");
7599
+ const logsDir = join(cwd, ".omx", "logs");
7600
+ const teamName = "queued-stop-dedupe";
7601
+ const teamDir = join(stateDir, "team", teamName);
7602
+ const fakeBinDir = join(cwd, "fake-bin");
7603
+ const tmuxLogPath = join(cwd, "tmux.log");
7604
+ await mkdir(fakeBinDir, { recursive: true });
7605
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, {
7606
+ busyLeader: true,
7607
+ captureText: `[OMX] worker-1 native Stop allowed. Run \`omx team status ${teamName}\`, read worker messages/results, then assign next task, reconcile completion, or shut down. [OMX_TMUX_INJECT]\n`
7608
+ + "• Working… (esc to interrupt)",
7609
+ }));
7610
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
7611
+ await writeJson(join(teamDir, "manifest.v2.json"), {
7612
+ name: teamName,
7613
+ tmux_session: "omx-team-worker-stop",
7614
+ leader_pane_id: "%42",
7615
+ workers: [{ name: "worker-2", index: 2, pane_id: "%11" }],
7616
+ });
7617
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
7618
+ const result = await maybeNudgeLeaderForAllowedWorkerStop({
7619
+ stateDir,
7620
+ logsDir,
7621
+ workerContext: { teamName, workerName: "worker-2" },
7622
+ });
7623
+ assert.equal(result.result, "queued");
7624
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
7625
+ assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-2 native Stop allowed/);
7626
+ assert.match(tmuxLog, /send-keys -t %42 Tab/);
7627
+ const teamNudgeState = JSON.parse(await readFile(join(teamDir, "worker-stop-nudge.json"), "utf-8"));
7628
+ assert.equal(teamNudgeState.worker, "worker-2");
7629
+ assert.equal(teamNudgeState.delivery, "queued");
7630
+ }
7631
+ finally {
7632
+ if (typeof prevPath === "string")
7633
+ process.env.PATH = prevPath;
7634
+ else
7635
+ delete process.env.PATH;
7636
+ await rm(cwd, { recursive: true, force: true });
7637
+ }
7638
+ });
7639
+ it("reports deferred when non-teardown persistence failure prevents worker Stop nudge cooldown state", async () => {
7640
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-persist-fail-"));
7641
+ const prevPath = process.env.PATH;
7642
+ try {
7643
+ const stateDir = join(cwd, ".omx", "state");
7644
+ const logsDir = join(cwd, ".omx", "logs");
7645
+ const teamName = "worker-stop-persist-fail";
7646
+ const teamDir = join(stateDir, "team", teamName);
7647
+ const fakeBinDir = join(cwd, "fake-bin");
7648
+ const tmuxLogPath = join(cwd, "tmux.log");
7649
+ await mkdir(fakeBinDir, { recursive: true });
7650
+ await writeJson(join(teamDir, "manifest.v2.json"), {
7651
+ name: teamName,
7652
+ tmux_session: "omx-team-worker-stop",
7653
+ leader_pane_id: "%42",
7654
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
7655
+ });
7656
+ await writeFile(join(teamDir, "workers"), "not a directory");
7657
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
7658
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
7659
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
7660
+ const result = await maybeNudgeLeaderForAllowedWorkerStop({
7661
+ stateDir,
7662
+ logsDir,
7663
+ workerContext: { teamName, workerName: "worker-1" },
7664
+ });
7665
+ assert.equal(result.result, "deferred");
7666
+ assert.equal(existsSync(join(teamDir, "worker-stop-nudge.json")), false);
7667
+ assert.equal(existsSync(join(teamDir, "workers", "worker-1", "worker-stop-nudge.json")), false);
7668
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
7669
+ assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
7670
+ const deliveryLogPath = join(logsDir, `team-delivery-${new Date().toISOString().split("T")[0]}.jsonl`);
7671
+ const deliveryEvents = (await readFile(deliveryLogPath, "utf-8"))
7672
+ .trim()
7673
+ .split("\n")
7674
+ .map((line) => JSON.parse(line));
7675
+ const deferredEvent = deliveryEvents.find((event) => event.event === "nudge_triggered" && event.result === "deferred");
7676
+ assert.equal(deferredEvent?.team, teamName);
7677
+ assert.equal(deferredEvent?.from_worker, "worker-1");
7678
+ assert.match(String(deferredEvent?.reason || ""), /EEXIST|ENOTDIR|not a directory|file already exists/);
7679
+ }
7680
+ finally {
7681
+ if (typeof prevPath === "string")
7682
+ process.env.PATH = prevPath;
7683
+ else
7684
+ delete process.env.PATH;
7685
+ await rm(cwd, { recursive: true, force: true });
7686
+ }
7687
+ });
7688
+ it("does not recreate team state when teardown removes it during worker Stop delivery", async () => {
7689
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-teardown-race-"));
7690
+ const prevPath = process.env.PATH;
7691
+ try {
7692
+ const stateDir = join(cwd, ".omx", "state");
7693
+ const logsDir = join(cwd, ".omx", "logs");
7694
+ const teamName = "worker-stop-teardown-race";
7695
+ const teamDir = join(stateDir, "team", teamName);
7696
+ const fakeBinDir = join(cwd, "fake-bin");
7697
+ const tmuxLogPath = join(cwd, "tmux.log");
7698
+ await mkdir(fakeBinDir, { recursive: true });
7699
+ await writeJson(join(teamDir, "manifest.v2.json"), {
7700
+ name: teamName,
7701
+ tmux_session: "omx-team-worker-stop",
7702
+ leader_pane_id: "%42",
7703
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
7704
+ });
7705
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, { removePathOnSend: teamDir }));
7706
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
7707
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
7708
+ const result = await maybeNudgeLeaderForAllowedWorkerStop({
7709
+ stateDir,
7710
+ logsDir,
7711
+ workerContext: { teamName, workerName: "worker-1" },
7712
+ });
7713
+ assert.equal(result.result, "sent");
7714
+ assert.equal(existsSync(teamDir), false, "worker Stop delivery must not recreate removed team state");
7715
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
7716
+ assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
7717
+ }
7718
+ finally {
7719
+ if (typeof prevPath === "string")
7720
+ process.env.PATH = prevPath;
7721
+ else
7722
+ delete process.env.PATH;
7723
+ await rm(cwd, { recursive: true, force: true });
7724
+ }
7725
+ });
7726
+ it("does not recreate team state when teardown removes it before deferred worker Stop recording", async () => {
7727
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-deferred-teardown-"));
7728
+ const prevPath = process.env.PATH;
7729
+ try {
7730
+ const stateDir = join(cwd, ".omx", "state");
7731
+ const logsDir = join(cwd, ".omx", "logs");
7732
+ const teamName = "worker-stop-deferred-teardown";
7733
+ const teamDir = join(stateDir, "team", teamName);
7734
+ const fakeBinDir = join(cwd, "fake-bin");
7735
+ const tmuxLogPath = join(cwd, "tmux.log");
7736
+ await mkdir(fakeBinDir, { recursive: true });
7737
+ await writeJson(join(teamDir, "manifest.v2.json"), {
7738
+ name: teamName,
7739
+ tmux_session: "omx-team-worker-stop",
7740
+ leader_pane_id: "%42",
7741
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
7742
+ });
7743
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, {
7744
+ currentCommand: "bash",
7745
+ captureText: "$ ",
7746
+ removePathOnCapture: teamDir,
7747
+ }));
7748
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
7749
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
7750
+ const result = await maybeNudgeLeaderForAllowedWorkerStop({
7751
+ stateDir,
7752
+ logsDir,
7753
+ workerContext: { teamName, workerName: "worker-1" },
7754
+ });
7755
+ assert.equal(result.result, "team_state_gone_or_shutdown");
7756
+ assert.equal(existsSync(teamDir), false, "deferred worker Stop recording must not recreate removed team state");
7757
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
7758
+ assert.doesNotMatch(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
7759
+ }
7760
+ finally {
7761
+ if (typeof prevPath === "string")
7762
+ process.env.PATH = prevPath;
7763
+ else
7764
+ delete process.env.PATH;
7765
+ await rm(cwd, { recursive: true, force: true });
7766
+ }
7767
+ });
6652
7768
  it("allows worker Stop when the Stop nudge helper cannot deliver", async () => {
6653
7769
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-helper-fail-"));
6654
7770
  const prevTeamWorker = process.env.OMX_TEAM_WORKER;
@@ -10337,6 +11453,315 @@ exit 0
10337
11453
  await rm(cwd, { recursive: true, force: true });
10338
11454
  }
10339
11455
  });
11456
+ it("blocks implementation writes while ralplan is active without execution handoff", async () => {
11457
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-block-"));
11458
+ try {
11459
+ const stateDir = join(cwd, ".omx", "state");
11460
+ const sessionId = "sess-ralplan-pretool-block";
11461
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
11462
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
11463
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
11464
+ active: true,
11465
+ skill: "ralplan",
11466
+ phase: "planning",
11467
+ session_id: sessionId,
11468
+ active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
11469
+ });
11470
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
11471
+ active: true,
11472
+ mode: "ralplan",
11473
+ current_phase: "critic-review",
11474
+ session_id: sessionId,
11475
+ });
11476
+ const result = await dispatchCodexNativeHook({
11477
+ hook_event_name: "PreToolUse",
11478
+ cwd,
11479
+ session_id: sessionId,
11480
+ thread_id: "thread-ralplan-pretool-block",
11481
+ tool_name: "Edit",
11482
+ tool_input: { file_path: "src/runtime.ts" },
11483
+ }, { cwd });
11484
+ assert.equal(result.omxEventName, "pre-tool-use");
11485
+ assert.equal(result.outputJson?.decision, "block");
11486
+ assert.match(String(result.outputJson?.reason ?? ""), /Ralplan is active .*implementation\/write tools are blocked/i);
11487
+ assert.match(String(result.outputJson?.hookSpecificOutput?.additionalContext ?? ""), /\$ultragoal.*\$team.*\$ralph/i);
11488
+ }
11489
+ finally {
11490
+ await rm(cwd, { recursive: true, force: true });
11491
+ }
11492
+ });
11493
+ it("blocks implementation writes while Autopilot is supervising ralplan without handoff", async () => {
11494
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-pretool-block-"));
11495
+ try {
11496
+ const stateDir = join(cwd, ".omx", "state");
11497
+ const sessionId = "sess-autopilot-ralplan-pretool-block";
11498
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
11499
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
11500
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
11501
+ active: true,
11502
+ skill: "autopilot",
11503
+ phase: "ralplan",
11504
+ session_id: sessionId,
11505
+ active_skills: [{ skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId }],
11506
+ });
11507
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
11508
+ active: true,
11509
+ mode: "autopilot",
11510
+ current_phase: "ralplan",
11511
+ session_id: sessionId,
11512
+ state: {
11513
+ handoff_artifacts: {
11514
+ ralplan_consensus_gate: { required: true, complete: false },
11515
+ },
11516
+ },
11517
+ });
11518
+ const result = await dispatchCodexNativeHook({
11519
+ hook_event_name: "PreToolUse",
11520
+ cwd,
11521
+ session_id: sessionId,
11522
+ thread_id: "thread-autopilot-ralplan-pretool-block",
11523
+ tool_name: "Edit",
11524
+ tool_input: { file_path: "src/runtime.ts" },
11525
+ }, { cwd });
11526
+ assert.equal(result.omxEventName, "pre-tool-use");
11527
+ assert.equal(result.outputJson?.decision, "block");
11528
+ assert.match(String(result.outputJson?.reason ?? ""), /Ralplan is active .*implementation\/write tools are blocked/i);
11529
+ }
11530
+ finally {
11531
+ await rm(cwd, { recursive: true, force: true });
11532
+ }
11533
+ });
11534
+ it("allows implementation writes when terminal Autopilot run-state shadows stale supervised ralplan state", async () => {
11535
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-terminal-pretool-"));
11536
+ try {
11537
+ const stateDir = join(cwd, ".omx", "state");
11538
+ const sessionId = "sess-autopilot-ralplan-terminal-pretool";
11539
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
11540
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
11541
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
11542
+ active: true,
11543
+ skill: "autopilot",
11544
+ phase: "ralplan",
11545
+ session_id: sessionId,
11546
+ active_skills: [{ skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId }],
11547
+ });
11548
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
11549
+ active: true,
11550
+ mode: "autopilot",
11551
+ current_phase: "ralplan",
11552
+ session_id: sessionId,
11553
+ });
11554
+ await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
11555
+ version: 1,
11556
+ active: false,
11557
+ mode: "autopilot",
11558
+ outcome: "finish",
11559
+ lifecycle_outcome: "finished",
11560
+ current_phase: "complete",
11561
+ completed_at: "2026-05-30T00:00:00.000Z",
11562
+ updated_at: "2026-05-30T00:00:00.000Z",
11563
+ });
11564
+ const result = await dispatchCodexNativeHook({
11565
+ hook_event_name: "PreToolUse",
11566
+ cwd,
11567
+ session_id: sessionId,
11568
+ thread_id: "thread-autopilot-ralplan-terminal-pretool",
11569
+ tool_name: "Edit",
11570
+ tool_input: { file_path: "src/runtime.ts" },
11571
+ }, { cwd });
11572
+ assert.equal(result.omxEventName, "pre-tool-use");
11573
+ assert.equal(result.outputJson, null);
11574
+ }
11575
+ finally {
11576
+ await rm(cwd, { recursive: true, force: true });
11577
+ }
11578
+ });
11579
+ it("blocks bash implementation writes while Autopilot is supervising ralplan without handoff", async () => {
11580
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-pretool-bash-block-"));
11581
+ try {
11582
+ const stateDir = join(cwd, ".omx", "state");
11583
+ const sessionId = "sess-autopilot-ralplan-pretool-bash-block";
11584
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
11585
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
11586
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
11587
+ active: true,
11588
+ skill: "autopilot",
11589
+ phase: "ralplan",
11590
+ session_id: sessionId,
11591
+ active_skills: [{ skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId }],
11592
+ });
11593
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
11594
+ active: true,
11595
+ mode: "autopilot",
11596
+ current_phase: "ralplan",
11597
+ session_id: sessionId,
11598
+ });
11599
+ const result = await dispatchCodexNativeHook({
11600
+ hook_event_name: "PreToolUse",
11601
+ cwd,
11602
+ session_id: sessionId,
11603
+ thread_id: "thread-autopilot-ralplan-pretool-bash-block",
11604
+ tool_name: "Bash",
11605
+ tool_input: { command: "cat <<'EOF' > src/runtime.ts\nimplementation\nEOF" },
11606
+ }, { cwd });
11607
+ assert.equal(result.omxEventName, "pre-tool-use");
11608
+ assert.equal(result.outputJson?.decision, "block");
11609
+ assert.match(String(result.outputJson?.reason ?? ""), /Ralplan is active .*implementation\/write tools are blocked/i);
11610
+ }
11611
+ finally {
11612
+ await rm(cwd, { recursive: true, force: true });
11613
+ }
11614
+ });
11615
+ it("allows ralplan planning artifact writes without execution handoff", async () => {
11616
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-artifact-"));
11617
+ try {
11618
+ const stateDir = join(cwd, ".omx", "state");
11619
+ const sessionId = "sess-ralplan-pretool-artifact";
11620
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
11621
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
11622
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
11623
+ active: true,
11624
+ skill: "ralplan",
11625
+ phase: "planning",
11626
+ session_id: sessionId,
11627
+ active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
11628
+ });
11629
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
11630
+ active: true,
11631
+ mode: "ralplan",
11632
+ current_phase: "planning",
11633
+ session_id: sessionId,
11634
+ });
11635
+ const result = await dispatchCodexNativeHook({
11636
+ hook_event_name: "PreToolUse",
11637
+ cwd,
11638
+ session_id: sessionId,
11639
+ thread_id: "thread-ralplan-pretool-artifact",
11640
+ tool_name: "Write",
11641
+ tool_input: { file_path: ".omx/plans/prd-issue-2603.md" },
11642
+ }, { cwd });
11643
+ assert.equal(result.omxEventName, "pre-tool-use");
11644
+ assert.equal(result.outputJson, null);
11645
+ }
11646
+ finally {
11647
+ await rm(cwd, { recursive: true, force: true });
11648
+ }
11649
+ });
11650
+ it("blocks bash implementation writes while ralplan is active without execution handoff", async () => {
11651
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-bash-block-"));
11652
+ try {
11653
+ const stateDir = join(cwd, ".omx", "state");
11654
+ const sessionId = "sess-ralplan-pretool-bash-block";
11655
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
11656
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
11657
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
11658
+ active: true,
11659
+ skill: "ralplan",
11660
+ phase: "planning",
11661
+ session_id: sessionId,
11662
+ active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
11663
+ });
11664
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
11665
+ active: true,
11666
+ mode: "ralplan",
11667
+ current_phase: "planning",
11668
+ session_id: sessionId,
11669
+ });
11670
+ const result = await dispatchCodexNativeHook({
11671
+ hook_event_name: "PreToolUse",
11672
+ cwd,
11673
+ session_id: sessionId,
11674
+ thread_id: "thread-ralplan-pretool-bash-block",
11675
+ tool_name: "Bash",
11676
+ tool_input: { command: "cat <<'EOF' > src/runtime.ts\nimplementation\nEOF" },
11677
+ }, { cwd });
11678
+ assert.equal(result.omxEventName, "pre-tool-use");
11679
+ assert.equal(result.outputJson?.decision, "block");
11680
+ assert.match(String(result.outputJson?.reason ?? ""), /Ralplan is active .*implementation\/write tools are blocked/i);
11681
+ }
11682
+ finally {
11683
+ await rm(cwd, { recursive: true, force: true });
11684
+ }
11685
+ });
11686
+ it("allows bash planning artifact writes while ralplan is active without execution handoff", async () => {
11687
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-bash-artifact-"));
11688
+ try {
11689
+ const stateDir = join(cwd, ".omx", "state");
11690
+ const sessionId = "sess-ralplan-pretool-bash-artifact";
11691
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
11692
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
11693
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
11694
+ active: true,
11695
+ skill: "ralplan",
11696
+ phase: "planning",
11697
+ session_id: sessionId,
11698
+ active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
11699
+ });
11700
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
11701
+ active: true,
11702
+ mode: "ralplan",
11703
+ current_phase: "planning",
11704
+ session_id: sessionId,
11705
+ });
11706
+ const result = await dispatchCodexNativeHook({
11707
+ hook_event_name: "PreToolUse",
11708
+ cwd,
11709
+ session_id: sessionId,
11710
+ thread_id: "thread-ralplan-pretool-bash-artifact",
11711
+ tool_name: "Bash",
11712
+ tool_input: { command: "cat <<'EOF' > .omx/plans/prd-issue-2603.md\nplanning\nEOF" },
11713
+ }, { cwd });
11714
+ assert.equal(result.omxEventName, "pre-tool-use");
11715
+ assert.equal(result.outputJson, null);
11716
+ }
11717
+ finally {
11718
+ await rm(cwd, { recursive: true, force: true });
11719
+ }
11720
+ });
11721
+ it("allows implementation writes when an explicit execution handoff is active", async () => {
11722
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-handoff-"));
11723
+ try {
11724
+ const stateDir = join(cwd, ".omx", "state");
11725
+ const sessionId = "sess-ralplan-pretool-handoff";
11726
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
11727
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
11728
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
11729
+ active: true,
11730
+ skill: "ultragoal",
11731
+ phase: "planning",
11732
+ session_id: sessionId,
11733
+ active_skills: [
11734
+ { skill: "ralplan", phase: "planning", active: true, session_id: sessionId },
11735
+ { skill: "ultragoal", phase: "planning", active: true, session_id: sessionId },
11736
+ ],
11737
+ });
11738
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
11739
+ active: true,
11740
+ mode: "ralplan",
11741
+ current_phase: "complete",
11742
+ session_id: sessionId,
11743
+ });
11744
+ await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
11745
+ active: true,
11746
+ mode: "ultragoal",
11747
+ current_phase: "planning",
11748
+ session_id: sessionId,
11749
+ });
11750
+ const result = await dispatchCodexNativeHook({
11751
+ hook_event_name: "PreToolUse",
11752
+ cwd,
11753
+ session_id: sessionId,
11754
+ thread_id: "thread-ralplan-pretool-handoff",
11755
+ tool_name: "Edit",
11756
+ tool_input: { file_path: "src/runtime.ts" },
11757
+ }, { cwd });
11758
+ assert.equal(result.omxEventName, "pre-tool-use");
11759
+ assert.equal(result.outputJson, null);
11760
+ }
11761
+ finally {
11762
+ await rm(cwd, { recursive: true, force: true });
11763
+ }
11764
+ });
10340
11765
  it("does not block Stop from root team state without team_name when no session is known", async () => {
10341
11766
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-root-team-no-session-no-name-"));
10342
11767
  try {
@@ -10866,6 +12291,34 @@ describe("codex native hook triage integration", () => {
10866
12291
  await rm(cwd, { recursive: true, force: true });
10867
12292
  }
10868
12293
  });
12294
+ it("makes bare autopilot command activation observable in state and prompt guidance", async () => {
12295
+ const cwd = await mkdtemp(join(tmpdir(), "omx-autopilot-bare-observable-"));
12296
+ try {
12297
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
12298
+ await writeSessionStart(cwd, "sess-autopilot-bare-observable");
12299
+ const result = await dispatchCodexNativeHook({
12300
+ hook_event_name: "UserPromptSubmit",
12301
+ cwd,
12302
+ session_id: "sess-autopilot-bare-observable",
12303
+ thread_id: "thread-autopilot-bare-observable",
12304
+ turn_id: "turn-autopilot-bare-observable",
12305
+ prompt: "run autopilot",
12306
+ }, { cwd });
12307
+ assert.equal(result.skillState?.skill, "autopilot");
12308
+ assert.equal(result.skillState?.phase, "deep-interview");
12309
+ assert.equal(result.skillState?.initialized_state_path, ".omx/state/sessions/sess-autopilot-bare-observable/autopilot-state.json");
12310
+ const additionalContext = String(result.outputJson?.hookSpecificOutput?.additionalContext ?? "");
12311
+ assert.match(additionalContext, /detected workflow keyword "autopilot" -> autopilot/);
12312
+ assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
12313
+ const statePath = join(cwd, ".omx", "state", "sessions", "sess-autopilot-bare-observable", "autopilot-state.json");
12314
+ const modeState = JSON.parse(await readFile(statePath, "utf-8"));
12315
+ assert.equal(modeState.active, true);
12316
+ assert.equal(modeState.current_phase, "deep-interview");
12317
+ }
12318
+ finally {
12319
+ await rm(cwd, { recursive: true, force: true });
12320
+ }
12321
+ });
10869
12322
  // ── Group 2: HEAVY injection ─────────────────────────────────────────────
10870
12323
  it("injects HEAVY advisory and writes prompt-routing-state for a multi-step goal prompt", async () => {
10871
12324
  const cwd = await mkdtemp(join(tmpdir(), "omx-triage-heavy-"));