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
@@ -33,6 +33,8 @@ import { getLegacyWikiDir, serializePage, writePage } from "../../wiki/storage.j
33
33
  import { WIKI_SCHEMA_VERSION } from "../../wiki/types.js";
34
34
  import { createUltragoalPlan, readUltragoalPlan } from "../../ultragoal/artifacts.js";
35
35
  import { getBaseStateDir } from "../../state/paths.js";
36
+ import { maybeNudgeLeaderForAllowedWorkerStop } from "../notify-hook/team-worker-stop.js";
37
+ import { MAX_NATIVE_STDIN_JSON_BYTES } from "../hook-payload-guard.js";
36
38
 
37
39
  function nativeHookScriptPath(): string {
38
40
  return join(process.cwd(), "dist", "scripts", "codex-native-hook.js");
@@ -136,8 +138,22 @@ async function withLoreGuardConfig<T>(
136
138
 
137
139
  function buildWorkerStopFakeTmux(
138
140
  tmuxLogPath: string,
139
- options: { failSend?: boolean; busyLeader?: boolean } = {},
141
+ options: {
142
+ failSend?: boolean;
143
+ busyLeader?: boolean;
144
+ captureText?: string;
145
+ currentCommand?: string;
146
+ sendDelayMs?: number;
147
+ removePathOnSend?: string;
148
+ removePathOnCapture?: string;
149
+ } = {},
140
150
  ): string {
151
+ const rawCaptureText = options.captureText ?? (options.busyLeader ? "• Working… (esc to interrupt)" : "› ready");
152
+ const captureText = `'${rawCaptureText.replace(/'/g, "'\"'\"'")}'`;
153
+ const currentCommand = `'${(options.currentCommand ?? "codex").replace(/'/g, "'\"'\"'")}'`;
154
+ const sendDelaySeconds = Math.max(0, options.sendDelayMs ?? 0) / 1000;
155
+ const removePathOnSend = options.removePathOnSend ? `'${options.removePathOnSend.replace(/'/g, "'\"'\"'")}'` : "";
156
+ const removePathOnCapture = options.removePathOnCapture ? `'${options.removePathOnCapture.replace(/'/g, "'\"'\"'")}'` : "";
141
157
  return `#!/usr/bin/env bash
142
158
  set -eu
143
159
  echo "$@" >> "${tmuxLogPath}"
@@ -158,17 +174,20 @@ if [[ "$cmd" == "display-message" ]]; then
158
174
  "#{pane_id}") echo "%42" ;;
159
175
  "#{pane_current_path}") pwd ;;
160
176
  "#{pane_start_command}") echo "codex" ;;
161
- "#{pane_current_command}") echo "codex" ;;
177
+ "#{pane_current_command}") printf '%s\\n' ${currentCommand} ;;
162
178
  "#S") echo "omx-team-worker-stop" ;;
163
179
  *) ;;
164
180
  esac
165
181
  exit 0
166
182
  fi
167
183
  if [[ "$cmd" == "capture-pane" ]]; then
168
- ${options.busyLeader ? 'echo "• Working… (esc to interrupt)"' : 'echo "› ready"'}
184
+ ${removePathOnCapture ? `rm -rf ${removePathOnCapture}` : ""}
185
+ printf '%s\\n' ${captureText}
169
186
  exit 0
170
187
  fi
171
188
  if [[ "$cmd" == "send-keys" ]]; then
189
+ ${sendDelaySeconds > 0 ? `sleep ${sendDelaySeconds}` : ""}
190
+ ${removePathOnSend ? `rm -rf ${removePathOnSend}` : ""}
172
191
  ${options.failSend ? "exit 1" : "exit 0"}
173
192
  fi
174
193
  exit 0
@@ -393,13 +412,94 @@ describe("codex native hook dispatch", () => {
393
412
  );
394
413
  });
395
414
 
396
- it("emits deterministic JSON stdout when CLI stdin is malformed", () => {
415
+ it("emits schema-safe JSON stdout when CLI stdin is malformed", () => {
397
416
  const stdout = runNativeHookCli("{");
398
417
 
418
+ const output = parseSingleJsonStdout(stdout) as {
419
+ continue?: boolean;
420
+ stopReason?: string;
421
+ systemMessage?: string;
422
+ hookSpecificOutput?: unknown;
423
+ };
424
+
425
+ assert.equal(output.continue, false);
426
+ assert.equal(output.stopReason, "native_hook_stdin_parse_error");
427
+ assert.equal(output.hookSpecificOutput, undefined);
428
+ assert.match(
429
+ String(output.systemMessage ?? ""),
430
+ /stdin JSON parsing failed inside codex-native-hook:/,
431
+ );
432
+ });
433
+
434
+ it("redacts unterminated prompt-like malformed stdin fields", async () => {
435
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-malformed-unterminated-"));
436
+ try {
437
+ const privatePrompt = "PRIVATE_UNTERMINATED_PROMPT";
438
+ const malformed = `{hook_event_name:"PostToolUse", prompt:"${privatePrompt}`;
439
+ const result = spawnSync(process.execPath, [nativeHookScriptPath()], {
440
+ cwd,
441
+ input: malformed,
442
+ encoding: "utf-8",
443
+ stdio: ["pipe", "pipe", "pipe"],
444
+ });
445
+
446
+ assert.equal(result.status, 0, result.stderr || result.stdout);
447
+ assert.equal(result.stderr, "");
448
+ const output = parseSingleJsonStdout(result.stdout);
449
+ assert.equal(output.stopReason, "native_hook_stdin_parse_error");
450
+
451
+ const log = await readFile(join(cwd, ".omx", "logs", `native-hook-${new Date().toISOString().split("T")[0]}.jsonl`), "utf-8");
452
+ const entry = JSON.parse(log.trim()) as Record<string, unknown>;
453
+ const prefix = String(entry.raw_input_prefix ?? "");
454
+ assert.doesNotMatch(prefix, new RegExp(privatePrompt));
455
+ assert.match(prefix, /prompt:"\[REDACTED\]"/);
456
+ } finally {
457
+ await rm(cwd, { recursive: true, force: true });
458
+ }
459
+ });
460
+
461
+ it("logs a bounded redacted raw stdin prefix when CLI stdin is malformed", async () => {
462
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-malformed-log-prefix-"));
463
+ try {
464
+ const secret = "sk-test-secret123456";
465
+ const promptText = "summarize private launch notes";
466
+ const malformed = `{hook_event_name:"PostToolUse", access_token:"${secret}", prompt:"${promptText}", text:"${promptText}", bad:"${"x".repeat(400)}"}${String.fromCharCode(10, 0, 7)}`;
467
+ const result = spawnSync(process.execPath, [nativeHookScriptPath()], {
468
+ cwd,
469
+ input: malformed,
470
+ encoding: "utf-8",
471
+ stdio: ["pipe", "pipe", "pipe"],
472
+ });
473
+
474
+ assert.equal(result.status, 0, result.stderr || result.stdout);
475
+ assert.equal(result.stderr, "");
476
+ const output = parseSingleJsonStdout(result.stdout);
477
+ assert.equal(output.stopReason, "native_hook_stdin_parse_error");
478
+
479
+ const log = await readFile(join(cwd, ".omx", "logs", `native-hook-${new Date().toISOString().split("T")[0]}.jsonl`), "utf-8");
480
+ const entry = JSON.parse(log.trim()) as Record<string, unknown>;
481
+ const prefix = String(entry.raw_input_prefix ?? "");
482
+ assert.equal(entry.type, "native_hook_stdin_parse_error");
483
+ assert.equal(entry.raw_input_length, Buffer.byteLength(malformed, "utf-8"));
484
+ assert.ok(prefix.length <= 240, `prefix should be bounded, got ${prefix.length}`);
485
+ assert.doesNotMatch(prefix, /[\u0000-\u001f\u007f-\u009f]/);
486
+ assert.doesNotMatch(prefix, new RegExp(secret));
487
+ assert.doesNotMatch(prefix, new RegExp(promptText));
488
+ assert.match(prefix, /\[REDACTED\]/);
489
+ } finally {
490
+ await rm(cwd, { recursive: true, force: true });
491
+ }
492
+ });
493
+
494
+ it("emits Stop-schema-safe block JSON when malformed stdin still identifies Stop", () => {
495
+ const stdout = runNativeHookCli('{hook_event_name:"Stop",');
496
+
399
497
  const output = parseSingleJsonStdout(stdout) as {
400
498
  decision?: string;
401
499
  reason?: string;
402
- hookSpecificOutput?: { hookEventName?: string; additionalContext?: string };
500
+ stopReason?: string;
501
+ systemMessage?: string;
502
+ hookSpecificOutput?: unknown;
403
503
  };
404
504
 
405
505
  assert.equal(output.decision, "block");
@@ -407,9 +507,10 @@ describe("codex native hook dispatch", () => {
407
507
  output.reason,
408
508
  "OMX native hook received malformed JSON input. Preserve runtime state, inspect the emitting hook payload yourself, and retry with valid JSON.",
409
509
  );
410
- assert.equal(output.hookSpecificOutput?.hookEventName, "Unknown");
510
+ assert.equal(output.stopReason, "native_hook_stdin_parse_error");
511
+ assert.equal(output.hookSpecificOutput, undefined);
411
512
  assert.match(
412
- String(output.hookSpecificOutput?.additionalContext ?? ""),
513
+ String(output.systemMessage ?? ""),
413
514
  /stdin JSON parsing failed inside codex-native-hook:/,
414
515
  );
415
516
  });
@@ -427,6 +528,125 @@ describe("codex native hook dispatch", () => {
427
528
  const output = parseSingleJsonStdout(stdout);
428
529
 
429
530
  assert.deepEqual(output, {});
531
+ assert.equal(existsSync(join(cwd, ".omx", "state")), false);
532
+ } finally {
533
+ await rm(cwd, { recursive: true, force: true });
534
+ }
535
+ });
536
+
537
+ it("returns empty JSON for oversized Stop stdin without parsing or creating inactive state", async () => {
538
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-"));
539
+ try {
540
+ const oversizedStop = JSON.stringify({
541
+ hook_event_name: "Stop",
542
+ cwd,
543
+ session_id: "sess-cli-stop-oversized",
544
+ transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
545
+ });
546
+
547
+ const stdout = runNativeHookCli(oversizedStop, { cwd });
548
+ assert.deepEqual(parseSingleJsonStdout(stdout), {});
549
+ assert.equal(existsSync(join(cwd, ".omx", "state")), false);
550
+ } finally {
551
+ await rm(cwd, { recursive: true, force: true });
552
+ }
553
+ });
554
+
555
+ it("blocks oversized Stop stdin when current session autopilot is active", async () => {
556
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-active-"));
557
+ try {
558
+ await writeActiveAutopilotSession(cwd, "sess-cli-stop-oversized-active");
559
+ const oversizedStop = JSON.stringify({
560
+ hook_event_name: "Stop",
561
+ cwd,
562
+ session_id: "native-session-hidden-by-oversized-payload",
563
+ transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
564
+ });
565
+
566
+ const output = parseSingleJsonStdout(runNativeHookCli(oversizedStop, { cwd })) as {
567
+ decision?: string;
568
+ stopReason?: string;
569
+ systemMessage?: string;
570
+ };
571
+ assert.equal(output.decision, "block");
572
+ assert.equal(output.stopReason, "native_stop_stdin_oversized_active_workflow");
573
+ assert.match(String(output.systemMessage ?? ""), /active current-session workflow state/);
574
+ assert.equal(existsSync(join(cwd, ".omx", "logs")), false);
575
+ } finally {
576
+ await rm(cwd, { recursive: true, force: true });
577
+ }
578
+ });
579
+
580
+ it("does not block oversized Stop stdin for unrelated root autopilot state", async () => {
581
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-stale-root-"));
582
+ try {
583
+ await writeJson(join(cwd, ".omx", "state", "session.json"), {
584
+ session_id: "sess-current-without-active-autopilot",
585
+ cwd,
586
+ });
587
+ await writeJson(join(cwd, ".omx", "state", "autopilot-state.json"), {
588
+ active: true,
589
+ current_phase: "execution",
590
+ });
591
+ const oversizedStop = JSON.stringify({
592
+ hook_event_name: "Stop",
593
+ cwd,
594
+ transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
595
+ });
596
+
597
+ assert.deepEqual(parseSingleJsonStdout(runNativeHookCli(oversizedStop, { cwd })), {});
598
+ assert.equal(existsSync(join(cwd, ".omx", "logs")), false);
599
+ } finally {
600
+ await rm(cwd, { recursive: true, force: true });
601
+ }
602
+ });
603
+
604
+ it("does not block oversized Stop stdin when terminal run-state shadows stale autopilot state", async () => {
605
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-stop-oversized-terminal-run-"));
606
+ try {
607
+ const sessionId = "sess-cli-stop-oversized-terminal-run";
608
+ await writeActiveAutopilotSession(cwd, sessionId);
609
+ await writeJson(join(cwd, ".omx", "state", "sessions", sessionId, "run-state.json"), {
610
+ version: 1,
611
+ active: false,
612
+ mode: "autopilot",
613
+ outcome: "finish",
614
+ lifecycle_outcome: "finished",
615
+ current_phase: "complete",
616
+ completed_at: "2026-05-20T11:00:00.000Z",
617
+ updated_at: "2026-05-20T11:00:00.000Z",
618
+ });
619
+ const oversizedStop = JSON.stringify({
620
+ hook_event_name: "Stop",
621
+ cwd,
622
+ transcript: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
623
+ });
624
+
625
+ assert.deepEqual(parseSingleJsonStdout(runNativeHookCli(oversizedStop, { cwd })), {});
626
+ assert.equal(existsSync(join(cwd, ".omx", "logs")), false);
627
+ } finally {
628
+ await rm(cwd, { recursive: true, force: true });
629
+ }
630
+ });
631
+
632
+ it("fails closed for oversized non-Stop stdin before parsing", async () => {
633
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-nonstop-oversized-"));
634
+ try {
635
+ const oversizedPrompt = JSON.stringify({
636
+ hook_event_name: "UserPromptSubmit",
637
+ cwd,
638
+ session_id: "sess-cli-prompt-oversized",
639
+ prompt: "x".repeat(MAX_NATIVE_STDIN_JSON_BYTES + 1),
640
+ });
641
+
642
+ const output = parseSingleJsonStdout(runNativeHookCli(oversizedPrompt, { cwd })) as {
643
+ continue?: boolean;
644
+ stopReason?: string;
645
+ systemMessage?: string;
646
+ };
647
+ assert.equal(output.continue, false);
648
+ assert.equal(output.stopReason, "native_hook_stdin_oversized");
649
+ assert.match(String(output.systemMessage ?? ""), /rejected oversized stdin JSON before parsing/);
430
650
  } finally {
431
651
  await rm(cwd, { recursive: true, force: true });
432
652
  }
@@ -1161,6 +1381,66 @@ describe("codex native hook dispatch", () => {
1161
1381
  }
1162
1382
  });
1163
1383
 
1384
+ it("keeps a self-parented native role thread as subagent evidence", async () => {
1385
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-self-parented-subagent-"));
1386
+ try {
1387
+ const stateDir = join(cwd, ".omx", "state");
1388
+ const canonicalSessionId = "omx-autopilot-session";
1389
+ const nativeRoleThreadId = "codex-architect-thread";
1390
+ await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
1391
+ await writeSessionStart(cwd, canonicalSessionId, {
1392
+ nativeSessionId: nativeRoleThreadId,
1393
+ });
1394
+
1395
+ const transcriptPath = join(cwd, "architect-subagent-rollout.jsonl");
1396
+ await writeFile(
1397
+ transcriptPath,
1398
+ `${JSON.stringify({
1399
+ type: "session_meta",
1400
+ payload: {
1401
+ id: nativeRoleThreadId,
1402
+ source: {
1403
+ subagent: {
1404
+ thread_spawn: {
1405
+ parent_thread_id: nativeRoleThreadId,
1406
+ depth: 1,
1407
+ agent_nickname: "Architect",
1408
+ agent_role: "architect",
1409
+ },
1410
+ },
1411
+ },
1412
+ agent_nickname: "Architect",
1413
+ agent_role: "architect",
1414
+ },
1415
+ })}\n`,
1416
+ );
1417
+
1418
+ await dispatchCodexNativeHook(
1419
+ {
1420
+ hook_event_name: "SessionStart",
1421
+ cwd,
1422
+ session_id: nativeRoleThreadId,
1423
+ transcript_path: transcriptPath,
1424
+ },
1425
+ { cwd, sessionOwnerPid: process.pid },
1426
+ );
1427
+
1428
+ const tracking = JSON.parse(
1429
+ await readFile(join(stateDir, "subagent-tracking.json"), "utf-8"),
1430
+ ) as {
1431
+ sessions?: Record<string, {
1432
+ leader_thread_id?: string;
1433
+ threads?: Record<string, { kind?: string; mode?: string }>;
1434
+ }>;
1435
+ };
1436
+ assert.equal(tracking.sessions?.[canonicalSessionId]?.leader_thread_id, undefined);
1437
+ assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[nativeRoleThreadId]?.kind, "subagent");
1438
+ assert.equal(tracking.sessions?.[canonicalSessionId]?.threads?.[nativeRoleThreadId]?.mode, "architect");
1439
+ } finally {
1440
+ await rm(cwd, { recursive: true, force: true });
1441
+ }
1442
+ });
1443
+
1164
1444
  it("does not attach a subagent SessionStart to an unrelated canonical leader", async () => {
1165
1445
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-session-start-mismatch-"));
1166
1446
  try {
@@ -1289,6 +1569,109 @@ describe("codex native hook dispatch", () => {
1289
1569
  }
1290
1570
  });
1291
1571
 
1572
+ it("prefers the OMX owner session id when a native new session revives HUD", async () => {
1573
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-owner-session-revive-"));
1574
+ try {
1575
+ const stateDir = join(cwd, ".omx", "state");
1576
+ const ownerSessionId = "omx-launch-owner-hud";
1577
+ const oldNativeSessionId = "codex-native-hud-old";
1578
+ const nativeSessionId = "codex-native-hud-new";
1579
+ await mkdir(stateDir, { recursive: true });
1580
+ await writeSessionStart(cwd, ownerSessionId, {
1581
+ nativeSessionId: oldNativeSessionId,
1582
+ pid: process.pid,
1583
+ });
1584
+ await dispatchCodexNativeHook(
1585
+ {
1586
+ hook_event_name: "SessionStart",
1587
+ cwd,
1588
+ session_id: nativeSessionId,
1589
+ },
1590
+ {
1591
+ cwd,
1592
+ sessionOwnerPid: process.pid,
1593
+ },
1594
+ );
1595
+
1596
+ const sessionState = JSON.parse(await readFile(join(stateDir, "session.json"), "utf-8")) as {
1597
+ session_id?: string;
1598
+ native_session_id?: string;
1599
+ previous_native_session_id?: string;
1600
+ owner_omx_session_id?: string;
1601
+ };
1602
+ assert.equal(sessionState.session_id, nativeSessionId);
1603
+ assert.equal(sessionState.native_session_id, nativeSessionId);
1604
+ assert.equal(sessionState.previous_native_session_id, oldNativeSessionId);
1605
+ assert.equal(sessionState.owner_omx_session_id, ownerSessionId);
1606
+
1607
+ let reconcileCall: { cwd: string; sessionId?: string } | null = null;
1608
+ const promptResult = await dispatchCodexNativeHook(
1609
+ {
1610
+ hook_event_name: "UserPromptSubmit",
1611
+ cwd,
1612
+ session_id: nativeSessionId,
1613
+ thread_id: "thread-hud-owner",
1614
+ turn_id: "turn-hud-owner",
1615
+ prompt: "$ralplan fix native new hud owner handoff",
1616
+ },
1617
+ {
1618
+ cwd,
1619
+ reconcileHudForPromptSubmitFn: async (hookCwd, deps = {}) => {
1620
+ reconcileCall = { cwd: hookCwd, sessionId: deps.sessionId };
1621
+ return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
1622
+ },
1623
+ },
1624
+ );
1625
+
1626
+ assert.equal(promptResult.omxEventName, "keyword-detector");
1627
+ assert.deepEqual(reconcileCall, { cwd, sessionId: ownerSessionId });
1628
+ } finally {
1629
+ await rm(cwd, { recursive: true, force: true });
1630
+ }
1631
+ });
1632
+
1633
+ it("falls back to the canonical session id for malformed HUD owner ids", async () => {
1634
+ for (const [index, invalidOwnerSessionId] of ["codex-native-hud-owner", "omx-../../stale"].entries()) {
1635
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-invalid-owner-revive-"));
1636
+ try {
1637
+ const stateDir = join(cwd, ".omx", "state");
1638
+ const canonicalSessionId = "omx-launch-hud-safe";
1639
+ const nativeSessionId = "codex-native-hud-safe";
1640
+ await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
1641
+ await writeSessionStart(cwd, canonicalSessionId);
1642
+
1643
+ const sessionStatePath = join(stateDir, "session.json");
1644
+ const sessionState = JSON.parse(await readFile(sessionStatePath, "utf-8")) as Record<string, unknown>;
1645
+ sessionState.owner_omx_session_id = invalidOwnerSessionId;
1646
+ await writeJson(sessionStatePath, sessionState);
1647
+
1648
+ let reconcileCall: { cwd: string; sessionId?: string } | null = null;
1649
+ const promptResult = await dispatchCodexNativeHook(
1650
+ {
1651
+ hook_event_name: "UserPromptSubmit",
1652
+ cwd,
1653
+ session_id: nativeSessionId,
1654
+ thread_id: `thread-hud-invalid-owner-${index}`,
1655
+ turn_id: "turn-hud-invalid-owner",
1656
+ prompt: "$ralplan fix malformed hud owner handoff",
1657
+ },
1658
+ {
1659
+ cwd,
1660
+ reconcileHudForPromptSubmitFn: async (hookCwd, deps = {}) => {
1661
+ reconcileCall = { cwd: hookCwd, sessionId: deps.sessionId };
1662
+ return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
1663
+ },
1664
+ },
1665
+ );
1666
+
1667
+ assert.equal(promptResult.omxEventName, "keyword-detector");
1668
+ assert.deepEqual(reconcileCall, { cwd, sessionId: canonicalSessionId });
1669
+ } finally {
1670
+ await rm(cwd, { recursive: true, force: true });
1671
+ }
1672
+ }
1673
+ });
1674
+
1292
1675
  it("passes the canonical OMX session id when UserPromptSubmit revives HUD", async () => {
1293
1676
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-session-revive-"));
1294
1677
  try {
@@ -2676,30 +3059,373 @@ standardMaxRounds = 15
2676
3059
  }
2677
3060
  });
2678
3061
 
2679
- it("records plugin-prefixed keyword activation from UserPromptSubmit payloads", async () => {
2680
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-plugin-prefixed-"));
3062
+ it("does not treat a corrupt leader kind=subagent tracker entry as native subagent prompt scope", async () => {
3063
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-corrupt-leader-subagent-"));
2681
3064
  try {
2682
- await mkdir(join(cwd, ".omx", "state"), { recursive: true });
3065
+ const stateDir = join(cwd, ".omx", "state");
3066
+ const canonicalSessionId = "sess-corrupt-leader";
3067
+ const leaderNativeSessionId = "native-corrupt-leader";
3068
+ const nowIso = new Date().toISOString();
3069
+
3070
+ await writeJson(join(stateDir, "session.json"), {
3071
+ session_id: canonicalSessionId,
3072
+ native_session_id: leaderNativeSessionId,
3073
+ });
3074
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
3075
+ schemaVersion: 1,
3076
+ sessions: {
3077
+ [canonicalSessionId]: {
3078
+ session_id: canonicalSessionId,
3079
+ leader_thread_id: leaderNativeSessionId,
3080
+ updated_at: nowIso,
3081
+ threads: {
3082
+ [leaderNativeSessionId]: {
3083
+ thread_id: leaderNativeSessionId,
3084
+ kind: "subagent",
3085
+ first_seen_at: nowIso,
3086
+ last_seen_at: nowIso,
3087
+ turn_count: 2,
3088
+ },
3089
+ },
3090
+ },
3091
+ },
3092
+ });
3093
+
2683
3094
  const result = await dispatchCodexNativeHook(
2684
3095
  {
2685
3096
  hook_event_name: "UserPromptSubmit",
2686
3097
  cwd,
2687
- session_id: "sess-plugin-1",
2688
- thread_id: "thread-plugin-1",
2689
- turn_id: "turn-plugin-1",
2690
- prompt: "$oh-my-codex:ralplan implement issue #1307",
3098
+ session_id: leaderNativeSessionId,
3099
+ thread_id: leaderNativeSessionId,
3100
+ turn_id: "turn-corrupt-leader",
3101
+ prompt: "$autopilot continue this review blocker fix",
2691
3102
  },
2692
3103
  { cwd },
2693
3104
  );
2694
3105
 
2695
3106
  assert.equal(result.omxEventName, "keyword-detector");
2696
- assert.equal(result.skillState?.skill, "ralplan");
2697
- const message = String(
2698
- (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
3107
+ assert.equal(result.skillState?.skill, "autopilot");
3108
+ assert.equal(
3109
+ existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")),
3110
+ true,
2699
3111
  );
2700
- assert.match(message, /\$oh-my-codex:ralplan" -> ralplan/);
2701
- assert.match(message, /use CLI-first state updates via `omx state write\/read\/clear --input '<json>' --json`/);
2702
- assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-plugin-1", "ralplan-state.json")), true);
3112
+ } finally {
3113
+ await rm(cwd, { recursive: true, force: true });
3114
+ }
3115
+ });
3116
+
3117
+ it("lets the current canonical leader boundary beat stale global subagent tracking with a distinct prompt thread id", async () => {
3118
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-current-leader-stale-global-"));
3119
+ try {
3120
+ const stateDir = join(cwd, ".omx", "state");
3121
+ const canonicalSessionId = "sess-current-leader";
3122
+ const leaderNativeSessionId = "native-current-leader";
3123
+ const staleSessionId = "sess-stale-subagent";
3124
+ const staleLeaderNativeSessionId = "native-stale-leader";
3125
+ const nowIso = new Date().toISOString();
3126
+
3127
+ await writeJson(join(stateDir, "session.json"), {
3128
+ session_id: canonicalSessionId,
3129
+ native_session_id: leaderNativeSessionId,
3130
+ });
3131
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
3132
+ schemaVersion: 1,
3133
+ sessions: {
3134
+ [canonicalSessionId]: {
3135
+ session_id: canonicalSessionId,
3136
+ leader_thread_id: leaderNativeSessionId,
3137
+ updated_at: nowIso,
3138
+ threads: {
3139
+ [leaderNativeSessionId]: {
3140
+ thread_id: leaderNativeSessionId,
3141
+ kind: "leader",
3142
+ first_seen_at: nowIso,
3143
+ last_seen_at: nowIso,
3144
+ turn_count: 1,
3145
+ },
3146
+ },
3147
+ },
3148
+ [staleSessionId]: {
3149
+ session_id: staleSessionId,
3150
+ leader_thread_id: staleLeaderNativeSessionId,
3151
+ updated_at: nowIso,
3152
+ threads: {
3153
+ [staleLeaderNativeSessionId]: {
3154
+ thread_id: staleLeaderNativeSessionId,
3155
+ kind: "leader",
3156
+ first_seen_at: nowIso,
3157
+ last_seen_at: nowIso,
3158
+ turn_count: 1,
3159
+ },
3160
+ [leaderNativeSessionId]: {
3161
+ thread_id: leaderNativeSessionId,
3162
+ kind: "subagent",
3163
+ first_seen_at: nowIso,
3164
+ last_seen_at: nowIso,
3165
+ turn_count: 1,
3166
+ mode: "architect",
3167
+ },
3168
+ },
3169
+ },
3170
+ },
3171
+ });
3172
+
3173
+ const result = await dispatchCodexNativeHook(
3174
+ {
3175
+ hook_event_name: "UserPromptSubmit",
3176
+ cwd,
3177
+ session_id: leaderNativeSessionId,
3178
+ thread_id: "thread-current-turn-not-native-session",
3179
+ turn_id: "turn-current-leader",
3180
+ prompt: "$autopilot continue",
3181
+ },
3182
+ { cwd },
3183
+ );
3184
+
3185
+ assert.equal(result.omxEventName, "keyword-detector");
3186
+ assert.equal(result.skillState?.skill, "autopilot");
3187
+ assert.equal(
3188
+ existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")),
3189
+ true,
3190
+ );
3191
+ assert.equal(
3192
+ existsSync(join(stateDir, "sessions", staleSessionId, "autopilot-state.json")),
3193
+ false,
3194
+ );
3195
+ } finally {
3196
+ await rm(cwd, { recursive: true, force: true });
3197
+ }
3198
+ });
3199
+
3200
+ it("lets the current session native leader beat stale global subagent tracking without a canonical summary and with a distinct prompt thread id", async () => {
3201
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-current-native-leader-stale-global-"));
3202
+ try {
3203
+ const stateDir = join(cwd, ".omx", "state");
3204
+ const canonicalSessionId = "sess-current-native-leader";
3205
+ const leaderNativeSessionId = "native-current-leader-no-summary";
3206
+ const staleSessionId = "sess-stale-native-subagent";
3207
+ const staleLeaderNativeSessionId = "native-stale-parent";
3208
+ const nowIso = new Date().toISOString();
3209
+
3210
+ await writeJson(join(stateDir, "session.json"), {
3211
+ session_id: canonicalSessionId,
3212
+ native_session_id: leaderNativeSessionId,
3213
+ });
3214
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
3215
+ schemaVersion: 1,
3216
+ sessions: {
3217
+ [staleSessionId]: {
3218
+ session_id: staleSessionId,
3219
+ leader_thread_id: staleLeaderNativeSessionId,
3220
+ updated_at: nowIso,
3221
+ threads: {
3222
+ [staleLeaderNativeSessionId]: {
3223
+ thread_id: staleLeaderNativeSessionId,
3224
+ kind: "leader",
3225
+ first_seen_at: nowIso,
3226
+ last_seen_at: nowIso,
3227
+ turn_count: 1,
3228
+ },
3229
+ [leaderNativeSessionId]: {
3230
+ thread_id: leaderNativeSessionId,
3231
+ kind: "subagent",
3232
+ first_seen_at: nowIso,
3233
+ last_seen_at: nowIso,
3234
+ turn_count: 1,
3235
+ mode: "critic",
3236
+ },
3237
+ },
3238
+ },
3239
+ },
3240
+ });
3241
+
3242
+ const result = await dispatchCodexNativeHook(
3243
+ {
3244
+ hook_event_name: "UserPromptSubmit",
3245
+ cwd,
3246
+ session_id: leaderNativeSessionId,
3247
+ thread_id: "thread-current-turn-not-native-session",
3248
+ turn_id: "turn-current-native-leader",
3249
+ prompt: "$autopilot continue",
3250
+ },
3251
+ { cwd },
3252
+ );
3253
+
3254
+ assert.equal(result.omxEventName, "keyword-detector");
3255
+ assert.equal(result.skillState?.skill, "autopilot");
3256
+ assert.equal(
3257
+ existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")),
3258
+ true,
3259
+ );
3260
+ assert.equal(
3261
+ existsSync(join(stateDir, "sessions", staleSessionId, "autopilot-state.json")),
3262
+ false,
3263
+ );
3264
+ } finally {
3265
+ await rm(cwd, { recursive: true, force: true });
3266
+ }
3267
+ });
3268
+
3269
+ it("lets the current session native leader beat a malformed canonical subagent entry", async () => {
3270
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-current-native-leader-malformed-canonical-"));
3271
+ try {
3272
+ const stateDir = join(cwd, ".omx", "state");
3273
+ const canonicalSessionId = "sess-current-native-leader-malformed";
3274
+ const leaderNativeSessionId = "native-current-leader-malformed";
3275
+ const nowIso = new Date().toISOString();
3276
+
3277
+ await writeJson(join(stateDir, "session.json"), {
3278
+ session_id: canonicalSessionId,
3279
+ native_session_id: leaderNativeSessionId,
3280
+ });
3281
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
3282
+ schemaVersion: 1,
3283
+ sessions: {
3284
+ [canonicalSessionId]: {
3285
+ session_id: canonicalSessionId,
3286
+ updated_at: nowIso,
3287
+ threads: {
3288
+ [leaderNativeSessionId]: {
3289
+ thread_id: leaderNativeSessionId,
3290
+ kind: "subagent",
3291
+ first_seen_at: nowIso,
3292
+ last_seen_at: nowIso,
3293
+ turn_count: 1,
3294
+ mode: "architect",
3295
+ },
3296
+ },
3297
+ },
3298
+ },
3299
+ });
3300
+
3301
+ const result = await dispatchCodexNativeHook(
3302
+ {
3303
+ hook_event_name: "UserPromptSubmit",
3304
+ cwd,
3305
+ session_id: leaderNativeSessionId,
3306
+ thread_id: leaderNativeSessionId,
3307
+ turn_id: "turn-current-native-leader-malformed",
3308
+ prompt: "$autopilot continue",
3309
+ },
3310
+ { cwd },
3311
+ );
3312
+
3313
+ assert.equal(result.omxEventName, "keyword-detector");
3314
+ assert.equal(result.skillState?.skill, "autopilot");
3315
+ assert.equal(
3316
+ existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")),
3317
+ true,
3318
+ );
3319
+ } finally {
3320
+ await rm(cwd, { recursive: true, force: true });
3321
+ }
3322
+ });
3323
+
3324
+ it("still treats mixed child and leader payload identities as native subagent scope", async () => {
3325
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-mixed-child-leader-identity-"));
3326
+ try {
3327
+ const stateDir = join(cwd, ".omx", "state");
3328
+ const canonicalSessionId = "sess-mixed-child-leader";
3329
+ const leaderNativeSessionId = "native-mixed-leader";
3330
+ const childNativeSessionId = "native-mixed-child";
3331
+ const nowIso = new Date().toISOString();
3332
+
3333
+ await writeJson(join(stateDir, "session.json"), {
3334
+ session_id: canonicalSessionId,
3335
+ native_session_id: leaderNativeSessionId,
3336
+ });
3337
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
3338
+ schemaVersion: 1,
3339
+ sessions: {
3340
+ [canonicalSessionId]: {
3341
+ session_id: canonicalSessionId,
3342
+ leader_thread_id: leaderNativeSessionId,
3343
+ updated_at: nowIso,
3344
+ threads: {
3345
+ [leaderNativeSessionId]: {
3346
+ thread_id: leaderNativeSessionId,
3347
+ kind: "leader",
3348
+ first_seen_at: nowIso,
3349
+ last_seen_at: nowIso,
3350
+ turn_count: 1,
3351
+ },
3352
+ [childNativeSessionId]: {
3353
+ thread_id: childNativeSessionId,
3354
+ kind: "subagent",
3355
+ first_seen_at: nowIso,
3356
+ last_seen_at: nowIso,
3357
+ turn_count: 1,
3358
+ mode: "critic",
3359
+ },
3360
+ },
3361
+ },
3362
+ },
3363
+ });
3364
+
3365
+ const result = await dispatchCodexNativeHook(
3366
+ {
3367
+ hook_event_name: "UserPromptSubmit",
3368
+ cwd,
3369
+ session_id: childNativeSessionId,
3370
+ thread_id: leaderNativeSessionId,
3371
+ turn_id: "turn-mixed-child-leader",
3372
+ prompt: "$ralplan review this as delegated text",
3373
+ },
3374
+ { cwd },
3375
+ );
3376
+
3377
+ assert.equal(result.omxEventName, "keyword-detector");
3378
+ assert.equal(result.skillState, null);
3379
+ assert.equal(result.outputJson, null);
3380
+ assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "ralplan-state.json")), false);
3381
+ assert.equal(existsSync(join(stateDir, "sessions", childNativeSessionId, "ralplan-state.json")), false);
3382
+
3383
+ const reversedResult = await dispatchCodexNativeHook(
3384
+ {
3385
+ hook_event_name: "UserPromptSubmit",
3386
+ cwd,
3387
+ session_id: leaderNativeSessionId,
3388
+ thread_id: childNativeSessionId,
3389
+ turn_id: "turn-mixed-leader-child",
3390
+ prompt: "$autopilot review this as delegated text",
3391
+ },
3392
+ { cwd },
3393
+ );
3394
+
3395
+ assert.equal(reversedResult.omxEventName, "keyword-detector");
3396
+ assert.equal(reversedResult.skillState, null);
3397
+ assert.equal(reversedResult.outputJson, null);
3398
+ assert.equal(existsSync(join(stateDir, "sessions", canonicalSessionId, "autopilot-state.json")), false);
3399
+ assert.equal(existsSync(join(stateDir, "sessions", childNativeSessionId, "autopilot-state.json")), false);
3400
+ } finally {
3401
+ await rm(cwd, { recursive: true, force: true });
3402
+ }
3403
+ });
3404
+
3405
+ it("records plugin-prefixed keyword activation from UserPromptSubmit payloads", async () => {
3406
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-plugin-prefixed-"));
3407
+ try {
3408
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
3409
+ const result = await dispatchCodexNativeHook(
3410
+ {
3411
+ hook_event_name: "UserPromptSubmit",
3412
+ cwd,
3413
+ session_id: "sess-plugin-1",
3414
+ thread_id: "thread-plugin-1",
3415
+ turn_id: "turn-plugin-1",
3416
+ prompt: "$oh-my-codex:ralplan implement issue #1307",
3417
+ },
3418
+ { cwd },
3419
+ );
3420
+
3421
+ assert.equal(result.omxEventName, "keyword-detector");
3422
+ assert.equal(result.skillState?.skill, "ralplan");
3423
+ const message = String(
3424
+ (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
3425
+ );
3426
+ assert.match(message, /\$oh-my-codex:ralplan" -> ralplan/);
3427
+ assert.match(message, /use CLI-first state updates via `omx state write\/read\/clear --input '<json>' --json`/);
3428
+ assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", "sess-plugin-1", "ralplan-state.json")), true);
2703
3429
  } finally {
2704
3430
  await rm(cwd, { recursive: true, force: true });
2705
3431
  }
@@ -2728,6 +3454,10 @@ standardMaxRounds = 15
2728
3454
  );
2729
3455
  assert.match(message, /Autopilot protocol:/);
2730
3456
  assert.match(message, /deep-interview -> ralplan -> ultragoal -> code-review -> ultraqa/);
3457
+ assert.match(message, /structured question chain, not a one-question gate/);
3458
+ assert.match(message, /re-score ambiguity against the active threshold/);
3459
+ assert.match(message, /max_rounds as a cap/);
3460
+ assert.match(message, /Do not advance from deep-interview to ralplan merely because the first question was answered/);
2731
3461
  assert.match(message, /Planner output has been reviewed sequentially by Architect and then Critic/);
2732
3462
  assert.match(message, /do not hand off to Ultragoal or implementation until .*ralplan_architect_review.*ralplan_critic_review/);
2733
3463
  } finally {
@@ -3280,6 +4010,11 @@ ${JSON.stringify({
3280
4010
  (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
3281
4011
  );
3282
4012
  assert.match(message, /"keep going" -> ralph/);
4013
+ assert.match(message, /Autopilot protocol:/);
4014
+ assert.match(message, /structured question chain, not a one-question gate/);
4015
+ assert.match(message, /re-score ambiguity against the active threshold/);
4016
+ assert.match(message, /max_rounds as a cap/);
4017
+ assert.match(message, /Do not advance from deep-interview to ralplan merely because the first question was answered/);
3283
4018
  assert.doesNotMatch(message, /denied workflow keyword/i);
3284
4019
  assert.doesNotMatch(message, /Unsupported workflow overlap: autopilot \+ ralph\./);
3285
4020
  assert.doesNotMatch(message, /Prompt-side `\$ralph` activation/);
@@ -3289,6 +4024,122 @@ ${JSON.stringify({
3289
4024
  }
3290
4025
  });
3291
4026
 
4027
+
4028
+ it("keeps omx question answers on the active autopilot skill so the interview chain guidance is injected", async () => {
4029
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-question-answer-continuation-"));
4030
+ try {
4031
+ const sessionId = "sess-autopilot-question-answer";
4032
+ const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
4033
+ await mkdir(sessionDir, { recursive: true });
4034
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
4035
+ version: 1,
4036
+ active: true,
4037
+ skill: "autopilot",
4038
+ keyword: "$autopilot",
4039
+ phase: "deep-interview",
4040
+ initialized_mode: "autopilot",
4041
+ initialized_state_path: `.omx/state/sessions/${sessionId}/autopilot-state.json`,
4042
+ session_id: sessionId,
4043
+ active_skills: [
4044
+ { skill: "autopilot", phase: "deep-interview", active: true, session_id: sessionId },
4045
+ ],
4046
+ });
4047
+ await writeJson(join(sessionDir, "autopilot-state.json"), {
4048
+ active: true,
4049
+ mode: "autopilot",
4050
+ current_phase: "deep-interview",
4051
+ started_at: "2026-04-19T00:00:00.000Z",
4052
+ updated_at: "2026-04-19T00:10:00.000Z",
4053
+ session_id: sessionId,
4054
+ });
4055
+
4056
+ const result = await dispatchCodexNativeHook(
4057
+ {
4058
+ hook_event_name: "UserPromptSubmit",
4059
+ cwd,
4060
+ session_id: sessionId,
4061
+ thread_id: "thread-autopilot-question-answer",
4062
+ turn_id: "turn-autopilot-question-answer",
4063
+ prompt: "[omx question answered] semantic_marker_expansion $ralplan",
4064
+ },
4065
+ { cwd },
4066
+ );
4067
+
4068
+ assert.equal(result.omxEventName, "keyword-detector");
4069
+ assert.equal(result.skillState?.skill, "autopilot");
4070
+ const message = String(
4071
+ (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
4072
+ );
4073
+ assert.match(message, /continued active workflow skill "autopilot"/);
4074
+ assert.match(message, /Autopilot protocol:/);
4075
+ assert.match(message, /structured question chain, not a one-question gate/);
4076
+ assert.match(message, /This turn is a marked omx question answer/);
4077
+ assert.match(message, /then re-score/);
4078
+ assert.match(message, /write interview_complete evidence and hand off/);
4079
+ assert.match(message, /readiness gate remains unresolved and the answer would materially change execution/);
4080
+ assert.match(message, /Do not advance from deep-interview to ralplan merely because the first question was answered/);
4081
+ assert.doesNotMatch(message, /denied workflow keyword/i);
4082
+ assert.equal(existsSync(join(sessionDir, "ralplan-state.json")), false);
4083
+ } finally {
4084
+ await rm(cwd, { recursive: true, force: true });
4085
+ }
4086
+ });
4087
+
4088
+ it("keeps deep-interview bridge guidance on marked question answers with workflow-like tokens", async () => {
4089
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-question-answer-continuation-"));
4090
+ try {
4091
+ const sessionId = "sess-deep-interview-question-answer";
4092
+ const sessionDir = join(cwd, ".omx", "state", "sessions", sessionId);
4093
+ await mkdir(sessionDir, { recursive: true });
4094
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
4095
+ version: 1,
4096
+ active: true,
4097
+ skill: "deep-interview",
4098
+ keyword: "$deep-interview",
4099
+ phase: "planning",
4100
+ initialized_mode: "deep-interview",
4101
+ initialized_state_path: `.omx/state/sessions/${sessionId}/deep-interview-state.json`,
4102
+ session_id: sessionId,
4103
+ active_skills: [
4104
+ { skill: "deep-interview", phase: "planning", active: true, session_id: sessionId },
4105
+ ],
4106
+ });
4107
+ await writeJson(join(sessionDir, "deep-interview-state.json"), {
4108
+ active: true,
4109
+ mode: "deep-interview",
4110
+ current_phase: "intent-first",
4111
+ started_at: "2026-04-21T10:00:00.000Z",
4112
+ updated_at: "2026-04-21T10:00:00.000Z",
4113
+ });
4114
+
4115
+ const result = await dispatchCodexNativeHook(
4116
+ {
4117
+ hook_event_name: "UserPromptSubmit",
4118
+ cwd,
4119
+ session_id: sessionId,
4120
+ thread_id: "thread-deep-interview-question-answer",
4121
+ turn_id: "turn-deep-interview-question-answer",
4122
+ prompt: "[omx question answered] answer text $ralplan",
4123
+ },
4124
+ { cwd },
4125
+ );
4126
+
4127
+ assert.equal(result.omxEventName, "keyword-detector");
4128
+ assert.equal(result.skillState?.skill, "deep-interview");
4129
+ const message = String(
4130
+ (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext || "",
4131
+ );
4132
+ assert.match(message, /continued active workflow skill "deep-interview"/);
4133
+ assert.match(message, /workflow-like tokens inside the marked omx question answer are treated as answer text/);
4134
+ assert.match(message, /Deep-interview is active, but this session is not attached to tmux/);
4135
+ assert.match(message, /native structured question tool when available/);
4136
+ assert.doesNotMatch(message, /detected workflow keyword "\$ralplan" -> ralplan/);
4137
+ assert.equal(existsSync(join(sessionDir, "ralplan-state.json")), false);
4138
+ } finally {
4139
+ await rm(cwd, { recursive: true, force: true });
4140
+ }
4141
+ });
4142
+
3292
4143
  it("clarifies outside-tmux prompt-side deep-interview activation without pretending omx question is directly available", async () => {
3293
4144
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-deep-interview-routing-"));
3294
4145
  try {
@@ -3810,6 +4661,10 @@ export async function onHookEvent(event) {
3810
4661
  active: true,
3811
4662
  mode: "deep-interview",
3812
4663
  current_phase: "intent-first",
4664
+ deep_interview_gate: {
4665
+ status: "complete",
4666
+ rationale: "Requirements are clarified and ready for ralplan consensus.",
4667
+ },
3813
4668
  });
3814
4669
  await writeJson(join(sessionDir, "skill-active-state.json"), {
3815
4670
  active: true,
@@ -6936,27 +7791,71 @@ exit 0
6936
7791
  current_phase: "team-exec",
6937
7792
  });
6938
7793
 
6939
- await dispatchCodexNativeHook(
7794
+ await dispatchCodexNativeHook(
7795
+ {
7796
+ hook_event_name: "PostToolUse",
7797
+ cwd,
7798
+ session_id: nativeSessionId,
7799
+ tool_name: "mcp__omx_state__state_write",
7800
+ tool_use_id: "tool-mcp-transport-team-native",
7801
+ tool_input: { mode: "team", active: true },
7802
+ tool_response: "{\"error\":\"MCP transport closed\",\"details\":\"stdio pipe closed before response\"}",
7803
+ },
7804
+ { cwd },
7805
+ );
7806
+
7807
+ const phase = await readTeamPhase("transport-team", cwd);
7808
+ const attention = await readTeamLeaderAttention("transport-team", cwd);
7809
+ assert.equal(phase?.current_phase, "failed");
7810
+ assert.equal(attention?.leader_attention_reason, "mcp_transport_dead");
7811
+ assert.equal(attention?.leader_attention_pending, true);
7812
+ assert.equal(attention?.leader_session_id, canonicalSessionId);
7813
+ } finally {
7814
+ process.chdir(previousCwd);
7815
+ await rm(cwd, { recursive: true, force: true });
7816
+ }
7817
+ });
7818
+
7819
+ it("does not block ordinary non-zero grep output in PostToolUse", async () => {
7820
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-grep-nonzero-"));
7821
+ try {
7822
+ const result = await dispatchCodexNativeHook(
7823
+ {
7824
+ hook_event_name: "PostToolUse",
7825
+ cwd,
7826
+ tool_name: "Bash",
7827
+ tool_use_id: "tool-grep-nonzero",
7828
+ tool_input: { command: "grep -R missing-pattern src | head -20" },
7829
+ tool_response: "{\"exit_code\":1,\"stdout\":\"src/example.ts:TODO\",\"stderr\":\"\"}",
7830
+ },
7831
+ { cwd },
7832
+ );
7833
+
7834
+ assert.equal(result.omxEventName, "post-tool-use");
7835
+ assert.equal(result.outputJson, null);
7836
+ } finally {
7837
+ await rm(cwd, { recursive: true, force: true });
7838
+ }
7839
+ });
7840
+
7841
+ it("does not block ordinary non-zero diagnostic output in PostToolUse", async () => {
7842
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-diagnostic-nonzero-"));
7843
+ try {
7844
+ const result = await dispatchCodexNativeHook(
6940
7845
  {
6941
7846
  hook_event_name: "PostToolUse",
6942
7847
  cwd,
6943
- session_id: nativeSessionId,
6944
- tool_name: "mcp__omx_state__state_write",
6945
- tool_use_id: "tool-mcp-transport-team-native",
6946
- tool_input: { mode: "team", active: true },
6947
- tool_response: "{\"error\":\"MCP transport closed\",\"details\":\"stdio pipe closed before response\"}",
7848
+ tool_name: "Bash",
7849
+ tool_use_id: "tool-diagnostic-nonzero",
7850
+ tool_input: { command: "find src -name nope -print" },
7851
+ tool_response: "{\"exit_code\":1,\"stdout\":\"searched 10 files\",\"stderr\":\"\"}",
6948
7852
  },
6949
7853
  { cwd },
6950
7854
  );
6951
7855
 
6952
- const phase = await readTeamPhase("transport-team", cwd);
6953
- const attention = await readTeamLeaderAttention("transport-team", cwd);
6954
- assert.equal(phase?.current_phase, "failed");
6955
- assert.equal(attention?.leader_attention_reason, "mcp_transport_dead");
6956
- assert.equal(attention?.leader_attention_pending, true);
6957
- assert.equal(attention?.leader_session_id, canonicalSessionId);
7856
+ assert.equal(result.omxEventName, "post-tool-use");
7857
+ assert.equal(result.outputJson, null);
6958
7858
  } finally {
6959
- process.chdir(previousCwd);
6960
7859
  await rm(cwd, { recursive: true, force: true });
6961
7860
  }
6962
7861
  });
@@ -7021,6 +7920,84 @@ exit 0
7021
7920
  }
7022
7921
  });
7023
7922
 
7923
+ it("treats wrapped gh pr checks output as reviewable", async () => {
7924
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-gh-wrapped-"));
7925
+ try {
7926
+ for (const command of [
7927
+ "GH_PAGER=cat gh pr checks",
7928
+ "env GH_TOKEN=ghp_testtoken gh pr checks",
7929
+ "/usr/bin/env gh pr checks",
7930
+ "env -- gh pr checks",
7931
+ "env -C repo gh pr checks",
7932
+ "/usr/bin/gh pr checks",
7933
+ "gh --repo owner/repo pr checks",
7934
+ "echo a; gh pr checks",
7935
+ "cd repo && gh pr checks",
7936
+ ]) {
7937
+ const result = await dispatchCodexNativeHook(
7938
+ {
7939
+ hook_event_name: "PostToolUse",
7940
+ cwd,
7941
+ tool_name: "Bash",
7942
+ tool_use_id: `tool-useful-${command}`,
7943
+ tool_input: { command },
7944
+ tool_response: "{\"exit_code\":8,\"stdout\":\"build pending\",\"stderr\":\"\"}",
7945
+ },
7946
+ { cwd },
7947
+ );
7948
+
7949
+ assert.equal(result.omxEventName, "post-tool-use");
7950
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block", command);
7951
+ }
7952
+ } finally {
7953
+ await rm(cwd, { recursive: true, force: true });
7954
+ }
7955
+ });
7956
+
7957
+ it("does not treat heredoc gh pr checks text as a reviewable command", async () => {
7958
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-gh-heredoc-"));
7959
+ try {
7960
+ const result = await dispatchCodexNativeHook(
7961
+ {
7962
+ hook_event_name: "PostToolUse",
7963
+ cwd,
7964
+ tool_name: "Bash",
7965
+ tool_use_id: "tool-heredoc-gh-checks",
7966
+ tool_input: { command: "cat <<'EOF'\ngh pr checks\nEOF\nfalse" },
7967
+ tool_response: "{\"exit_code\":1,\"stdout\":\"gh pr checks\",\"stderr\":\"\"}",
7968
+ },
7969
+ { cwd },
7970
+ );
7971
+
7972
+ assert.equal(result.omxEventName, "post-tool-use");
7973
+ assert.equal(result.outputJson, null);
7974
+ } finally {
7975
+ await rm(cwd, { recursive: true, force: true });
7976
+ }
7977
+ });
7978
+
7979
+ it("does not treat echoed gh pr checks text as a reviewable command", async () => {
7980
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-gh-echo-"));
7981
+ try {
7982
+ const result = await dispatchCodexNativeHook(
7983
+ {
7984
+ hook_event_name: "PostToolUse",
7985
+ cwd,
7986
+ tool_name: "Bash",
7987
+ tool_use_id: "tool-echo-gh-checks",
7988
+ tool_input: { command: "echo gh pr checks" },
7989
+ tool_response: "{\"exit_code\":1,\"stdout\":\"gh pr checks\",\"stderr\":\"\"}",
7990
+ },
7991
+ { cwd },
7992
+ );
7993
+
7994
+ assert.equal(result.omxEventName, "post-tool-use");
7995
+ assert.equal(result.outputJson, null);
7996
+ } finally {
7997
+ await rm(cwd, { recursive: true, force: true });
7998
+ }
7999
+ });
8000
+
7024
8001
  it("returns MCP transport-death guidance and preserves failed team state", async () => {
7025
8002
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-dead-"));
7026
8003
  try {
@@ -8029,31 +9006,341 @@ exit 0
8029
9006
  process.env.OMX_TEAM_STATE_ROOT = stateDir;
8030
9007
  process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
8031
9008
 
8032
- const result = await dispatchCodexNativeHook(
8033
- {
8034
- hook_event_name: "Stop",
8035
- cwd,
8036
- session_id: "sess-stop-team-worker-busy-leader",
8037
- },
8038
- { cwd },
8039
- );
9009
+ const result = await dispatchCodexNativeHook(
9010
+ {
9011
+ hook_event_name: "Stop",
9012
+ cwd,
9013
+ session_id: "sess-stop-team-worker-busy-leader",
9014
+ },
9015
+ { cwd },
9016
+ );
9017
+
9018
+ assert.equal(result.outputJson, null);
9019
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
9020
+ assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
9021
+ assert.match(tmuxLog, /send-keys -t %42 Tab/);
9022
+ assert.match(tmuxLog, /send-keys -t %42 C-m/);
9023
+ assert.ok(
9024
+ tmuxLog.indexOf("send-keys -t %42 Tab") < tmuxLog.indexOf("send-keys -t %42 C-m"),
9025
+ "busy worker-stop nudge should press Tab before C-m",
9026
+ );
9027
+ const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
9028
+ assert.equal(nudgeState.delivery, "queued");
9029
+ } finally {
9030
+ if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
9031
+ else delete process.env.OMX_TEAM_WORKER;
9032
+ if (typeof prevTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
9033
+ else delete process.env.OMX_TEAM_STATE_ROOT;
9034
+ if (typeof prevPath === "string") process.env.PATH = prevPath;
9035
+ else delete process.env.PATH;
9036
+ await rm(cwd, { recursive: true, force: true });
9037
+ }
9038
+ });
9039
+
9040
+ it("dedupes allowed worker Stop leader nudges across workers in the same team window", async () => {
9041
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-team-dedupe-"));
9042
+ const prevPath = process.env.PATH;
9043
+ try {
9044
+ const stateDir = join(cwd, ".omx", "state");
9045
+ const logsDir = join(cwd, ".omx", "logs");
9046
+ const teamName = "worker-stop-team-dedupe";
9047
+ const teamDir = join(stateDir, "team", teamName);
9048
+ const fakeBinDir = join(cwd, "fake-bin");
9049
+ const tmuxLogPath = join(cwd, "tmux.log");
9050
+ await mkdir(fakeBinDir, { recursive: true });
9051
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
9052
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
9053
+ await writeJson(join(teamDir, "manifest.v2.json"), {
9054
+ name: teamName,
9055
+ tmux_session: "omx-team-worker-stop",
9056
+ leader_pane_id: "%42",
9057
+ workers: [
9058
+ { name: "worker-1", index: 1, pane_id: "%10" },
9059
+ { name: "worker-2", index: 2, pane_id: "%11" },
9060
+ ],
9061
+ });
9062
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
9063
+
9064
+ const first = await maybeNudgeLeaderForAllowedWorkerStop({
9065
+ stateDir,
9066
+ logsDir,
9067
+ workerContext: { teamName, workerName: "worker-1" },
9068
+ });
9069
+ const second = await maybeNudgeLeaderForAllowedWorkerStop({
9070
+ stateDir,
9071
+ logsDir,
9072
+ workerContext: { teamName, workerName: "worker-2" },
9073
+ });
9074
+
9075
+ assert.equal(first.result, "sent");
9076
+ assert.equal(second.result, "suppressed_team_cooldown");
9077
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
9078
+ const stopNudges = tmuxLog.match(/send-keys -t %42 -l \[OMX\] worker-\d+ native Stop allowed/g) || [];
9079
+ assert.equal(stopNudges.length, 1, "same-team workers should share one leader nudge cooldown window");
9080
+ const teamNudgeState = JSON.parse(await readFile(join(teamDir, "worker-stop-nudge.json"), "utf-8"));
9081
+ assert.equal(teamNudgeState.worker, "worker-1");
9082
+ assert.equal(teamNudgeState.delivery, "sent");
9083
+ } finally {
9084
+ if (typeof prevPath === "string") process.env.PATH = prevPath;
9085
+ else delete process.env.PATH;
9086
+ await rm(cwd, { recursive: true, force: true });
9087
+ }
9088
+ });
9089
+
9090
+ it("serializes concurrent allowed worker Stop leader nudges with a team lock", async () => {
9091
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-concurrent-dedupe-"));
9092
+ const prevPath = process.env.PATH;
9093
+ try {
9094
+ const stateDir = join(cwd, ".omx", "state");
9095
+ const logsDir = join(cwd, ".omx", "logs");
9096
+ const teamName = "worker-stop-concurrent";
9097
+ const teamDir = join(stateDir, "team", teamName);
9098
+ const fakeBinDir = join(cwd, "fake-bin");
9099
+ const tmuxLogPath = join(cwd, "tmux.log");
9100
+ await mkdir(fakeBinDir, { recursive: true });
9101
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, { sendDelayMs: 100 }));
9102
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
9103
+ await writeJson(join(teamDir, "manifest.v2.json"), {
9104
+ name: teamName,
9105
+ tmux_session: "omx-team-worker-stop",
9106
+ leader_pane_id: "%42",
9107
+ workers: [
9108
+ { name: "worker-1", index: 1, pane_id: "%10" },
9109
+ { name: "worker-2", index: 2, pane_id: "%11" },
9110
+ ],
9111
+ });
9112
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
9113
+
9114
+ const results = await Promise.all([
9115
+ maybeNudgeLeaderForAllowedWorkerStop({
9116
+ stateDir,
9117
+ logsDir,
9118
+ workerContext: { teamName, workerName: "worker-1" },
9119
+ }),
9120
+ maybeNudgeLeaderForAllowedWorkerStop({
9121
+ stateDir,
9122
+ logsDir,
9123
+ workerContext: { teamName, workerName: "worker-2" },
9124
+ }),
9125
+ ]);
9126
+
9127
+ assert.equal(results.filter((result) => result.result === "sent").length, 1);
9128
+ assert.equal(results.filter((result) => result.result === "suppressed_team_lock_held").length, 1);
9129
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
9130
+ const stopNudges = tmuxLog.match(/send-keys -t %42 -l \[OMX\] worker-\d+ native Stop allowed/g) || [];
9131
+ assert.equal(stopNudges.length, 1, "concurrent same-team workers should emit only one leader nudge");
9132
+ assert.equal(existsSync(join(teamDir, "worker-stop-nudge.lock")), false);
9133
+ } finally {
9134
+ if (typeof prevPath === "string") process.env.PATH = prevPath;
9135
+ else delete process.env.PATH;
9136
+ await rm(cwd, { recursive: true, force: true });
9137
+ }
9138
+ });
9139
+
9140
+ it("skips worker Stop leader nudge when team state is missing or shut down", async () => {
9141
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-missing-team-"));
9142
+ try {
9143
+ const stateDir = join(cwd, ".omx", "state");
9144
+ const logsDir = join(cwd, ".omx", "logs");
9145
+ const result = await maybeNudgeLeaderForAllowedWorkerStop({
9146
+ stateDir,
9147
+ logsDir,
9148
+ workerContext: { teamName: "removed-team", workerName: "worker-1" },
9149
+ });
9150
+
9151
+ assert.equal(result.result, "team_state_gone_or_shutdown");
9152
+ assert.equal(existsSync(join(stateDir, "team", "removed-team", "worker-stop-nudge.json")), false);
9153
+
9154
+ await writeJson(join(stateDir, "team", "shutdown-team", "shutdown.json"), {
9155
+ started_at: new Date().toISOString(),
9156
+ });
9157
+ const shutdownResult = await maybeNudgeLeaderForAllowedWorkerStop({
9158
+ stateDir,
9159
+ logsDir,
9160
+ workerContext: { teamName: "shutdown-team", workerName: "worker-1" },
9161
+ });
9162
+ assert.equal(shutdownResult.result, "team_state_gone_or_shutdown");
9163
+ assert.equal(existsSync(join(stateDir, "team", "shutdown-team", "worker-stop-nudge.json")), false);
9164
+ } finally {
9165
+ await rm(cwd, { recursive: true, force: true });
9166
+ }
9167
+ });
9168
+
9169
+ it("does not treat old visible worker Stop transcript as pending queue state", async () => {
9170
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-queue-dedupe-"));
9171
+ const prevPath = process.env.PATH;
9172
+ try {
9173
+ const stateDir = join(cwd, ".omx", "state");
9174
+ const logsDir = join(cwd, ".omx", "logs");
9175
+ const teamName = "queued-stop-dedupe";
9176
+ const teamDir = join(stateDir, "team", teamName);
9177
+ const fakeBinDir = join(cwd, "fake-bin");
9178
+ const tmuxLogPath = join(cwd, "tmux.log");
9179
+ await mkdir(fakeBinDir, { recursive: true });
9180
+ await writeFile(
9181
+ join(fakeBinDir, "tmux"),
9182
+ buildWorkerStopFakeTmux(tmuxLogPath, {
9183
+ busyLeader: true,
9184
+ captureText:
9185
+ `[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`
9186
+ + "• Working… (esc to interrupt)",
9187
+ }),
9188
+ );
9189
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
9190
+ await writeJson(join(teamDir, "manifest.v2.json"), {
9191
+ name: teamName,
9192
+ tmux_session: "omx-team-worker-stop",
9193
+ leader_pane_id: "%42",
9194
+ workers: [{ name: "worker-2", index: 2, pane_id: "%11" }],
9195
+ });
9196
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
9197
+
9198
+ const result = await maybeNudgeLeaderForAllowedWorkerStop({
9199
+ stateDir,
9200
+ logsDir,
9201
+ workerContext: { teamName, workerName: "worker-2" },
9202
+ });
9203
+
9204
+ assert.equal(result.result, "queued");
9205
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
9206
+ assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-2 native Stop allowed/);
9207
+ assert.match(tmuxLog, /send-keys -t %42 Tab/);
9208
+ const teamNudgeState = JSON.parse(await readFile(join(teamDir, "worker-stop-nudge.json"), "utf-8"));
9209
+ assert.equal(teamNudgeState.worker, "worker-2");
9210
+ assert.equal(teamNudgeState.delivery, "queued");
9211
+ } finally {
9212
+ if (typeof prevPath === "string") process.env.PATH = prevPath;
9213
+ else delete process.env.PATH;
9214
+ await rm(cwd, { recursive: true, force: true });
9215
+ }
9216
+ });
9217
+
9218
+ it("reports deferred when non-teardown persistence failure prevents worker Stop nudge cooldown state", async () => {
9219
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-persist-fail-"));
9220
+ const prevPath = process.env.PATH;
9221
+ try {
9222
+ const stateDir = join(cwd, ".omx", "state");
9223
+ const logsDir = join(cwd, ".omx", "logs");
9224
+ const teamName = "worker-stop-persist-fail";
9225
+ const teamDir = join(stateDir, "team", teamName);
9226
+ const fakeBinDir = join(cwd, "fake-bin");
9227
+ const tmuxLogPath = join(cwd, "tmux.log");
9228
+ await mkdir(fakeBinDir, { recursive: true });
9229
+ await writeJson(join(teamDir, "manifest.v2.json"), {
9230
+ name: teamName,
9231
+ tmux_session: "omx-team-worker-stop",
9232
+ leader_pane_id: "%42",
9233
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
9234
+ });
9235
+ await writeFile(join(teamDir, "workers"), "not a directory");
9236
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath));
9237
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
9238
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
9239
+
9240
+ const result = await maybeNudgeLeaderForAllowedWorkerStop({
9241
+ stateDir,
9242
+ logsDir,
9243
+ workerContext: { teamName, workerName: "worker-1" },
9244
+ });
9245
+
9246
+ assert.equal(result.result, "deferred");
9247
+ assert.equal(existsSync(join(teamDir, "worker-stop-nudge.json")), false);
9248
+ assert.equal(existsSync(join(teamDir, "workers", "worker-1", "worker-stop-nudge.json")), false);
9249
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
9250
+ assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
9251
+ const deliveryLogPath = join(logsDir, `team-delivery-${new Date().toISOString().split("T")[0]}.jsonl`);
9252
+ const deliveryEvents = (await readFile(deliveryLogPath, "utf-8"))
9253
+ .trim()
9254
+ .split("\n")
9255
+ .map((line) => JSON.parse(line));
9256
+ const deferredEvent = deliveryEvents.find((event) => event.event === "nudge_triggered" && event.result === "deferred");
9257
+ assert.equal(deferredEvent?.team, teamName);
9258
+ assert.equal(deferredEvent?.from_worker, "worker-1");
9259
+ assert.match(String(deferredEvent?.reason || ""), /EEXIST|ENOTDIR|not a directory|file already exists/);
9260
+ } finally {
9261
+ if (typeof prevPath === "string") process.env.PATH = prevPath;
9262
+ else delete process.env.PATH;
9263
+ await rm(cwd, { recursive: true, force: true });
9264
+ }
9265
+ });
9266
+
9267
+ it("does not recreate team state when teardown removes it during worker Stop delivery", async () => {
9268
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-teardown-race-"));
9269
+ const prevPath = process.env.PATH;
9270
+ try {
9271
+ const stateDir = join(cwd, ".omx", "state");
9272
+ const logsDir = join(cwd, ".omx", "logs");
9273
+ const teamName = "worker-stop-teardown-race";
9274
+ const teamDir = join(stateDir, "team", teamName);
9275
+ const fakeBinDir = join(cwd, "fake-bin");
9276
+ const tmuxLogPath = join(cwd, "tmux.log");
9277
+ await mkdir(fakeBinDir, { recursive: true });
9278
+ await writeJson(join(teamDir, "manifest.v2.json"), {
9279
+ name: teamName,
9280
+ tmux_session: "omx-team-worker-stop",
9281
+ leader_pane_id: "%42",
9282
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
9283
+ });
9284
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, { removePathOnSend: teamDir }));
9285
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
9286
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
9287
+
9288
+ const result = await maybeNudgeLeaderForAllowedWorkerStop({
9289
+ stateDir,
9290
+ logsDir,
9291
+ workerContext: { teamName, workerName: "worker-1" },
9292
+ });
9293
+
9294
+ assert.equal(result.result, "sent");
9295
+ assert.equal(existsSync(teamDir), false, "worker Stop delivery must not recreate removed team state");
9296
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
9297
+ assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
9298
+ } finally {
9299
+ if (typeof prevPath === "string") process.env.PATH = prevPath;
9300
+ else delete process.env.PATH;
9301
+ await rm(cwd, { recursive: true, force: true });
9302
+ }
9303
+ });
9304
+
9305
+ it("does not recreate team state when teardown removes it before deferred worker Stop recording", async () => {
9306
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-deferred-teardown-"));
9307
+ const prevPath = process.env.PATH;
9308
+ try {
9309
+ const stateDir = join(cwd, ".omx", "state");
9310
+ const logsDir = join(cwd, ".omx", "logs");
9311
+ const teamName = "worker-stop-deferred-teardown";
9312
+ const teamDir = join(stateDir, "team", teamName);
9313
+ const fakeBinDir = join(cwd, "fake-bin");
9314
+ const tmuxLogPath = join(cwd, "tmux.log");
9315
+ await mkdir(fakeBinDir, { recursive: true });
9316
+ await writeJson(join(teamDir, "manifest.v2.json"), {
9317
+ name: teamName,
9318
+ tmux_session: "omx-team-worker-stop",
9319
+ leader_pane_id: "%42",
9320
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
9321
+ });
9322
+ await writeFile(
9323
+ join(fakeBinDir, "tmux"),
9324
+ buildWorkerStopFakeTmux(tmuxLogPath, {
9325
+ currentCommand: "bash",
9326
+ captureText: "$ ",
9327
+ removePathOnCapture: teamDir,
9328
+ }),
9329
+ );
9330
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
9331
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
9332
+
9333
+ const result = await maybeNudgeLeaderForAllowedWorkerStop({
9334
+ stateDir,
9335
+ logsDir,
9336
+ workerContext: { teamName, workerName: "worker-1" },
9337
+ });
8040
9338
 
8041
- assert.equal(result.outputJson, null);
9339
+ assert.equal(result.result, "team_state_gone_or_shutdown");
9340
+ assert.equal(existsSync(teamDir), false, "deferred worker Stop recording must not recreate removed team state");
8042
9341
  const tmuxLog = await readFile(tmuxLogPath, "utf-8");
8043
- assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
8044
- assert.match(tmuxLog, /send-keys -t %42 Tab/);
8045
- assert.match(tmuxLog, /send-keys -t %42 C-m/);
8046
- assert.ok(
8047
- tmuxLog.indexOf("send-keys -t %42 Tab") < tmuxLog.indexOf("send-keys -t %42 C-m"),
8048
- "busy worker-stop nudge should press Tab before C-m",
8049
- );
8050
- const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
8051
- assert.equal(nudgeState.delivery, "queued");
9342
+ assert.doesNotMatch(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
8052
9343
  } finally {
8053
- if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
8054
- else delete process.env.OMX_TEAM_WORKER;
8055
- if (typeof prevTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
8056
- else delete process.env.OMX_TEAM_STATE_ROOT;
8057
9344
  if (typeof prevPath === "string") process.env.PATH = prevPath;
8058
9345
  else delete process.env.PATH;
8059
9346
  await rm(cwd, { recursive: true, force: true });
@@ -11618,19 +12905,259 @@ exit 0
11618
12905
  await rm(cwd, { recursive: true, force: true });
11619
12906
  }
11620
12907
  });
11621
-
11622
- it("auto-continues native Stop on permission-seeking prompts", async () => {
11623
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-permission-"));
12908
+
12909
+ it("auto-continues native Stop on permission-seeking prompts", async () => {
12910
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-permission-"));
12911
+ try {
12912
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
12913
+ process.env.OMX_SESSION_ID = "sess-stop-auto-permission";
12914
+
12915
+ const result = await dispatchCodexNativeHook(
12916
+ {
12917
+ hook_event_name: "Stop",
12918
+ cwd,
12919
+ session_id: "sess-stop-auto-permission",
12920
+ last_assistant_message: "Would you like me to continue with the cleanup?",
12921
+ },
12922
+ { cwd },
12923
+ );
12924
+
12925
+ assert.equal(result.omxEventName, "stop");
12926
+ assert.deepEqual(result.outputJson, {
12927
+ decision: "block",
12928
+ reason: DEFAULT_AUTO_NUDGE_RESPONSE,
12929
+ stopReason: "auto_nudge",
12930
+ systemMessage:
12931
+ "OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
12932
+ });
12933
+ } finally {
12934
+ await rm(cwd, { recursive: true, force: true });
12935
+ }
12936
+ });
12937
+
12938
+ it("auto-continues native Stop on \"if you want\" permission-seeking prompts", async () => {
12939
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-if-you-want-"));
12940
+ try {
12941
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
12942
+ process.env.OMX_SESSION_ID = "sess-stop-auto-if-you-want";
12943
+
12944
+ const result = await dispatchCodexNativeHook(
12945
+ {
12946
+ hook_event_name: "Stop",
12947
+ cwd,
12948
+ session_id: "sess-stop-auto-if-you-want",
12949
+ last_assistant_message: "If you want, I can continue with the cleanup from here.",
12950
+ },
12951
+ { cwd },
12952
+ );
12953
+
12954
+ assert.equal(result.omxEventName, "stop");
12955
+ assert.deepEqual(result.outputJson, {
12956
+ decision: "block",
12957
+ reason: DEFAULT_AUTO_NUDGE_RESPONSE,
12958
+ stopReason: "auto_nudge",
12959
+ systemMessage:
12960
+ "OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
12961
+ });
12962
+ } finally {
12963
+ await rm(cwd, { recursive: true, force: true });
12964
+ }
12965
+ });
12966
+
12967
+ it("does not auto-continue native Stop while deep-interview is waiting on an intent-first question", async () => {
12968
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-deep-interview-question-"));
12969
+ try {
12970
+ const stateDir = join(cwd, ".omx", "state");
12971
+ await mkdir(join(stateDir, "sessions", "sess-stop-auto-question"), { recursive: true });
12972
+ process.env.OMX_SESSION_ID = "sess-stop-auto-question";
12973
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-auto-question" });
12974
+ await writeJson(join(stateDir, "sessions", "sess-stop-auto-question", "skill-active-state.json"), {
12975
+ version: 1,
12976
+ active: true,
12977
+ skill: "deep-interview",
12978
+ phase: "planning",
12979
+ session_id: "sess-stop-auto-question",
12980
+ thread_id: "thread-stop-auto-question",
12981
+ input_lock: {
12982
+ active: true,
12983
+ scope: "deep-interview-auto-approval",
12984
+ blocked_inputs: ["yes", "proceed"],
12985
+ message: "Deep interview is active; auto-approval shortcuts are blocked until the interview finishes.",
12986
+ },
12987
+ });
12988
+ await writeJson(join(stateDir, "sessions", "sess-stop-auto-question", "deep-interview-state.json"), {
12989
+ active: true,
12990
+ mode: "deep-interview",
12991
+ current_phase: "intent-first",
12992
+ });
12993
+
12994
+ const result = await dispatchCodexNativeHook(
12995
+ {
12996
+ hook_event_name: "Stop",
12997
+ cwd,
12998
+ session_id: "sess-stop-auto-question",
12999
+ thread_id: "thread-stop-auto-question",
13000
+ turn_id: "turn-stop-auto-question-1",
13001
+ last_assistant_message: [
13002
+ "Round 2 | Target: Decision boundary | Ambiguity: 24%",
13003
+ "",
13004
+ "If an existing project spider still declares session_mode = \"owned\", should ZenX fail loudly so the stale attribute is removed, or should it ignore the attribute and initialize the session pool anyway?",
13005
+ "Keep going once I have your answer.",
13006
+ ].join("\n"),
13007
+ },
13008
+ { cwd },
13009
+ );
13010
+
13011
+ assert.equal(result.omxEventName, "stop");
13012
+ assert.equal(result.outputJson, null);
13013
+ } finally {
13014
+ await rm(cwd, { recursive: true, force: true });
13015
+ }
13016
+ });
13017
+
13018
+ it("suppresses native auto-nudge re-fire while session-scoped deep-interview state is still active", async () => {
13019
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-deep-interview-state-"));
13020
+ try {
13021
+ const stateDir = join(cwd, ".omx", "state");
13022
+ await mkdir(join(stateDir, "sessions", "sess-stop-auto-interview"), { recursive: true });
13023
+ process.env.OMX_SESSION_ID = "sess-stop-auto-interview";
13024
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-auto-interview" });
13025
+ await writeJson(join(stateDir, "sessions", "sess-stop-auto-interview", "deep-interview-state.json"), {
13026
+ active: true,
13027
+ mode: "deep-interview",
13028
+ current_phase: "intent-first",
13029
+ });
13030
+
13031
+ const result = await dispatchCodexNativeHook(
13032
+ {
13033
+ hook_event_name: "Stop",
13034
+ cwd,
13035
+ session_id: "sess-stop-auto-interview",
13036
+ thread_id: "thread-stop-auto-interview",
13037
+ turn_id: "turn-stop-auto-interview-2",
13038
+ stop_hook_active: true,
13039
+ last_assistant_message: "If you want, I can keep going from here.",
13040
+ },
13041
+ { cwd },
13042
+ );
13043
+
13044
+ assert.equal(result.omxEventName, "stop");
13045
+ assert.equal(result.outputJson, null);
13046
+ } finally {
13047
+ await rm(cwd, { recursive: true, force: true });
13048
+ }
13049
+ });
13050
+
13051
+ it("suppresses native auto-nudge when root deep-interview mode state is active and no session is known", async () => {
13052
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-deep-interview-mode-"));
13053
+ try {
13054
+ const stateDir = join(cwd, ".omx", "state");
13055
+ await mkdir(stateDir, { recursive: true });
13056
+ await writeJson(join(stateDir, "deep-interview-state.json"), {
13057
+ active: true,
13058
+ mode: "deep-interview",
13059
+ current_phase: "intent-first",
13060
+ });
13061
+
13062
+ const result = await dispatchCodexNativeHook(
13063
+ {
13064
+ hook_event_name: "Stop",
13065
+ cwd,
13066
+ turn_id: "turn-stop-auto-mode-1",
13067
+ last_assistant_message: "Would you like me to continue with the next step?",
13068
+ },
13069
+ { cwd },
13070
+ );
13071
+
13072
+ assert.equal(result.omxEventName, "stop");
13073
+ assert.equal(result.outputJson, null);
13074
+ } finally {
13075
+ await rm(cwd, { recursive: true, force: true });
13076
+ }
13077
+ });
13078
+
13079
+ it("treats inherited OMX_SESSION_ID as session-aware for native auto-nudge Stop checks", async () => {
13080
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-env-session-"));
13081
+ try {
13082
+ const stateDir = join(cwd, ".omx", "state");
13083
+ await mkdir(stateDir, { recursive: true });
13084
+ process.env.OMX_SESSION_ID = "sess-stop-auto-mode";
13085
+
13086
+ const result = await dispatchCodexNativeHook(
13087
+ {
13088
+ hook_event_name: "Stop",
13089
+ cwd,
13090
+ thread_id: "thread-stop-auto-env-session",
13091
+ turn_id: "turn-stop-auto-env-session-1",
13092
+ last_assistant_message: "Keep going and finish the cleanup.",
13093
+ },
13094
+ { cwd },
13095
+ );
13096
+
13097
+ assert.equal(result.omxEventName, "stop");
13098
+ assert.deepEqual(result.outputJson, {
13099
+ decision: "block",
13100
+ reason: DEFAULT_AUTO_NUDGE_RESPONSE,
13101
+ stopReason: "auto_nudge",
13102
+ systemMessage:
13103
+ "OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
13104
+ });
13105
+ const stopState = JSON.parse(await readFile(join(stateDir, "native-stop-state.json"), "utf-8")) as Record<string, unknown>;
13106
+ assert.ok((stopState.sessions as Record<string, unknown>)["sess-stop-auto-mode"]);
13107
+ } finally {
13108
+ await rm(cwd, { recursive: true, force: true });
13109
+ }
13110
+ });
13111
+
13112
+
13113
+ it("ignores generic SESSION_ID for native auto-nudge Stop session scoping", async () => {
13114
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-generic-session-"));
13115
+ try {
13116
+ const stateDir = join(cwd, ".omx", "state");
13117
+ await mkdir(stateDir, { recursive: true });
13118
+ process.env.SESSION_ID = "generic-shell-session";
13119
+
13120
+ const result = await dispatchCodexNativeHook(
13121
+ {
13122
+ hook_event_name: "Stop",
13123
+ cwd,
13124
+ thread_id: "thread-stop-auto-generic-session",
13125
+ turn_id: "turn-stop-auto-generic-session-1",
13126
+ last_assistant_message: "Keep going and finish the cleanup.",
13127
+ },
13128
+ { cwd },
13129
+ );
13130
+
13131
+ assert.equal(result.omxEventName, "stop");
13132
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
13133
+ const stopState = JSON.parse(await readFile(join(stateDir, "native-stop-state.json"), "utf-8")) as Record<string, unknown>;
13134
+ const sessions = stopState.sessions as Record<string, unknown>;
13135
+ assert.equal(sessions["generic-shell-session"], undefined);
13136
+ assert.ok(sessions["thread-stop-auto-generic-session"]);
13137
+ } finally {
13138
+ await rm(cwd, { recursive: true, force: true });
13139
+ }
13140
+ });
13141
+ it("does not suppress native auto-nudge from stale root deep-interview mode state when the explicit session-scoped mode state is absent", async () => {
13142
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-stale-root-mode-"));
11624
13143
  try {
11625
- await mkdir(join(cwd, ".omx", "state"), { recursive: true });
11626
- process.env.OMX_SESSION_ID = "sess-stop-auto-permission";
13144
+ const stateDir = join(cwd, ".omx", "state");
13145
+ await mkdir(stateDir, { recursive: true });
13146
+ process.env.OMX_SESSION_ID = "sess-stop-auto-stale-root-mode";
13147
+ await writeJson(join(stateDir, "deep-interview-state.json"), {
13148
+ active: true,
13149
+ mode: "deep-interview",
13150
+ current_phase: "intent-first",
13151
+ });
11627
13152
 
11628
13153
  const result = await dispatchCodexNativeHook(
11629
13154
  {
11630
13155
  hook_event_name: "Stop",
11631
13156
  cwd,
11632
- session_id: "sess-stop-auto-permission",
11633
- last_assistant_message: "Would you like me to continue with the cleanup?",
13157
+ session_id: "sess-stop-auto-stale-root-mode",
13158
+ thread_id: "thread-stop-auto-stale-root-mode",
13159
+ turn_id: "turn-stop-auto-stale-root-mode-1",
13160
+ last_assistant_message: "Keep going and finish the cleanup.",
11634
13161
  },
11635
13162
  { cwd },
11636
13163
  );
@@ -11648,18 +13175,26 @@ exit 0
11648
13175
  }
11649
13176
  });
11650
13177
 
11651
- it("auto-continues native Stop on \"if you want\" permission-seeking prompts", async () => {
11652
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-if-you-want-"));
13178
+ it("does not suppress native auto-nudge from stale root deep-interview skill state when the explicit session-scoped canonical skill state is absent", async () => {
13179
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-stale-root-skill-"));
11653
13180
  try {
11654
- await mkdir(join(cwd, ".omx", "state"), { recursive: true });
11655
- process.env.OMX_SESSION_ID = "sess-stop-auto-if-you-want";
13181
+ const stateDir = join(cwd, ".omx", "state");
13182
+ await mkdir(stateDir, { recursive: true });
13183
+ process.env.OMX_SESSION_ID = "sess-stop-auto-stale-root-skill";
13184
+ await writeJson(join(stateDir, "skill-active-state.json"), {
13185
+ active: true,
13186
+ skill: "deep-interview",
13187
+ phase: "planning",
13188
+ });
11656
13189
 
11657
13190
  const result = await dispatchCodexNativeHook(
11658
13191
  {
11659
13192
  hook_event_name: "Stop",
11660
13193
  cwd,
11661
- session_id: "sess-stop-auto-if-you-want",
11662
- last_assistant_message: "If you want, I can continue with the cleanup from here.",
13194
+ session_id: "sess-stop-auto-stale-root-skill",
13195
+ thread_id: "thread-stop-auto-stale-root-skill",
13196
+ turn_id: "turn-stop-auto-stale-root-skill-1",
13197
+ last_assistant_message: "Keep going and finish the cleanup.",
11663
13198
  },
11664
13199
  { cwd },
11665
13200
  );
@@ -11677,20 +13212,16 @@ exit 0
11677
13212
  }
11678
13213
  });
11679
13214
 
11680
- it("does not auto-continue native Stop while deep-interview is waiting on an intent-first question", async () => {
11681
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-deep-interview-question-"));
13215
+ it("does not suppress native auto-nudge from stale root deep-interview input lock when the explicit session-scoped canonical skill state is absent", async () => {
13216
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-stale-root-lock-"));
11682
13217
  try {
11683
13218
  const stateDir = join(cwd, ".omx", "state");
11684
- await mkdir(join(stateDir, "sessions", "sess-stop-auto-question"), { recursive: true });
11685
- process.env.OMX_SESSION_ID = "sess-stop-auto-question";
11686
- await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-auto-question" });
11687
- await writeJson(join(stateDir, "sessions", "sess-stop-auto-question", "skill-active-state.json"), {
11688
- version: 1,
13219
+ await mkdir(stateDir, { recursive: true });
13220
+ process.env.OMX_SESSION_ID = "sess-stop-auto-stale-root-lock";
13221
+ await writeJson(join(stateDir, "skill-active-state.json"), {
11689
13222
  active: true,
11690
13223
  skill: "deep-interview",
11691
13224
  phase: "planning",
11692
- session_id: "sess-stop-auto-question",
11693
- thread_id: "thread-stop-auto-question",
11694
13225
  input_lock: {
11695
13226
  active: true,
11696
13227
  scope: "deep-interview-auto-approval",
@@ -11698,44 +13229,45 @@ exit 0
11698
13229
  message: "Deep interview is active; auto-approval shortcuts are blocked until the interview finishes.",
11699
13230
  },
11700
13231
  });
11701
- await writeJson(join(stateDir, "sessions", "sess-stop-auto-question", "deep-interview-state.json"), {
11702
- active: true,
11703
- mode: "deep-interview",
11704
- current_phase: "intent-first",
11705
- });
11706
13232
 
11707
13233
  const result = await dispatchCodexNativeHook(
11708
13234
  {
11709
13235
  hook_event_name: "Stop",
11710
13236
  cwd,
11711
- session_id: "sess-stop-auto-question",
11712
- thread_id: "thread-stop-auto-question",
11713
- turn_id: "turn-stop-auto-question-1",
11714
- last_assistant_message: [
11715
- "Round 2 | Target: Decision boundary | Ambiguity: 24%",
11716
- "",
11717
- "If an existing project spider still declares session_mode = \"owned\", should ZenX fail loudly so the stale attribute is removed, or should it ignore the attribute and initialize the session pool anyway?",
11718
- "Keep going once I have your answer.",
11719
- ].join("\n"),
13237
+ session_id: "sess-stop-auto-stale-root-lock",
13238
+ thread_id: "thread-stop-auto-stale-root-lock",
13239
+ turn_id: "turn-stop-auto-stale-root-lock-1",
13240
+ last_assistant_message: "Keep going and finish the cleanup.",
11720
13241
  },
11721
13242
  { cwd },
11722
13243
  );
11723
13244
 
11724
13245
  assert.equal(result.omxEventName, "stop");
11725
- assert.equal(result.outputJson, null);
13246
+ assert.deepEqual(result.outputJson, {
13247
+ decision: "block",
13248
+ reason: DEFAULT_AUTO_NUDGE_RESPONSE,
13249
+ stopReason: "auto_nudge",
13250
+ systemMessage:
13251
+ "OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
13252
+ });
11726
13253
  } finally {
11727
13254
  await rm(cwd, { recursive: true, force: true });
11728
13255
  }
11729
13256
  });
11730
13257
 
11731
- it("suppresses native auto-nudge re-fire while session-scoped deep-interview state is still active", async () => {
11732
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-deep-interview-state-"));
13258
+ it("does not suppress native auto-nudge from active root deep-interview state when the current scoped mode state is explicitly inactive", async () => {
13259
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-inactive-scoped-mode-"));
11733
13260
  try {
11734
13261
  const stateDir = join(cwd, ".omx", "state");
11735
- await mkdir(join(stateDir, "sessions", "sess-stop-auto-interview"), { recursive: true });
11736
- process.env.OMX_SESSION_ID = "sess-stop-auto-interview";
11737
- await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-auto-interview" });
11738
- await writeJson(join(stateDir, "sessions", "sess-stop-auto-interview", "deep-interview-state.json"), {
13262
+ await mkdir(join(stateDir, "sessions", "sess-stop-auto-inactive-mode"), { recursive: true });
13263
+ process.env.OMX_SESSION_ID = "sess-stop-auto-inactive-mode";
13264
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-auto-inactive-mode" });
13265
+ await writeJson(join(stateDir, "sessions", "sess-stop-auto-inactive-mode", "deep-interview-state.json"), {
13266
+ active: false,
13267
+ mode: "deep-interview",
13268
+ current_phase: "completed",
13269
+ });
13270
+ await writeJson(join(stateDir, "deep-interview-state.json"), {
11739
13271
  active: true,
11740
13272
  mode: "deep-interview",
11741
13273
  current_phase: "intent-first",
@@ -11745,64 +13277,108 @@ exit 0
11745
13277
  {
11746
13278
  hook_event_name: "Stop",
11747
13279
  cwd,
11748
- session_id: "sess-stop-auto-interview",
11749
- thread_id: "thread-stop-auto-interview",
11750
- turn_id: "turn-stop-auto-interview-2",
11751
- stop_hook_active: true,
11752
- last_assistant_message: "If you want, I can keep going from here.",
13280
+ session_id: "sess-stop-auto-inactive-mode",
13281
+ thread_id: "thread-stop-auto-inactive-mode",
13282
+ turn_id: "turn-stop-auto-inactive-mode-1",
13283
+ last_assistant_message: "Keep going and finish the cleanup.",
11753
13284
  },
11754
13285
  { cwd },
11755
13286
  );
11756
13287
 
11757
13288
  assert.equal(result.omxEventName, "stop");
11758
- assert.equal(result.outputJson, null);
13289
+ assert.deepEqual(result.outputJson, {
13290
+ decision: "block",
13291
+ reason: DEFAULT_AUTO_NUDGE_RESPONSE,
13292
+ stopReason: "auto_nudge",
13293
+ systemMessage:
13294
+ "OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
13295
+ });
11759
13296
  } finally {
11760
13297
  await rm(cwd, { recursive: true, force: true });
11761
13298
  }
11762
13299
  });
11763
13300
 
11764
- it("suppresses native auto-nudge when root deep-interview mode state is active and no session is known", async () => {
11765
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-deep-interview-mode-"));
13301
+ it("clears stale root skill-active state when current session ralplan is terminal", async () => {
13302
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-skill-terminal-"));
11766
13303
  try {
11767
13304
  const stateDir = join(cwd, ".omx", "state");
11768
- await mkdir(stateDir, { recursive: true });
11769
- await writeJson(join(stateDir, "deep-interview-state.json"), {
13305
+ const sessionId = "sess-stop-terminal-ralplan";
13306
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
13307
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
13308
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
13309
+ active: false,
13310
+ mode: "ralplan",
13311
+ current_phase: "completed",
13312
+ lifecycle_outcome: "finished",
13313
+ run_outcome: "finish",
13314
+ final_artifact: "proposed_plan",
13315
+ });
13316
+ await writeJson(join(stateDir, "skill-active-state.json"), {
11770
13317
  active: true,
11771
- mode: "deep-interview",
11772
- current_phase: "intent-first",
13318
+ skill: "ultrawork",
13319
+ phase: "planning",
13320
+ source: "keyword-detector",
13321
+ active_skills: [
13322
+ { skill: "ultrawork", phase: "planning", active: true },
13323
+ ],
11773
13324
  });
11774
13325
 
11775
13326
  const result = await dispatchCodexNativeHook(
11776
13327
  {
11777
13328
  hook_event_name: "Stop",
11778
13329
  cwd,
11779
- turn_id: "turn-stop-auto-mode-1",
11780
- last_assistant_message: "Would you like me to continue with the next step?",
13330
+ session_id: sessionId,
13331
+ thread_id: "thread-stop-terminal-ralplan",
13332
+ turn_id: "turn-stop-terminal-ralplan-1",
13333
+ last_assistant_message: "Done.",
11781
13334
  },
11782
13335
  { cwd },
11783
13336
  );
11784
13337
 
11785
13338
  assert.equal(result.omxEventName, "stop");
11786
13339
  assert.equal(result.outputJson, null);
13340
+
13341
+ const rootSkillState = JSON.parse(
13342
+ await readFile(join(stateDir, "skill-active-state.json"), "utf-8"),
13343
+ ) as { active?: boolean; active_skills?: unknown[]; reconciliation_reason?: string };
13344
+ assert.equal(rootSkillState.active, false);
13345
+ assert.deepEqual(rootSkillState.active_skills, []);
13346
+ assert.equal(rootSkillState.reconciliation_reason, "stop_hook_session_state_terminal");
11787
13347
  } finally {
11788
13348
  await rm(cwd, { recursive: true, force: true });
11789
13349
  }
11790
13350
  });
11791
13351
 
11792
- it("treats inherited OMX_SESSION_ID as session-aware for native auto-nudge Stop checks", async () => {
11793
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-env-session-"));
13352
+ it("preserves legitimate session-scoped ultrawork blocking while reconciling root skill-active state", async () => {
13353
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-active-root-skill-session-mode-"));
11794
13354
  try {
11795
13355
  const stateDir = join(cwd, ".omx", "state");
11796
- await mkdir(stateDir, { recursive: true });
11797
- process.env.OMX_SESSION_ID = "sess-stop-auto-mode";
13356
+ const sessionId = "sess-stop-active-ultrawork";
13357
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
13358
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
13359
+ await writeJson(join(stateDir, "sessions", sessionId, "ultrawork-state.json"), {
13360
+ active: true,
13361
+ mode: "ultrawork",
13362
+ current_phase: "executing",
13363
+ session_id: sessionId,
13364
+ });
13365
+ await writeJson(join(stateDir, "skill-active-state.json"), {
13366
+ active: true,
13367
+ skill: "ultrawork",
13368
+ phase: "planning",
13369
+ source: "keyword-detector",
13370
+ active_skills: [
13371
+ { skill: "ultrawork", phase: "planning", active: true, session_id: sessionId },
13372
+ ],
13373
+ });
11798
13374
 
11799
13375
  const result = await dispatchCodexNativeHook(
11800
13376
  {
11801
13377
  hook_event_name: "Stop",
11802
13378
  cwd,
11803
- thread_id: "thread-stop-auto-env-session",
11804
- turn_id: "turn-stop-auto-env-session-1",
11805
- last_assistant_message: "Keep going and finish the cleanup.",
13379
+ session_id: sessionId,
13380
+ thread_id: "thread-stop-active-ultrawork",
13381
+ turn_id: "turn-stop-active-ultrawork-1",
11806
13382
  },
11807
13383
  { cwd },
11808
13384
  );
@@ -11810,104 +13386,102 @@ exit 0
11810
13386
  assert.equal(result.omxEventName, "stop");
11811
13387
  assert.deepEqual(result.outputJson, {
11812
13388
  decision: "block",
11813
- reason: DEFAULT_AUTO_NUDGE_RESPONSE,
11814
- stopReason: "auto_nudge",
11815
- systemMessage:
11816
- "OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
13389
+ reason: "OMX ultrawork is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
13390
+ stopReason: "ultrawork_executing",
13391
+ systemMessage: "OMX ultrawork is still active (phase: executing).",
11817
13392
  });
11818
- const stopState = JSON.parse(await readFile(join(stateDir, "native-stop-state.json"), "utf-8")) as Record<string, unknown>;
11819
- assert.ok((stopState.sessions as Record<string, unknown>)["sess-stop-auto-mode"]);
11820
- } finally {
11821
- await rm(cwd, { recursive: true, force: true });
11822
- }
11823
- });
11824
-
11825
-
11826
- it("ignores generic SESSION_ID for native auto-nudge Stop session scoping", async () => {
11827
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-generic-session-"));
11828
- try {
11829
- const stateDir = join(cwd, ".omx", "state");
11830
- await mkdir(stateDir, { recursive: true });
11831
- process.env.SESSION_ID = "generic-shell-session";
11832
-
11833
- const result = await dispatchCodexNativeHook(
11834
- {
11835
- hook_event_name: "Stop",
11836
- cwd,
11837
- thread_id: "thread-stop-auto-generic-session",
11838
- turn_id: "turn-stop-auto-generic-session-1",
11839
- last_assistant_message: "Keep going and finish the cleanup.",
11840
- },
11841
- { cwd },
11842
- );
11843
13393
 
11844
- assert.equal(result.omxEventName, "stop");
11845
- assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
11846
- const stopState = JSON.parse(await readFile(join(stateDir, "native-stop-state.json"), "utf-8")) as Record<string, unknown>;
11847
- const sessions = stopState.sessions as Record<string, unknown>;
11848
- assert.equal(sessions["generic-shell-session"], undefined);
11849
- assert.ok(sessions["thread-stop-auto-generic-session"]);
13394
+ const rootSkillState = JSON.parse(
13395
+ await readFile(join(stateDir, "skill-active-state.json"), "utf-8"),
13396
+ ) as { active?: boolean; active_skills?: Array<{ skill?: string }> };
13397
+ assert.equal(rootSkillState.active, true);
13398
+ assert.deepEqual(rootSkillState.active_skills?.map((entry) => entry.skill), ["ultrawork"]);
11850
13399
  } finally {
11851
13400
  await rm(cwd, { recursive: true, force: true });
11852
13401
  }
11853
13402
  });
11854
- it("does not suppress native auto-nudge from stale root deep-interview mode state when the explicit session-scoped mode state is absent", async () => {
11855
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-stale-root-mode-"));
13403
+
13404
+ it("reconciles stale root skill-active state under OMX_ROOT boxed state", async () => {
13405
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-boxed-source-"));
13406
+ const omxRoot = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-boxed-root-"));
13407
+ const previousOmxRoot = process.env.OMX_ROOT;
11856
13408
  try {
11857
- const stateDir = join(cwd, ".omx", "state");
11858
- await mkdir(stateDir, { recursive: true });
11859
- process.env.OMX_SESSION_ID = "sess-stop-auto-stale-root-mode";
11860
- await writeJson(join(stateDir, "deep-interview-state.json"), {
13409
+ process.env.OMX_ROOT = omxRoot;
13410
+ const stateDir = join(omxRoot, ".omx", "state");
13411
+ const sourceStateDir = join(cwd, ".omx", "state");
13412
+ const sessionId = "sess-stop-boxed-ralplan";
13413
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
13414
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
13415
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
13416
+ active: false,
13417
+ mode: "ralplan",
13418
+ current_phase: "completed",
13419
+ lifecycle_outcome: "finished",
13420
+ run_outcome: "finish",
13421
+ });
13422
+ await writeJson(join(stateDir, "skill-active-state.json"), {
11861
13423
  active: true,
11862
- mode: "deep-interview",
11863
- current_phase: "intent-first",
13424
+ skill: "ultrawork",
13425
+ phase: "planning",
13426
+ source: "keyword-detector",
13427
+ active_skills: [
13428
+ { skill: "ultrawork", phase: "planning", active: true },
13429
+ ],
11864
13430
  });
11865
13431
 
11866
13432
  const result = await dispatchCodexNativeHook(
11867
13433
  {
11868
13434
  hook_event_name: "Stop",
11869
13435
  cwd,
11870
- session_id: "sess-stop-auto-stale-root-mode",
11871
- thread_id: "thread-stop-auto-stale-root-mode",
11872
- turn_id: "turn-stop-auto-stale-root-mode-1",
11873
- last_assistant_message: "Keep going and finish the cleanup.",
13436
+ session_id: sessionId,
13437
+ thread_id: "thread-stop-boxed-ralplan",
13438
+ turn_id: "turn-stop-boxed-ralplan-1",
13439
+ last_assistant_message: "Done.",
11874
13440
  },
11875
13441
  { cwd },
11876
13442
  );
11877
13443
 
11878
13444
  assert.equal(result.omxEventName, "stop");
11879
- assert.deepEqual(result.outputJson, {
11880
- decision: "block",
11881
- reason: DEFAULT_AUTO_NUDGE_RESPONSE,
11882
- stopReason: "auto_nudge",
11883
- systemMessage:
11884
- "OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
11885
- });
13445
+ assert.equal(result.outputJson, null);
13446
+
13447
+ const boxedRootSkillState = JSON.parse(
13448
+ await readFile(join(stateDir, "skill-active-state.json"), "utf-8"),
13449
+ ) as { active?: boolean; active_skills?: unknown[]; reconciliation_reason?: string };
13450
+ assert.equal(boxedRootSkillState.active, false);
13451
+ assert.deepEqual(boxedRootSkillState.active_skills, []);
13452
+ assert.equal(boxedRootSkillState.reconciliation_reason, "stop_hook_session_state_terminal");
13453
+ assert.equal(existsSync(join(sourceStateDir, "skill-active-state.json")), false);
11886
13454
  } finally {
13455
+ if (previousOmxRoot === undefined) delete process.env.OMX_ROOT;
13456
+ else process.env.OMX_ROOT = previousOmxRoot;
11887
13457
  await rm(cwd, { recursive: true, force: true });
13458
+ await rm(omxRoot, { recursive: true, force: true });
11888
13459
  }
11889
13460
  });
11890
13461
 
11891
- it("does not suppress native auto-nudge from stale root deep-interview skill state when the explicit session-scoped canonical skill state is absent", async () => {
11892
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-stale-root-skill-"));
13462
+ it("auto-continues native Stop for permission-seeking prompts even outside OMX runtime", async () => {
13463
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-plain-session-"));
11893
13464
  try {
11894
- const stateDir = join(cwd, ".omx", "state");
11895
- await mkdir(stateDir, { recursive: true });
11896
- process.env.OMX_SESSION_ID = "sess-stop-auto-stale-root-skill";
11897
- await writeJson(join(stateDir, "skill-active-state.json"), {
11898
- active: true,
11899
- skill: "deep-interview",
11900
- phase: "planning",
11901
- });
13465
+ await dispatchCodexNativeHook(
13466
+ {
13467
+ hook_event_name: "SessionStart",
13468
+ cwd,
13469
+ session_id: "plain-stop-session",
13470
+ },
13471
+ {
13472
+ cwd,
13473
+ sessionOwnerPid: process.pid,
13474
+ },
13475
+ );
11902
13476
 
11903
13477
  const result = await dispatchCodexNativeHook(
11904
13478
  {
11905
13479
  hook_event_name: "Stop",
11906
13480
  cwd,
11907
- session_id: "sess-stop-auto-stale-root-skill",
11908
- thread_id: "thread-stop-auto-stale-root-skill",
11909
- turn_id: "turn-stop-auto-stale-root-skill-1",
11910
- last_assistant_message: "Keep going and finish the cleanup.",
13481
+ session_id: "plain-stop-session",
13482
+ thread_id: "plain-thread",
13483
+ turn_id: "plain-turn-1",
13484
+ last_assistant_message: "If you want, I can continue with the cleanup from here.",
11911
13485
  },
11912
13486
  { cwd },
11913
13487
  );
@@ -11925,32 +13499,45 @@ exit 0
11925
13499
  }
11926
13500
  });
11927
13501
 
11928
- it("does not suppress native auto-nudge from stale root deep-interview input lock when the explicit session-scoped canonical skill state is absent", async () => {
11929
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-stale-root-lock-"));
13502
+ it("re-fires team Stop output for a later fresh Stop reply while the team is still active", async () => {
13503
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-refire-"));
11930
13504
  try {
11931
13505
  const stateDir = join(cwd, ".omx", "state");
11932
13506
  await mkdir(stateDir, { recursive: true });
11933
- process.env.OMX_SESSION_ID = "sess-stop-auto-stale-root-lock";
11934
- await writeJson(join(stateDir, "skill-active-state.json"), {
13507
+ await writeJson(join(stateDir, "team-state.json"), {
11935
13508
  active: true,
11936
- skill: "deep-interview",
11937
- phase: "planning",
11938
- input_lock: {
11939
- active: true,
11940
- scope: "deep-interview-auto-approval",
11941
- blocked_inputs: ["yes", "proceed"],
11942
- message: "Deep interview is active; auto-approval shortcuts are blocked until the interview finishes.",
11943
- },
13509
+ current_phase: "team-exec",
13510
+ team_name: "review-team",
13511
+ session_id: "sess-stop-team-refire",
13512
+ thread_id: "thread-stop-team-refire",
13513
+ });
13514
+ await writeJson(join(stateDir, "team", "review-team", "phase.json"), {
13515
+ current_phase: "team-verify",
13516
+ max_fix_attempts: 3,
13517
+ current_fix_attempt: 0,
13518
+ transitions: [],
13519
+ updated_at: new Date().toISOString(),
11944
13520
  });
11945
13521
 
13522
+ await dispatchCodexNativeHook(
13523
+ {
13524
+ hook_event_name: "Stop",
13525
+ cwd,
13526
+ session_id: "sess-stop-team-refire",
13527
+ thread_id: "thread-stop-team-refire",
13528
+ turn_id: "turn-stop-team-refire-1",
13529
+ },
13530
+ { cwd },
13531
+ );
13532
+
11946
13533
  const result = await dispatchCodexNativeHook(
11947
13534
  {
11948
13535
  hook_event_name: "Stop",
11949
13536
  cwd,
11950
- session_id: "sess-stop-auto-stale-root-lock",
11951
- thread_id: "thread-stop-auto-stale-root-lock",
11952
- turn_id: "turn-stop-auto-stale-root-lock-1",
11953
- last_assistant_message: "Keep going and finish the cleanup.",
13537
+ session_id: "sess-stop-team-refire",
13538
+ thread_id: "thread-stop-team-refire",
13539
+ turn_id: "turn-stop-team-refire-2",
13540
+ stop_hook_active: true,
11954
13541
  },
11955
13542
  { cwd },
11956
13543
  );
@@ -11958,505 +13545,557 @@ exit 0
11958
13545
  assert.equal(result.omxEventName, "stop");
11959
13546
  assert.deepEqual(result.outputJson, {
11960
13547
  decision: "block",
11961
- reason: DEFAULT_AUTO_NUDGE_RESPONSE,
11962
- stopReason: "auto_nudge",
11963
- systemMessage:
11964
- "OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
13548
+ reason:
13549
+ `OMX team pipeline is still active (review-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
13550
+ stopReason: "team_team-verify",
13551
+ systemMessage: "OMX team pipeline is still active at phase team-verify.",
11965
13552
  });
11966
13553
  } finally {
11967
13554
  await rm(cwd, { recursive: true, force: true });
11968
13555
  }
11969
13556
  });
11970
13557
 
11971
- it("does not suppress native auto-nudge from active root deep-interview state when the current scoped mode state is explicitly inactive", async () => {
11972
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-inactive-scoped-mode-"));
13558
+ it("suppresses duplicate team Stop replays across native/canonical session-id drift", async () => {
13559
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-session-drift-"));
11973
13560
  try {
11974
13561
  const stateDir = join(cwd, ".omx", "state");
11975
- await mkdir(join(stateDir, "sessions", "sess-stop-auto-inactive-mode"), { recursive: true });
11976
- process.env.OMX_SESSION_ID = "sess-stop-auto-inactive-mode";
11977
- await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-auto-inactive-mode" });
11978
- await writeJson(join(stateDir, "sessions", "sess-stop-auto-inactive-mode", "deep-interview-state.json"), {
11979
- active: false,
11980
- mode: "deep-interview",
11981
- current_phase: "completed",
13562
+ await mkdir(join(stateDir, "sessions", "omx-canonical"), { recursive: true });
13563
+ process.env.OMX_SESSION_ID = "omx-canonical";
13564
+ await writeJson(join(stateDir, "session.json"), {
13565
+ session_id: "omx-canonical",
13566
+ native_session_id: "codex-native",
11982
13567
  });
11983
- await writeJson(join(stateDir, "deep-interview-state.json"), {
13568
+ await writeJson(join(stateDir, "sessions", "omx-canonical", "team-state.json"), {
11984
13569
  active: true,
11985
- mode: "deep-interview",
11986
- current_phase: "intent-first",
13570
+ current_phase: "starting",
13571
+ team_name: "current-team",
13572
+ session_id: "omx-canonical",
13573
+ });
13574
+ await writeJson(join(stateDir, "team", "current-team", "phase.json"), {
13575
+ current_phase: "team-verify",
13576
+ max_fix_attempts: 3,
13577
+ current_fix_attempt: 1,
13578
+ transitions: [],
13579
+ updated_at: new Date().toISOString(),
11987
13580
  });
11988
13581
 
11989
- const result = await dispatchCodexNativeHook(
13582
+ await dispatchCodexNativeHook(
11990
13583
  {
11991
13584
  hook_event_name: "Stop",
11992
13585
  cwd,
11993
- session_id: "sess-stop-auto-inactive-mode",
11994
- thread_id: "thread-stop-auto-inactive-mode",
11995
- turn_id: "turn-stop-auto-inactive-mode-1",
11996
- last_assistant_message: "Keep going and finish the cleanup.",
13586
+ session_id: "codex-native",
13587
+ thread_id: "thread-stop-team-drift",
13588
+ turn_id: "turn-stop-team-drift-1",
11997
13589
  },
11998
13590
  { cwd },
11999
13591
  );
12000
13592
 
12001
- assert.equal(result.omxEventName, "stop");
12002
- assert.deepEqual(result.outputJson, {
12003
- decision: "block",
12004
- reason: DEFAULT_AUTO_NUDGE_RESPONSE,
12005
- stopReason: "auto_nudge",
12006
- systemMessage:
12007
- "OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
12008
- });
12009
- } finally {
12010
- await rm(cwd, { recursive: true, force: true });
12011
- }
12012
- });
13593
+ const duplicate = await dispatchCodexNativeHook(
13594
+ {
13595
+ hook_event_name: "Stop",
13596
+ cwd,
13597
+ session_id: "omx-canonical",
13598
+ thread_id: "thread-stop-team-drift",
13599
+ turn_id: "turn-stop-team-drift-1",
13600
+ stop_hook_active: true,
13601
+ },
13602
+ { cwd },
13603
+ );
12013
13604
 
12014
- it("clears stale root skill-active state when current session ralplan is terminal", async () => {
12015
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-skill-terminal-"));
12016
- try {
12017
- const stateDir = join(cwd, ".omx", "state");
12018
- const sessionId = "sess-stop-terminal-ralplan";
12019
- await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
12020
- await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
12021
- await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
12022
- active: false,
12023
- mode: "ralplan",
12024
- current_phase: "completed",
12025
- lifecycle_outcome: "finished",
12026
- run_outcome: "finish",
12027
- final_artifact: "proposed_plan",
12028
- });
12029
- await writeJson(join(stateDir, "skill-active-state.json"), {
12030
- active: true,
12031
- skill: "ultrawork",
12032
- phase: "planning",
12033
- source: "keyword-detector",
12034
- active_skills: [
12035
- { skill: "ultrawork", phase: "planning", active: true },
12036
- ],
13605
+ assert.equal(duplicate.omxEventName, "stop");
13606
+ assert.deepEqual(duplicate.outputJson, {
13607
+ decision: "block",
13608
+ reason:
13609
+ `OMX team pipeline is still active (current-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
13610
+ stopReason: "team_team-verify",
13611
+ systemMessage: "OMX team pipeline is still active at phase team-verify.",
12037
13612
  });
12038
13613
 
12039
- const result = await dispatchCodexNativeHook(
13614
+ const fresh = await dispatchCodexNativeHook(
12040
13615
  {
12041
13616
  hook_event_name: "Stop",
12042
13617
  cwd,
12043
- session_id: sessionId,
12044
- thread_id: "thread-stop-terminal-ralplan",
12045
- turn_id: "turn-stop-terminal-ralplan-1",
12046
- last_assistant_message: "Done.",
13618
+ session_id: "omx-canonical",
13619
+ thread_id: "thread-stop-team-drift",
13620
+ turn_id: "turn-stop-team-drift-2",
13621
+ stop_hook_active: true,
12047
13622
  },
12048
13623
  { cwd },
12049
13624
  );
12050
13625
 
12051
- assert.equal(result.omxEventName, "stop");
12052
- assert.equal(result.outputJson, null);
13626
+ assert.equal(fresh.omxEventName, "stop");
13627
+ assert.deepEqual(fresh.outputJson, {
13628
+ decision: "block",
13629
+ reason:
13630
+ `OMX team pipeline is still active (current-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
13631
+ stopReason: "team_team-verify",
13632
+ systemMessage: "OMX team pipeline is still active at phase team-verify.",
13633
+ });
12053
13634
 
12054
- const rootSkillState = JSON.parse(
12055
- await readFile(join(stateDir, "skill-active-state.json"), "utf-8"),
12056
- ) as { active?: boolean; active_skills?: unknown[]; reconciliation_reason?: string };
12057
- assert.equal(rootSkillState.active, false);
12058
- assert.deepEqual(rootSkillState.active_skills, []);
12059
- assert.equal(rootSkillState.reconciliation_reason, "stop_hook_session_state_terminal");
13635
+ const persisted = JSON.parse(
13636
+ await readFile(join(stateDir, "native-stop-state.json"), "utf-8"),
13637
+ ) as { sessions?: Record<string, unknown> };
13638
+ assert.deepEqual(Object.keys(persisted.sessions ?? {}), ["omx-canonical"]);
12060
13639
  } finally {
12061
13640
  await rm(cwd, { recursive: true, force: true });
12062
13641
  }
12063
13642
  });
12064
13643
 
12065
- it("preserves legitimate session-scoped ultrawork blocking while reconciling root skill-active state", async () => {
12066
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-active-root-skill-session-mode-"));
13644
+ it("suppresses duplicate ultrawork Stop replays while stop_hook_active stays true", async () => {
13645
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultrawork-repeat-"));
12067
13646
  try {
12068
13647
  const stateDir = join(cwd, ".omx", "state");
12069
- const sessionId = "sess-stop-active-ultrawork";
12070
- await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
12071
- await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
12072
- await writeJson(join(stateDir, "sessions", sessionId, "ultrawork-state.json"), {
12073
- active: true,
12074
- mode: "ultrawork",
12075
- current_phase: "executing",
12076
- session_id: sessionId,
12077
- });
12078
- await writeJson(join(stateDir, "skill-active-state.json"), {
13648
+ await mkdir(join(stateDir, "sessions", "sess-stop-ultrawork-repeat"), { recursive: true });
13649
+ await writeJson(join(stateDir, "sessions", "sess-stop-ultrawork-repeat", "ultrawork-state.json"), {
12079
13650
  active: true,
12080
- skill: "ultrawork",
12081
- phase: "planning",
12082
- source: "keyword-detector",
12083
- active_skills: [
12084
- { skill: "ultrawork", phase: "planning", active: true, session_id: sessionId },
12085
- ],
13651
+ current_phase: "executing",
12086
13652
  });
12087
13653
 
12088
- const result = await dispatchCodexNativeHook(
13654
+ const first = await dispatchCodexNativeHook(
12089
13655
  {
12090
13656
  hook_event_name: "Stop",
12091
13657
  cwd,
12092
- session_id: sessionId,
12093
- thread_id: "thread-stop-active-ultrawork",
12094
- turn_id: "turn-stop-active-ultrawork-1",
13658
+ session_id: "sess-stop-ultrawork-repeat",
13659
+ thread_id: "thread-stop-ultrawork-repeat",
13660
+ turn_id: "turn-stop-ultrawork-repeat-1",
12095
13661
  },
12096
13662
  { cwd },
12097
13663
  );
12098
13664
 
12099
- assert.equal(result.omxEventName, "stop");
12100
- assert.deepEqual(result.outputJson, {
13665
+ const repeated = await dispatchCodexNativeHook(
13666
+ {
13667
+ hook_event_name: "Stop",
13668
+ cwd,
13669
+ session_id: "sess-stop-ultrawork-repeat",
13670
+ thread_id: "thread-stop-ultrawork-repeat",
13671
+ turn_id: "turn-stop-ultrawork-repeat-1",
13672
+ stop_hook_active: true,
13673
+ },
13674
+ { cwd },
13675
+ );
13676
+
13677
+ const fresh = await dispatchCodexNativeHook(
13678
+ {
13679
+ hook_event_name: "Stop",
13680
+ cwd,
13681
+ session_id: "sess-stop-ultrawork-repeat",
13682
+ thread_id: "thread-stop-ultrawork-repeat",
13683
+ turn_id: "turn-stop-ultrawork-repeat-2",
13684
+ stop_hook_active: true,
13685
+ },
13686
+ { cwd },
13687
+ );
13688
+
13689
+ assert.equal(first.omxEventName, "stop");
13690
+ assert.deepEqual(repeated.outputJson, null);
13691
+ assert.equal(fresh.omxEventName, "stop");
13692
+ assert.deepEqual(fresh.outputJson, {
12101
13693
  decision: "block",
12102
13694
  reason: "OMX ultrawork is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
12103
13695
  stopReason: "ultrawork_executing",
12104
13696
  systemMessage: "OMX ultrawork is still active (phase: executing).",
12105
13697
  });
12106
-
12107
- const rootSkillState = JSON.parse(
12108
- await readFile(join(stateDir, "skill-active-state.json"), "utf-8"),
12109
- ) as { active?: boolean; active_skills?: Array<{ skill?: string }> };
12110
- assert.equal(rootSkillState.active, true);
12111
- assert.deepEqual(rootSkillState.active_skills?.map((entry) => entry.skill), ["ultrawork"]);
12112
13698
  } finally {
12113
13699
  await rm(cwd, { recursive: true, force: true });
12114
13700
  }
12115
13701
  });
12116
13702
 
12117
- it("reconciles stale root skill-active state under OMX_ROOT boxed state", async () => {
12118
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-boxed-source-"));
12119
- const omxRoot = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-boxed-root-"));
12120
- const previousOmxRoot = process.env.OMX_ROOT;
13703
+ it("re-blocks active ralplan skill state on repeated Stop hooks", async () => {
13704
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-repeat-"));
12121
13705
  try {
12122
- process.env.OMX_ROOT = omxRoot;
12123
- const stateDir = join(omxRoot, ".omx", "state");
12124
- const sourceStateDir = join(cwd, ".omx", "state");
12125
- const sessionId = "sess-stop-boxed-ralplan";
12126
- await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
12127
- await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
12128
- await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
12129
- active: false,
12130
- mode: "ralplan",
12131
- current_phase: "completed",
12132
- lifecycle_outcome: "finished",
12133
- run_outcome: "finish",
12134
- });
12135
- await writeJson(join(stateDir, "skill-active-state.json"), {
13706
+ const stateDir = join(cwd, ".omx", "state");
13707
+ await mkdir(join(stateDir, "sessions", "sess-stop-skill-repeat"), { recursive: true });
13708
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-skill-repeat" });
13709
+ await writeJson(join(stateDir, "sessions", "sess-stop-skill-repeat", "skill-active-state.json"), {
12136
13710
  active: true,
12137
- skill: "ultrawork",
13711
+ skill: "ralplan",
12138
13712
  phase: "planning",
12139
- source: "keyword-detector",
12140
- active_skills: [
12141
- { skill: "ultrawork", phase: "planning", active: true },
12142
- ],
13713
+ });
13714
+ await writeJson(join(stateDir, "sessions", "sess-stop-skill-repeat", "ralplan-state.json"), {
13715
+ active: true,
13716
+ current_phase: "planning",
12143
13717
  });
12144
13718
 
12145
- const result = await dispatchCodexNativeHook(
13719
+ await dispatchCodexNativeHook(
12146
13720
  {
12147
13721
  hook_event_name: "Stop",
12148
13722
  cwd,
12149
- session_id: sessionId,
12150
- thread_id: "thread-stop-boxed-ralplan",
12151
- turn_id: "turn-stop-boxed-ralplan-1",
12152
- last_assistant_message: "Done.",
13723
+ session_id: "sess-stop-skill-repeat",
13724
+ thread_id: "thread-stop-skill-repeat",
13725
+ turn_id: "turn-stop-skill-repeat-1",
12153
13726
  },
12154
13727
  { cwd },
12155
13728
  );
12156
13729
 
12157
- assert.equal(result.omxEventName, "stop");
12158
- assert.equal(result.outputJson, null);
13730
+ const repeated = await dispatchCodexNativeHook(
13731
+ {
13732
+ hook_event_name: "Stop",
13733
+ cwd,
13734
+ session_id: "sess-stop-skill-repeat",
13735
+ thread_id: "thread-stop-skill-repeat",
13736
+ turn_id: "turn-stop-skill-repeat-1",
13737
+ stop_hook_active: true,
13738
+ },
13739
+ { cwd },
13740
+ );
12159
13741
 
12160
- const boxedRootSkillState = JSON.parse(
12161
- await readFile(join(stateDir, "skill-active-state.json"), "utf-8"),
12162
- ) as { active?: boolean; active_skills?: unknown[]; reconciliation_reason?: string };
12163
- assert.equal(boxedRootSkillState.active, false);
12164
- assert.deepEqual(boxedRootSkillState.active_skills, []);
12165
- assert.equal(boxedRootSkillState.reconciliation_reason, "stop_hook_session_state_terminal");
12166
- assert.equal(existsSync(join(sourceStateDir, "skill-active-state.json")), false);
13742
+ assert.equal(repeated.omxEventName, "stop");
13743
+ assert.equal(repeated.outputJson?.decision, "block");
13744
+ assert.match(String(repeated.outputJson?.reason ?? ""), /Status: continue_from_artifact/);
13745
+ assert.match(String(repeated.outputJson?.reason ?? ""), /continue from the current ralplan artifact/i);
13746
+ assert.equal(repeated.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
12167
13747
  } finally {
12168
- if (previousOmxRoot === undefined) delete process.env.OMX_ROOT;
12169
- else process.env.OMX_ROOT = previousOmxRoot;
12170
13748
  await rm(cwd, { recursive: true, force: true });
12171
- await rm(omxRoot, { recursive: true, force: true });
12172
13749
  }
12173
13750
  });
12174
13751
 
12175
- it("auto-continues native Stop for permission-seeking prompts even outside OMX runtime", async () => {
12176
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-plain-session-"));
13752
+ it("blocks implementation writes while ralplan is active without execution handoff", async () => {
13753
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-block-"));
12177
13754
  try {
12178
- await dispatchCodexNativeHook(
12179
- {
12180
- hook_event_name: "SessionStart",
12181
- cwd,
12182
- session_id: "plain-stop-session",
12183
- },
12184
- {
12185
- cwd,
12186
- sessionOwnerPid: process.pid,
12187
- },
12188
- );
13755
+ const stateDir = join(cwd, ".omx", "state");
13756
+ const sessionId = "sess-ralplan-pretool-block";
13757
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
13758
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
13759
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
13760
+ active: true,
13761
+ skill: "ralplan",
13762
+ phase: "planning",
13763
+ session_id: sessionId,
13764
+ active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
13765
+ });
13766
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
13767
+ active: true,
13768
+ mode: "ralplan",
13769
+ current_phase: "critic-review",
13770
+ session_id: sessionId,
13771
+ });
12189
13772
 
12190
13773
  const result = await dispatchCodexNativeHook(
12191
13774
  {
12192
- hook_event_name: "Stop",
13775
+ hook_event_name: "PreToolUse",
12193
13776
  cwd,
12194
- session_id: "plain-stop-session",
12195
- thread_id: "plain-thread",
12196
- turn_id: "plain-turn-1",
12197
- last_assistant_message: "If you want, I can continue with the cleanup from here.",
13777
+ session_id: sessionId,
13778
+ thread_id: "thread-ralplan-pretool-block",
13779
+ tool_name: "Edit",
13780
+ tool_input: { file_path: "src/runtime.ts" },
12198
13781
  },
12199
13782
  { cwd },
12200
13783
  );
12201
13784
 
12202
- assert.equal(result.omxEventName, "stop");
12203
- assert.deepEqual(result.outputJson, {
12204
- decision: "block",
12205
- reason: DEFAULT_AUTO_NUDGE_RESPONSE,
12206
- stopReason: "auto_nudge",
12207
- systemMessage:
12208
- "OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
12209
- });
13785
+ assert.equal(result.omxEventName, "pre-tool-use");
13786
+ assert.equal(result.outputJson?.decision, "block");
13787
+ assert.match(String(result.outputJson?.reason ?? ""), /Ralplan is active .*implementation\/write tools are blocked/i);
13788
+ assert.match(
13789
+ String((result.outputJson?.hookSpecificOutput as { additionalContext?: string } | undefined)?.additionalContext ?? ""),
13790
+ /\$ultragoal.*\$team.*\$ralph/i,
13791
+ );
12210
13792
  } finally {
12211
13793
  await rm(cwd, { recursive: true, force: true });
12212
13794
  }
12213
13795
  });
12214
13796
 
12215
- it("re-fires team Stop output for a later fresh Stop reply while the team is still active", async () => {
12216
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-refire-"));
13797
+ it("blocks implementation writes while Autopilot is supervising ralplan without handoff", async () => {
13798
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-pretool-block-"));
12217
13799
  try {
12218
13800
  const stateDir = join(cwd, ".omx", "state");
12219
- await mkdir(stateDir, { recursive: true });
12220
- await writeJson(join(stateDir, "team-state.json"), {
13801
+ const sessionId = "sess-autopilot-ralplan-pretool-block";
13802
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
13803
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
13804
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
12221
13805
  active: true,
12222
- current_phase: "team-exec",
12223
- team_name: "review-team",
12224
- session_id: "sess-stop-team-refire",
12225
- thread_id: "thread-stop-team-refire",
12226
- });
12227
- await writeJson(join(stateDir, "team", "review-team", "phase.json"), {
12228
- current_phase: "team-verify",
12229
- max_fix_attempts: 3,
12230
- current_fix_attempt: 0,
12231
- transitions: [],
12232
- updated_at: new Date().toISOString(),
13806
+ skill: "autopilot",
13807
+ phase: "ralplan",
13808
+ session_id: sessionId,
13809
+ active_skills: [{ skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId }],
12233
13810
  });
12234
-
12235
- await dispatchCodexNativeHook(
12236
- {
12237
- hook_event_name: "Stop",
12238
- cwd,
12239
- session_id: "sess-stop-team-refire",
12240
- thread_id: "thread-stop-team-refire",
12241
- turn_id: "turn-stop-team-refire-1",
13811
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
13812
+ active: true,
13813
+ mode: "autopilot",
13814
+ current_phase: "ralplan",
13815
+ session_id: sessionId,
13816
+ state: {
13817
+ handoff_artifacts: {
13818
+ ralplan_consensus_gate: { required: true, complete: false },
13819
+ },
12242
13820
  },
12243
- { cwd },
12244
- );
13821
+ });
12245
13822
 
12246
13823
  const result = await dispatchCodexNativeHook(
12247
13824
  {
12248
- hook_event_name: "Stop",
13825
+ hook_event_name: "PreToolUse",
12249
13826
  cwd,
12250
- session_id: "sess-stop-team-refire",
12251
- thread_id: "thread-stop-team-refire",
12252
- turn_id: "turn-stop-team-refire-2",
12253
- stop_hook_active: true,
13827
+ session_id: sessionId,
13828
+ thread_id: "thread-autopilot-ralplan-pretool-block",
13829
+ tool_name: "Edit",
13830
+ tool_input: { file_path: "src/runtime.ts" },
12254
13831
  },
12255
13832
  { cwd },
12256
13833
  );
12257
13834
 
12258
- assert.equal(result.omxEventName, "stop");
12259
- assert.deepEqual(result.outputJson, {
12260
- decision: "block",
12261
- reason:
12262
- `OMX team pipeline is still active (review-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
12263
- stopReason: "team_team-verify",
12264
- systemMessage: "OMX team pipeline is still active at phase team-verify.",
12265
- });
13835
+ assert.equal(result.omxEventName, "pre-tool-use");
13836
+ assert.equal(result.outputJson?.decision, "block");
13837
+ assert.match(String(result.outputJson?.reason ?? ""), /Ralplan is active .*implementation\/write tools are blocked/i);
12266
13838
  } finally {
12267
13839
  await rm(cwd, { recursive: true, force: true });
12268
13840
  }
12269
13841
  });
12270
13842
 
12271
- it("suppresses duplicate team Stop replays across native/canonical session-id drift", async () => {
12272
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-session-drift-"));
13843
+ it("allows implementation writes when terminal Autopilot run-state shadows stale supervised ralplan state", async () => {
13844
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-terminal-pretool-"));
12273
13845
  try {
12274
13846
  const stateDir = join(cwd, ".omx", "state");
12275
- await mkdir(join(stateDir, "sessions", "omx-canonical"), { recursive: true });
12276
- process.env.OMX_SESSION_ID = "omx-canonical";
12277
- await writeJson(join(stateDir, "session.json"), {
12278
- session_id: "omx-canonical",
12279
- native_session_id: "codex-native",
13847
+ const sessionId = "sess-autopilot-ralplan-terminal-pretool";
13848
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
13849
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
13850
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
13851
+ active: true,
13852
+ skill: "autopilot",
13853
+ phase: "ralplan",
13854
+ session_id: sessionId,
13855
+ active_skills: [{ skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId }],
12280
13856
  });
12281
- await writeJson(join(stateDir, "sessions", "omx-canonical", "team-state.json"), {
13857
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
12282
13858
  active: true,
12283
- current_phase: "starting",
12284
- team_name: "current-team",
12285
- session_id: "omx-canonical",
13859
+ mode: "autopilot",
13860
+ current_phase: "ralplan",
13861
+ session_id: sessionId,
12286
13862
  });
12287
- await writeJson(join(stateDir, "team", "current-team", "phase.json"), {
12288
- current_phase: "team-verify",
12289
- max_fix_attempts: 3,
12290
- current_fix_attempt: 1,
12291
- transitions: [],
12292
- updated_at: new Date().toISOString(),
13863
+ await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
13864
+ version: 1,
13865
+ active: false,
13866
+ mode: "autopilot",
13867
+ outcome: "finish",
13868
+ lifecycle_outcome: "finished",
13869
+ current_phase: "complete",
13870
+ completed_at: "2026-05-30T00:00:00.000Z",
13871
+ updated_at: "2026-05-30T00:00:00.000Z",
12293
13872
  });
12294
13873
 
12295
- await dispatchCodexNativeHook(
13874
+ const result = await dispatchCodexNativeHook(
12296
13875
  {
12297
- hook_event_name: "Stop",
13876
+ hook_event_name: "PreToolUse",
12298
13877
  cwd,
12299
- session_id: "codex-native",
12300
- thread_id: "thread-stop-team-drift",
12301
- turn_id: "turn-stop-team-drift-1",
13878
+ session_id: sessionId,
13879
+ thread_id: "thread-autopilot-ralplan-terminal-pretool",
13880
+ tool_name: "Edit",
13881
+ tool_input: { file_path: "src/runtime.ts" },
12302
13882
  },
12303
13883
  { cwd },
12304
13884
  );
12305
13885
 
12306
- const duplicate = await dispatchCodexNativeHook(
12307
- {
12308
- hook_event_name: "Stop",
12309
- cwd,
12310
- session_id: "omx-canonical",
12311
- thread_id: "thread-stop-team-drift",
12312
- turn_id: "turn-stop-team-drift-1",
12313
- stop_hook_active: true,
12314
- },
12315
- { cwd },
12316
- );
13886
+ assert.equal(result.omxEventName, "pre-tool-use");
13887
+ assert.equal(result.outputJson, null);
13888
+ } finally {
13889
+ await rm(cwd, { recursive: true, force: true });
13890
+ }
13891
+ });
12317
13892
 
12318
- assert.equal(duplicate.omxEventName, "stop");
12319
- assert.deepEqual(duplicate.outputJson, {
12320
- decision: "block",
12321
- reason:
12322
- `OMX team pipeline is still active (current-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
12323
- stopReason: "team_team-verify",
12324
- systemMessage: "OMX team pipeline is still active at phase team-verify.",
13893
+ it("blocks bash implementation writes while Autopilot is supervising ralplan without handoff", async () => {
13894
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-pretool-bash-block-"));
13895
+ try {
13896
+ const stateDir = join(cwd, ".omx", "state");
13897
+ const sessionId = "sess-autopilot-ralplan-pretool-bash-block";
13898
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
13899
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
13900
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
13901
+ active: true,
13902
+ skill: "autopilot",
13903
+ phase: "ralplan",
13904
+ session_id: sessionId,
13905
+ active_skills: [{ skill: "autopilot", phase: "ralplan", active: true, session_id: sessionId }],
13906
+ });
13907
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
13908
+ active: true,
13909
+ mode: "autopilot",
13910
+ current_phase: "ralplan",
13911
+ session_id: sessionId,
12325
13912
  });
12326
13913
 
12327
- const fresh = await dispatchCodexNativeHook(
13914
+ const result = await dispatchCodexNativeHook(
12328
13915
  {
12329
- hook_event_name: "Stop",
13916
+ hook_event_name: "PreToolUse",
12330
13917
  cwd,
12331
- session_id: "omx-canonical",
12332
- thread_id: "thread-stop-team-drift",
12333
- turn_id: "turn-stop-team-drift-2",
12334
- stop_hook_active: true,
13918
+ session_id: sessionId,
13919
+ thread_id: "thread-autopilot-ralplan-pretool-bash-block",
13920
+ tool_name: "Bash",
13921
+ tool_input: { command: "cat <<'EOF' > src/runtime.ts\nimplementation\nEOF" },
12335
13922
  },
12336
13923
  { cwd },
12337
13924
  );
12338
13925
 
12339
- assert.equal(fresh.omxEventName, "stop");
12340
- assert.deepEqual(fresh.outputJson, {
12341
- decision: "block",
12342
- reason:
12343
- `OMX team pipeline is still active (current-team) at phase team-verify; continue coordinating until the team reaches a terminal phase.${TEAM_STOP_COMMIT_GUIDANCE}`,
12344
- stopReason: "team_team-verify",
12345
- systemMessage: "OMX team pipeline is still active at phase team-verify.",
12346
- });
12347
-
12348
- const persisted = JSON.parse(
12349
- await readFile(join(stateDir, "native-stop-state.json"), "utf-8"),
12350
- ) as { sessions?: Record<string, unknown> };
12351
- assert.deepEqual(Object.keys(persisted.sessions ?? {}), ["omx-canonical"]);
13926
+ assert.equal(result.omxEventName, "pre-tool-use");
13927
+ assert.equal(result.outputJson?.decision, "block");
13928
+ assert.match(String(result.outputJson?.reason ?? ""), /Ralplan is active .*implementation\/write tools are blocked/i);
12352
13929
  } finally {
12353
13930
  await rm(cwd, { recursive: true, force: true });
12354
13931
  }
12355
13932
  });
12356
13933
 
12357
- it("suppresses duplicate ultrawork Stop replays while stop_hook_active stays true", async () => {
12358
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ultrawork-repeat-"));
13934
+ it("allows ralplan planning artifact writes without execution handoff", async () => {
13935
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-artifact-"));
12359
13936
  try {
12360
13937
  const stateDir = join(cwd, ".omx", "state");
12361
- await mkdir(join(stateDir, "sessions", "sess-stop-ultrawork-repeat"), { recursive: true });
12362
- await writeJson(join(stateDir, "sessions", "sess-stop-ultrawork-repeat", "ultrawork-state.json"), {
13938
+ const sessionId = "sess-ralplan-pretool-artifact";
13939
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
13940
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
13941
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
12363
13942
  active: true,
12364
- current_phase: "executing",
13943
+ skill: "ralplan",
13944
+ phase: "planning",
13945
+ session_id: sessionId,
13946
+ active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
13947
+ });
13948
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
13949
+ active: true,
13950
+ mode: "ralplan",
13951
+ current_phase: "planning",
13952
+ session_id: sessionId,
12365
13953
  });
12366
13954
 
12367
- const first = await dispatchCodexNativeHook(
13955
+ const result = await dispatchCodexNativeHook(
12368
13956
  {
12369
- hook_event_name: "Stop",
13957
+ hook_event_name: "PreToolUse",
12370
13958
  cwd,
12371
- session_id: "sess-stop-ultrawork-repeat",
12372
- thread_id: "thread-stop-ultrawork-repeat",
12373
- turn_id: "turn-stop-ultrawork-repeat-1",
13959
+ session_id: sessionId,
13960
+ thread_id: "thread-ralplan-pretool-artifact",
13961
+ tool_name: "Write",
13962
+ tool_input: { file_path: ".omx/plans/prd-issue-2603.md" },
12374
13963
  },
12375
13964
  { cwd },
12376
13965
  );
12377
13966
 
12378
- const repeated = await dispatchCodexNativeHook(
12379
- {
12380
- hook_event_name: "Stop",
12381
- cwd,
12382
- session_id: "sess-stop-ultrawork-repeat",
12383
- thread_id: "thread-stop-ultrawork-repeat",
12384
- turn_id: "turn-stop-ultrawork-repeat-1",
12385
- stop_hook_active: true,
12386
- },
12387
- { cwd },
12388
- );
13967
+ assert.equal(result.omxEventName, "pre-tool-use");
13968
+ assert.equal(result.outputJson, null);
13969
+ } finally {
13970
+ await rm(cwd, { recursive: true, force: true });
13971
+ }
13972
+ });
12389
13973
 
12390
- const fresh = await dispatchCodexNativeHook(
13974
+ it("blocks bash implementation writes while ralplan is active without execution handoff", async () => {
13975
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-bash-block-"));
13976
+ try {
13977
+ const stateDir = join(cwd, ".omx", "state");
13978
+ const sessionId = "sess-ralplan-pretool-bash-block";
13979
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
13980
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
13981
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
13982
+ active: true,
13983
+ skill: "ralplan",
13984
+ phase: "planning",
13985
+ session_id: sessionId,
13986
+ active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
13987
+ });
13988
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
13989
+ active: true,
13990
+ mode: "ralplan",
13991
+ current_phase: "planning",
13992
+ session_id: sessionId,
13993
+ });
13994
+
13995
+ const result = await dispatchCodexNativeHook(
12391
13996
  {
12392
- hook_event_name: "Stop",
13997
+ hook_event_name: "PreToolUse",
12393
13998
  cwd,
12394
- session_id: "sess-stop-ultrawork-repeat",
12395
- thread_id: "thread-stop-ultrawork-repeat",
12396
- turn_id: "turn-stop-ultrawork-repeat-2",
12397
- stop_hook_active: true,
13999
+ session_id: sessionId,
14000
+ thread_id: "thread-ralplan-pretool-bash-block",
14001
+ tool_name: "Bash",
14002
+ tool_input: { command: "cat <<'EOF' > src/runtime.ts\nimplementation\nEOF" },
12398
14003
  },
12399
14004
  { cwd },
12400
14005
  );
12401
14006
 
12402
- assert.equal(first.omxEventName, "stop");
12403
- assert.deepEqual(repeated.outputJson, null);
12404
- assert.equal(fresh.omxEventName, "stop");
12405
- assert.deepEqual(fresh.outputJson, {
12406
- decision: "block",
12407
- reason: "OMX ultrawork is still active (phase: executing); continue the task and gather fresh verification evidence before stopping.",
12408
- stopReason: "ultrawork_executing",
12409
- systemMessage: "OMX ultrawork is still active (phase: executing).",
12410
- });
14007
+ assert.equal(result.omxEventName, "pre-tool-use");
14008
+ assert.equal(result.outputJson?.decision, "block");
14009
+ assert.match(String(result.outputJson?.reason ?? ""), /Ralplan is active .*implementation\/write tools are blocked/i);
12411
14010
  } finally {
12412
14011
  await rm(cwd, { recursive: true, force: true });
12413
14012
  }
12414
14013
  });
12415
14014
 
12416
- it("re-blocks active ralplan skill state on repeated Stop hooks", async () => {
12417
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-repeat-"));
14015
+ it("allows bash planning artifact writes while ralplan is active without execution handoff", async () => {
14016
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-bash-artifact-"));
12418
14017
  try {
12419
14018
  const stateDir = join(cwd, ".omx", "state");
12420
- await mkdir(join(stateDir, "sessions", "sess-stop-skill-repeat"), { recursive: true });
12421
- await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-skill-repeat" });
12422
- await writeJson(join(stateDir, "sessions", "sess-stop-skill-repeat", "skill-active-state.json"), {
14019
+ const sessionId = "sess-ralplan-pretool-bash-artifact";
14020
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
14021
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
14022
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
12423
14023
  active: true,
12424
14024
  skill: "ralplan",
12425
14025
  phase: "planning",
14026
+ session_id: sessionId,
14027
+ active_skills: [{ skill: "ralplan", phase: "planning", active: true, session_id: sessionId }],
12426
14028
  });
12427
- await writeJson(join(stateDir, "sessions", "sess-stop-skill-repeat", "ralplan-state.json"), {
14029
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
12428
14030
  active: true,
14031
+ mode: "ralplan",
12429
14032
  current_phase: "planning",
14033
+ session_id: sessionId,
12430
14034
  });
12431
14035
 
12432
- await dispatchCodexNativeHook(
14036
+ const result = await dispatchCodexNativeHook(
12433
14037
  {
12434
- hook_event_name: "Stop",
14038
+ hook_event_name: "PreToolUse",
12435
14039
  cwd,
12436
- session_id: "sess-stop-skill-repeat",
12437
- thread_id: "thread-stop-skill-repeat",
12438
- turn_id: "turn-stop-skill-repeat-1",
14040
+ session_id: sessionId,
14041
+ thread_id: "thread-ralplan-pretool-bash-artifact",
14042
+ tool_name: "Bash",
14043
+ tool_input: { command: "cat <<'EOF' > .omx/plans/prd-issue-2603.md\nplanning\nEOF" },
12439
14044
  },
12440
14045
  { cwd },
12441
14046
  );
12442
14047
 
12443
- const repeated = await dispatchCodexNativeHook(
14048
+ assert.equal(result.omxEventName, "pre-tool-use");
14049
+ assert.equal(result.outputJson, null);
14050
+ } finally {
14051
+ await rm(cwd, { recursive: true, force: true });
14052
+ }
14053
+ });
14054
+
14055
+ it("allows implementation writes when an explicit execution handoff is active", async () => {
14056
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ralplan-pretool-handoff-"));
14057
+ try {
14058
+ const stateDir = join(cwd, ".omx", "state");
14059
+ const sessionId = "sess-ralplan-pretool-handoff";
14060
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
14061
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
14062
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
14063
+ active: true,
14064
+ skill: "ultragoal",
14065
+ phase: "planning",
14066
+ session_id: sessionId,
14067
+ active_skills: [
14068
+ { skill: "ralplan", phase: "planning", active: true, session_id: sessionId },
14069
+ { skill: "ultragoal", phase: "planning", active: true, session_id: sessionId },
14070
+ ],
14071
+ });
14072
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
14073
+ active: true,
14074
+ mode: "ralplan",
14075
+ current_phase: "complete",
14076
+ session_id: sessionId,
14077
+ });
14078
+ await writeJson(join(stateDir, "sessions", sessionId, "ultragoal-state.json"), {
14079
+ active: true,
14080
+ mode: "ultragoal",
14081
+ current_phase: "planning",
14082
+ session_id: sessionId,
14083
+ });
14084
+
14085
+ const result = await dispatchCodexNativeHook(
12444
14086
  {
12445
- hook_event_name: "Stop",
14087
+ hook_event_name: "PreToolUse",
12446
14088
  cwd,
12447
- session_id: "sess-stop-skill-repeat",
12448
- thread_id: "thread-stop-skill-repeat",
12449
- turn_id: "turn-stop-skill-repeat-1",
12450
- stop_hook_active: true,
14089
+ session_id: sessionId,
14090
+ thread_id: "thread-ralplan-pretool-handoff",
14091
+ tool_name: "Edit",
14092
+ tool_input: { file_path: "src/runtime.ts" },
12451
14093
  },
12452
14094
  { cwd },
12453
14095
  );
12454
14096
 
12455
- assert.equal(repeated.omxEventName, "stop");
12456
- assert.equal(repeated.outputJson?.decision, "block");
12457
- assert.match(String(repeated.outputJson?.reason ?? ""), /Status: continue_from_artifact/);
12458
- assert.match(String(repeated.outputJson?.reason ?? ""), /continue from the current ralplan artifact/i);
12459
- assert.equal(repeated.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
14097
+ assert.equal(result.omxEventName, "pre-tool-use");
14098
+ assert.equal(result.outputJson, null);
12460
14099
  } finally {
12461
14100
  await rm(cwd, { recursive: true, force: true });
12462
14101
  }
@@ -13091,6 +14730,46 @@ describe("codex native hook triage integration", () => {
13091
14730
  }
13092
14731
  });
13093
14732
 
14733
+ it("makes bare autopilot command activation observable in state and prompt guidance", async () => {
14734
+ const cwd = await mkdtemp(join(tmpdir(), "omx-autopilot-bare-observable-"));
14735
+ try {
14736
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
14737
+ await writeSessionStart(cwd, "sess-autopilot-bare-observable");
14738
+
14739
+ const result = await dispatchCodexNativeHook(
14740
+ {
14741
+ hook_event_name: "UserPromptSubmit",
14742
+ cwd,
14743
+ session_id: "sess-autopilot-bare-observable",
14744
+ thread_id: "thread-autopilot-bare-observable",
14745
+ turn_id: "turn-autopilot-bare-observable",
14746
+ prompt: "run autopilot",
14747
+ },
14748
+ { cwd },
14749
+ );
14750
+
14751
+ assert.equal(result.skillState?.skill, "autopilot");
14752
+ assert.equal(result.skillState?.phase, "deep-interview");
14753
+ assert.equal(result.skillState?.initialized_state_path, ".omx/state/sessions/sess-autopilot-bare-observable/autopilot-state.json");
14754
+
14755
+ const additionalContext = String(
14756
+ (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
14757
+ );
14758
+ assert.match(additionalContext, /detected workflow keyword "autopilot" -> autopilot/);
14759
+ assert.doesNotMatch(additionalContext, /multi-step goal with no workflow keyword/);
14760
+
14761
+ const statePath = join(cwd, ".omx", "state", "sessions", "sess-autopilot-bare-observable", "autopilot-state.json");
14762
+ const modeState = JSON.parse(await readFile(statePath, "utf-8")) as {
14763
+ active: boolean;
14764
+ current_phase: string;
14765
+ };
14766
+ assert.equal(modeState.active, true);
14767
+ assert.equal(modeState.current_phase, "deep-interview");
14768
+ } finally {
14769
+ await rm(cwd, { recursive: true, force: true });
14770
+ }
14771
+ });
14772
+
13094
14773
  // ── Group 2: HEAVY injection ─────────────────────────────────────────────
13095
14774
 
13096
14775
  it("injects HEAVY advisory and writes prompt-routing-state for a multi-step goal prompt", async () => {