oh-my-codex 0.17.2 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/Cargo.lock +13 -5
  2. package/Cargo.toml +2 -1
  3. package/README.md +1 -0
  4. package/crates/omx-api/Cargo.toml +19 -0
  5. package/crates/omx-api/src/lib.rs +2940 -0
  6. package/crates/omx-api/src/main.rs +10 -0
  7. package/crates/omx-api/tests/cli.rs +558 -0
  8. package/crates/omx-explore/src/main.rs +4 -0
  9. package/crates/omx-sparkshell/src/codex_bridge.rs +437 -123
  10. package/crates/omx-sparkshell/src/exec.rs +4 -0
  11. package/crates/omx-sparkshell/src/main.rs +738 -29
  12. package/crates/omx-sparkshell/src/prompt.rs +25 -3
  13. package/crates/omx-sparkshell/src/redaction.rs +241 -0
  14. package/crates/omx-sparkshell/tests/execution.rs +479 -238
  15. package/dist/cli/__tests__/api.test.d.ts +2 -0
  16. package/dist/cli/__tests__/api.test.d.ts.map +1 -0
  17. package/dist/cli/__tests__/api.test.js +175 -0
  18. package/dist/cli/__tests__/api.test.js.map +1 -0
  19. package/dist/cli/__tests__/ask.test.js +72 -5
  20. package/dist/cli/__tests__/ask.test.js.map +1 -1
  21. package/dist/cli/__tests__/autoresearch-goal.test.js +14 -1
  22. package/dist/cli/__tests__/autoresearch-goal.test.js.map +1 -1
  23. package/dist/cli/__tests__/doctor-warning-copy.test.js +51 -0
  24. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  25. package/dist/cli/__tests__/explore.test.js +23 -0
  26. package/dist/cli/__tests__/explore.test.js.map +1 -1
  27. package/dist/cli/__tests__/index.test.js +123 -5
  28. package/dist/cli/__tests__/index.test.js.map +1 -1
  29. package/dist/cli/__tests__/launch-fallback.test.js +76 -0
  30. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  31. package/dist/cli/__tests__/package-bin-contract.test.js +4 -3
  32. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  33. package/dist/cli/__tests__/question.test.js +45 -22
  34. package/dist/cli/__tests__/question.test.js.map +1 -1
  35. package/dist/cli/__tests__/setup-agents-overwrite.test.js +2 -0
  36. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  37. package/dist/cli/__tests__/setup-install-mode.test.js +138 -0
  38. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  39. package/dist/cli/__tests__/setup-scope.test.js +8 -2
  40. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  41. package/dist/cli/__tests__/sparkshell-cli.test.js +5 -0
  42. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  43. package/dist/cli/__tests__/version-sync-contract.test.js +4 -0
  44. package/dist/cli/__tests__/version-sync-contract.test.js.map +1 -1
  45. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +1 -1
  46. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  47. package/dist/cli/api.d.ts +26 -0
  48. package/dist/cli/api.d.ts.map +1 -0
  49. package/dist/cli/api.js +153 -0
  50. package/dist/cli/api.js.map +1 -0
  51. package/dist/cli/doctor.d.ts.map +1 -1
  52. package/dist/cli/doctor.js +39 -4
  53. package/dist/cli/doctor.js.map +1 -1
  54. package/dist/cli/explore.d.ts +2 -0
  55. package/dist/cli/explore.d.ts.map +1 -1
  56. package/dist/cli/explore.js +43 -1
  57. package/dist/cli/explore.js.map +1 -1
  58. package/dist/cli/index.d.ts +10 -4
  59. package/dist/cli/index.d.ts.map +1 -1
  60. package/dist/cli/index.js +128 -10
  61. package/dist/cli/index.js.map +1 -1
  62. package/dist/cli/native-assets.d.ts +2 -1
  63. package/dist/cli/native-assets.d.ts.map +1 -1
  64. package/dist/cli/native-assets.js +1 -0
  65. package/dist/cli/native-assets.js.map +1 -1
  66. package/dist/cli/setup.d.ts.map +1 -1
  67. package/dist/cli/setup.js +6 -1
  68. package/dist/cli/setup.js.map +1 -1
  69. package/dist/cli/sparkshell.d.ts.map +1 -1
  70. package/dist/cli/sparkshell.js +20 -3
  71. package/dist/cli/sparkshell.js.map +1 -1
  72. package/dist/config/generator.d.ts.map +1 -1
  73. package/dist/config/generator.js +90 -0
  74. package/dist/config/generator.js.map +1 -1
  75. package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts +2 -0
  76. package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts.map +1 -0
  77. package/dist/hooks/__tests__/best-practice-research-skill.test.js +27 -0
  78. package/dist/hooks/__tests__/best-practice-research-skill.test.js.map +1 -0
  79. package/dist/hooks/__tests__/keyword-detector.test.js +11 -0
  80. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  81. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +6 -0
  82. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  83. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +4 -0
  84. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
  85. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  86. package/dist/hooks/keyword-registry.js +1 -0
  87. package/dist/hooks/keyword-registry.js.map +1 -1
  88. package/dist/hud/__tests__/reconcile.test.js +2 -2
  89. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  90. package/dist/hud/__tests__/tmux.test.js +23 -18
  91. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  92. package/dist/hud/tmux.d.ts.map +1 -1
  93. package/dist/hud/tmux.js +7 -6
  94. package/dist/hud/tmux.js.map +1 -1
  95. package/dist/mcp/__tests__/bootstrap.test.js +75 -1
  96. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  97. package/dist/mcp/bootstrap.d.ts +3 -1
  98. package/dist/mcp/bootstrap.d.ts.map +1 -1
  99. package/dist/mcp/bootstrap.js +71 -2
  100. package/dist/mcp/bootstrap.js.map +1 -1
  101. package/dist/scripts/__tests__/codex-native-hook.test.js +737 -26
  102. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  103. package/dist/scripts/__tests__/notify-dispatcher.test.js +183 -1
  104. package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
  105. package/dist/scripts/__tests__/smoke-packed-install.test.js +4 -1
  106. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  107. package/dist/scripts/build-api.d.ts +2 -0
  108. package/dist/scripts/build-api.d.ts.map +1 -0
  109. package/dist/scripts/build-api.js +44 -0
  110. package/dist/scripts/build-api.js.map +1 -0
  111. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  112. package/dist/scripts/codex-native-hook.js +208 -8
  113. package/dist/scripts/codex-native-hook.js.map +1 -1
  114. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  115. package/dist/scripts/codex-native-pre-post.js +89 -24
  116. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  117. package/dist/scripts/notify-dispatcher.js +88 -0
  118. package/dist/scripts/notify-dispatcher.js.map +1 -1
  119. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  120. package/dist/scripts/notify-hook/team-dispatch.js +27 -9
  121. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  122. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  123. package/dist/scripts/notify-hook/team-leader-nudge.js +26 -11
  124. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  125. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +1 -0
  126. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  127. package/dist/scripts/notify-hook/team-tmux-guard.js +38 -0
  128. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  129. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  130. package/dist/scripts/notify-hook/team-worker-stop.js +27 -14
  131. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  132. package/dist/scripts/run-provider-advisor.js +9 -3
  133. package/dist/scripts/run-provider-advisor.js.map +1 -1
  134. package/dist/scripts/smoke-packed-install.d.ts +1 -1
  135. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  136. package/dist/scripts/smoke-packed-install.js +2 -0
  137. package/dist/scripts/smoke-packed-install.js.map +1 -1
  138. package/dist/team/__tests__/runtime.test.js +2 -2
  139. package/dist/team/__tests__/runtime.test.js.map +1 -1
  140. package/dist/team/__tests__/tmux-session.test.js +153 -25
  141. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  142. package/dist/team/tmux-session.d.ts +1 -0
  143. package/dist/team/tmux-session.d.ts.map +1 -1
  144. package/dist/team/tmux-session.js +55 -10
  145. package/dist/team/tmux-session.js.map +1 -1
  146. package/dist/utils/__tests__/agents-md.test.js +45 -1
  147. package/dist/utils/__tests__/agents-md.test.js.map +1 -1
  148. package/dist/utils/agents-md.d.ts +2 -0
  149. package/dist/utils/agents-md.d.ts.map +1 -1
  150. package/dist/utils/agents-md.js +19 -0
  151. package/dist/utils/agents-md.js.map +1 -1
  152. package/dist/verification/__tests__/ci-rust-gates.test.js +85 -10
  153. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  154. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +1 -0
  155. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  156. package/package.json +4 -3
  157. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  158. package/plugins/oh-my-codex/skills/best-practice-research/SKILL.md +83 -0
  159. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +1 -0
  160. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +1 -1
  161. package/prompts/researcher.md +15 -10
  162. package/skills/best-practice-research/SKILL.md +83 -0
  163. package/skills/deep-interview/SKILL.md +1 -0
  164. package/skills/ralplan/SKILL.md +1 -1
  165. package/src/scripts/__tests__/codex-native-hook.test.ts +810 -4
  166. package/src/scripts/__tests__/notify-dispatcher.test.ts +223 -1
  167. package/src/scripts/__tests__/smoke-packed-install.test.ts +8 -2
  168. package/src/scripts/build-api.ts +48 -0
  169. package/src/scripts/codex-native-hook.ts +262 -10
  170. package/src/scripts/codex-native-pre-post.ts +103 -24
  171. package/src/scripts/notify-dispatcher.ts +97 -0
  172. package/src/scripts/notify-hook/team-dispatch.ts +27 -8
  173. package/src/scripts/notify-hook/team-leader-nudge.ts +25 -11
  174. package/src/scripts/notify-hook/team-tmux-guard.ts +42 -0
  175. package/src/scripts/notify-hook/team-worker-stop.ts +24 -13
  176. package/src/scripts/run-provider-advisor.ts +11 -3
  177. package/src/scripts/smoke-packed-install.ts +2 -0
  178. package/templates/catalog-manifest.json +7 -0
@@ -62,7 +62,10 @@ async function writeJson(path: string, value: unknown): Promise<void> {
62
62
  await writeFile(path, JSON.stringify(value, null, 2));
63
63
  }
64
64
 
65
- function buildWorkerStopFakeTmux(tmuxLogPath: string, options: { failSend?: boolean } = {}): string {
65
+ function buildWorkerStopFakeTmux(
66
+ tmuxLogPath: string,
67
+ options: { failSend?: boolean; busyLeader?: boolean } = {},
68
+ ): string {
66
69
  return `#!/usr/bin/env bash
67
70
  set -eu
68
71
  echo "$@" >> "${tmuxLogPath}"
@@ -90,7 +93,7 @@ if [[ "$cmd" == "display-message" ]]; then
90
93
  exit 0
91
94
  fi
92
95
  if [[ "$cmd" == "capture-pane" ]]; then
93
- echo "› ready"
96
+ ${options.busyLeader ? 'echo "• Working… (esc to interrupt)"' : 'echo "› ready"'}
94
97
  exit 0
95
98
  fi
96
99
  if [[ "$cmd" == "send-keys" ]]; then
@@ -1701,6 +1704,71 @@ describe("codex native hook dispatch", () => {
1701
1704
  }
1702
1705
  });
1703
1706
 
1707
+ it("does not repeat Stop block when the last autoresearch-goal completion attempt reported objective mismatch", async () => {
1708
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autoresearch-mismatch-reported-stop-"));
1709
+ try {
1710
+ await writeJson(join(cwd, ".omx", "goals", "autoresearch", "mismatched-mission", "mission.json"), {
1711
+ version: 1,
1712
+ workflow: "autoresearch-goal",
1713
+ slug: "mismatched-mission",
1714
+ topic: "Passing research bound to another Codex goal",
1715
+ status: "passed",
1716
+ });
1717
+ await writeJson(join(cwd, ".omx", "goals", "autoresearch", "mismatched-mission", "completion.json"), {
1718
+ verdict: "pass",
1719
+ passed: true,
1720
+ });
1721
+
1722
+ const result = await dispatchCodexNativeHook({
1723
+ hook_event_name: "Stop",
1724
+ cwd,
1725
+ session_id: "sess-autoresearch-mismatch-reported-stop",
1726
+ thread_id: "thread-autoresearch-mismatch-reported-stop",
1727
+ last_assistant_message: [
1728
+ "I called get_goal and ran omx autoresearch-goal complete --slug mismatched-mission --codex-goal-json /tmp/snapshot.json.",
1729
+ "The autoresearch-goal completion failed with Codex goal objective mismatch, so I will not repeat the same complete command blindly in this thread.",
1730
+ ].join("\n"),
1731
+ }, { cwd });
1732
+
1733
+ assert.notEqual(result.outputJson?.decision, "block");
1734
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /autoresearch-goal complete --slug mismatched-mission/);
1735
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
1736
+ } finally {
1737
+ await rm(cwd, { recursive: true, force: true });
1738
+ }
1739
+ });
1740
+
1741
+ it("still blocks later autoresearch-goal completion claims after an objective mismatch if no mismatch is reported in the final answer", async () => {
1742
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autoresearch-mismatch-later-retry-stop-"));
1743
+ try {
1744
+ await writeJson(join(cwd, ".omx", "goals", "autoresearch", "retryable-mission", "mission.json"), {
1745
+ version: 1,
1746
+ workflow: "autoresearch-goal",
1747
+ slug: "retryable-mission",
1748
+ topic: "Passing research that can still retry with the correct snapshot",
1749
+ status: "passed",
1750
+ });
1751
+ await writeJson(join(cwd, ".omx", "goals", "autoresearch", "retryable-mission", "completion.json"), {
1752
+ verdict: "pass",
1753
+ passed: true,
1754
+ });
1755
+
1756
+ const result = await dispatchCodexNativeHook({
1757
+ hook_event_name: "Stop",
1758
+ cwd,
1759
+ session_id: "sess-autoresearch-mismatch-later-retry-stop",
1760
+ thread_id: "thread-autoresearch-mismatch-later-retry-stop",
1761
+ last_assistant_message: "Autoresearch goal complete; next call update_goal({status: \"complete\"}).",
1762
+ }, { cwd });
1763
+
1764
+ assert.equal(result.outputJson?.decision, "block");
1765
+ assert.match(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
1766
+ assert.match(JSON.stringify(result.outputJson), /omx autoresearch-goal complete --slug retryable-mission/);
1767
+ } finally {
1768
+ await rm(cwd, { recursive: true, force: true });
1769
+ }
1770
+ });
1771
+
1704
1772
  it("treats workflow keywords in native subagent prompt text as literal delegation text", async () => {
1705
1773
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-keyword-literal-"));
1706
1774
  try {
@@ -2863,6 +2931,52 @@ exit 0
2863
2931
  }
2864
2932
  });
2865
2933
 
2934
+ it("does not block Bash commands that only mention omx question in quoted arguments", async () => {
2935
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-quoted-mention-"));
2936
+ try {
2937
+ const result = await dispatchCodexNativeHook(
2938
+ {
2939
+ hook_event_name: "PreToolUse",
2940
+ cwd,
2941
+ tool_name: "Bash",
2942
+ tool_use_id: "tool-question-quoted-mention",
2943
+ tool_input: {
2944
+ command: `omx ultragoal create-goals --brief "Deep interview says omx question failed in tmux"`,
2945
+ },
2946
+ },
2947
+ { cwd },
2948
+ );
2949
+
2950
+ assert.equal(result.omxEventName, "pre-tool-use");
2951
+ assert.equal(result.outputJson, null);
2952
+ } finally {
2953
+ await rm(cwd, { recursive: true, force: true });
2954
+ }
2955
+ });
2956
+
2957
+ it("does not block Bash heredocs that only document omx question text", async () => {
2958
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-heredoc-mention-"));
2959
+ try {
2960
+ const result = await dispatchCodexNativeHook(
2961
+ {
2962
+ hook_event_name: "PreToolUse",
2963
+ cwd,
2964
+ tool_name: "Bash",
2965
+ tool_use_id: "tool-question-heredoc-mention",
2966
+ tool_input: {
2967
+ command: `cat > issue-notes.md <<'EOF'\nomx question failed in the attached tmux pane\nEOF`,
2968
+ },
2969
+ },
2970
+ { cwd },
2971
+ );
2972
+
2973
+ assert.equal(result.omxEventName, "pre-tool-use");
2974
+ assert.equal(result.outputJson, null);
2975
+ } finally {
2976
+ await rm(cwd, { recursive: true, force: true });
2977
+ }
2978
+ });
2979
+
2866
2980
  it("allows Bash omx question when the command preserves the leader-pane return hint", async () => {
2867
2981
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-allow-"));
2868
2982
  try {
@@ -4866,6 +4980,99 @@ exit 0
4866
4980
  }
4867
4981
  });
4868
4982
 
4983
+ it("does not treat non-MCP source output containing detector constants as MCP transport death", async () => {
4984
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-read-mcp-source-"));
4985
+ try {
4986
+ const result = await dispatchCodexNativeHook(
4987
+ {
4988
+ hook_event_name: "PostToolUse",
4989
+ cwd,
4990
+ tool_name: "Read",
4991
+ tool_use_id: "tool-read-mcp-source",
4992
+ tool_input: { file_path: "src/scripts/codex-native-pre-post.ts" },
4993
+ tool_response:
4994
+ "const MCP_TRANSPORT_FAILURE_PATTERNS = [/transport closed/i, /server disconnected/i];\nconst context = /\\bmcp\\b/i;",
4995
+ },
4996
+ { cwd },
4997
+ );
4998
+
4999
+ assert.equal(result.omxEventName, "post-tool-use");
5000
+ assert.equal(result.outputJson, null);
5001
+ } finally {
5002
+ await rm(cwd, { recursive: true, force: true });
5003
+ }
5004
+ });
5005
+
5006
+ it("does not treat non-MCP docs stdout mentioning closed MCP transport as transport death", async () => {
5007
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-docs-mcp-log-"));
5008
+ try {
5009
+ const result = await dispatchCodexNativeHook(
5010
+ {
5011
+ hook_event_name: "PostToolUse",
5012
+ cwd,
5013
+ tool_name: "ShellOutput",
5014
+ tool_use_id: "tool-docs-mcp-log",
5015
+ tool_response: JSON.stringify({
5016
+ stdout: "Troubleshooting note: MCP transport closed after the server disconnected in an old log.",
5017
+ stderr: "",
5018
+ }),
5019
+ },
5020
+ { cwd },
5021
+ );
5022
+
5023
+ assert.equal(result.omxEventName, "post-tool-use");
5024
+ assert.equal(result.outputJson, null);
5025
+ } finally {
5026
+ await rm(cwd, { recursive: true, force: true });
5027
+ }
5028
+ });
5029
+
5030
+ it("does not MCP-block non-MCP command output with unrelated stderr and MCP transport stdout", async () => {
5031
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-nonmcp-mixed-output-"));
5032
+ try {
5033
+ const result = await dispatchCodexNativeHook(
5034
+ {
5035
+ hook_event_name: "PostToolUse",
5036
+ cwd,
5037
+ tool_name: "ShellOutput",
5038
+ tool_use_id: "tool-nonmcp-mixed-output",
5039
+ tool_response: JSON.stringify({
5040
+ stdout: "captured log line: MCP transport closed before response",
5041
+ stderr: "grep: fixture.txt: No such file or directory",
5042
+ }),
5043
+ },
5044
+ { cwd },
5045
+ );
5046
+
5047
+ assert.equal(result.omxEventName, "post-tool-use");
5048
+ assert.equal(result.outputJson, null);
5049
+ } finally {
5050
+ await rm(cwd, { recursive: true, force: true });
5051
+ }
5052
+ });
5053
+
5054
+ it("still blocks MCP-like raw transport failures", async () => {
5055
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-raw-transport-"));
5056
+ try {
5057
+ const result = await dispatchCodexNativeHook(
5058
+ {
5059
+ hook_event_name: "PostToolUse",
5060
+ cwd,
5061
+ tool_name: "mcp__omx_state__state_write",
5062
+ tool_use_id: "tool-mcp-raw-transport",
5063
+ tool_response: "transport closed after server disconnected",
5064
+ },
5065
+ { cwd },
5066
+ );
5067
+
5068
+ assert.equal(result.omxEventName, "post-tool-use");
5069
+ assert.equal(result.outputJson?.decision, "block");
5070
+ assert.match(String(result.outputJson?.reason || ""), /lost its transport\/server connection/);
5071
+ } finally {
5072
+ await rm(cwd, { recursive: true, force: true });
5073
+ }
5074
+ });
5075
+
4869
5076
  it("returns PostToolUse MCP transport fallback guidance for clear MCP transport death", async () => {
4870
5077
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-transport-"));
4871
5078
  try {
@@ -5313,6 +5520,61 @@ exit 0
5313
5520
  }
5314
5521
  });
5315
5522
 
5523
+ it("requires Autopilot code review after a compact-boundary Stop exemption", async () => {
5524
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-review-compact-"));
5525
+ try {
5526
+ const stateDir = join(cwd, ".omx", "state");
5527
+ const sessionId = "sess-stop-autopilot-review-compact";
5528
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
5529
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
5530
+ active: true,
5531
+ mode: "autopilot",
5532
+ current_phase: "code-review",
5533
+ state: {
5534
+ phase_cycle: ["ralplan", "ralph", "code-review"],
5535
+ handoff_artifacts: {
5536
+ ralplan: ".omx/plans/prd-issue-2366.md",
5537
+ ralph: { verification: ["npm test"] },
5538
+ code_review: null,
5539
+ },
5540
+ review_verdict: null,
5541
+ },
5542
+ });
5543
+
5544
+ const compactBoundary = await dispatchCodexNativeHook(
5545
+ {
5546
+ hook_event_name: "Stop",
5547
+ cwd,
5548
+ session_id: sessionId,
5549
+ stop_reason: "context compact",
5550
+ },
5551
+ { cwd },
5552
+ );
5553
+ const resumedStop = await dispatchCodexNativeHook(
5554
+ {
5555
+ hook_event_name: "Stop",
5556
+ cwd,
5557
+ session_id: sessionId,
5558
+ },
5559
+ { cwd },
5560
+ );
5561
+
5562
+ assert.equal(compactBoundary.omxEventName, "stop");
5563
+ assert.equal(compactBoundary.outputJson, null);
5564
+ assert.equal(resumedStop.omxEventName, "stop");
5565
+ assert.deepEqual(resumedStop.outputJson, {
5566
+ decision: "block",
5567
+ reason:
5568
+ "OMX autopilot is still active (phase: code-review); continue the task and gather fresh verification evidence before stopping.",
5569
+ stopReason: "autopilot_code-review",
5570
+ systemMessage:
5571
+ "OMX autopilot is still active (phase: code-review). Run the required $code-review step before completing or clearing Autopilot state.",
5572
+ });
5573
+ } finally {
5574
+ await rm(cwd, { recursive: true, force: true });
5575
+ }
5576
+ });
5577
+
5316
5578
  it("suppresses duplicate Autopilot planning Stop replays so stale planning state cannot loop indefinitely", async () => {
5317
5579
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-planning-replay-"));
5318
5580
  try {
@@ -5913,6 +6175,98 @@ exit 0
5913
6175
  }
5914
6176
  });
5915
6177
 
6178
+ it("queues worker Stop leader nudge with Tab and submit when leader pane is busy", async () => {
6179
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-busy-leader-"));
6180
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
6181
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
6182
+ const prevPath = process.env.PATH;
6183
+ try {
6184
+ await initTeamState(
6185
+ "worker-stop-team-busy-leader",
6186
+ "worker stop busy leader",
6187
+ "executor",
6188
+ 1,
6189
+ cwd,
6190
+ undefined,
6191
+ { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-busy-leader" },
6192
+ );
6193
+ const fakeBinDir = join(cwd, "fake-bin");
6194
+ const tmuxLogPath = join(cwd, "tmux.log");
6195
+ await mkdir(fakeBinDir, { recursive: true });
6196
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, { busyLeader: true }));
6197
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
6198
+ const stateDir = join(cwd, ".omx", "state");
6199
+ const teamDir = join(stateDir, "team", "worker-stop-team-busy-leader");
6200
+ const workerDir = join(teamDir, "workers", "worker-1");
6201
+ await writeJson(join(teamDir, "config.json"), {
6202
+ name: "worker-stop-team-busy-leader",
6203
+ tmux_session: "omx-team-worker-stop",
6204
+ leader_pane_id: "%42",
6205
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
6206
+ });
6207
+ await writeJson(join(teamDir, "manifest.v2.json"), {
6208
+ name: "worker-stop-team-busy-leader",
6209
+ tmux_session: "omx-team-worker-stop",
6210
+ leader_pane_id: "%42",
6211
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
6212
+ });
6213
+ await writeJson(join(workerDir, "identity.json"), {
6214
+ name: "worker-1",
6215
+ index: 1,
6216
+ role: "executor",
6217
+ assigned_tasks: ["1"],
6218
+ worktree_path: cwd,
6219
+ team_state_root: stateDir,
6220
+ });
6221
+ await writeJson(join(workerDir, "status.json"), {
6222
+ state: "done",
6223
+ current_task_id: "1",
6224
+ updated_at: new Date().toISOString(),
6225
+ });
6226
+ await writeJson(join(teamDir, "tasks", "task-1.json"), {
6227
+ id: "1",
6228
+ subject: "hook task",
6229
+ description: "finish hook task",
6230
+ status: "completed",
6231
+ owner: "worker-1",
6232
+ created_at: new Date().toISOString(),
6233
+ });
6234
+
6235
+ process.env.OMX_TEAM_WORKER = "worker-stop-team-busy-leader/worker-1";
6236
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
6237
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
6238
+
6239
+ const result = await dispatchCodexNativeHook(
6240
+ {
6241
+ hook_event_name: "Stop",
6242
+ cwd,
6243
+ session_id: "sess-stop-team-worker-busy-leader",
6244
+ },
6245
+ { cwd },
6246
+ );
6247
+
6248
+ assert.equal(result.outputJson, null);
6249
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
6250
+ assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
6251
+ assert.match(tmuxLog, /send-keys -t %42 Tab/);
6252
+ assert.match(tmuxLog, /send-keys -t %42 C-m/);
6253
+ assert.ok(
6254
+ tmuxLog.indexOf("send-keys -t %42 Tab") < tmuxLog.indexOf("send-keys -t %42 C-m"),
6255
+ "busy worker-stop nudge should press Tab before C-m",
6256
+ );
6257
+ const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
6258
+ assert.equal(nudgeState.delivery, "queued");
6259
+ } finally {
6260
+ if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
6261
+ else delete process.env.OMX_TEAM_WORKER;
6262
+ if (typeof prevTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
6263
+ else delete process.env.OMX_TEAM_STATE_ROOT;
6264
+ if (typeof prevPath === "string") process.env.PATH = prevPath;
6265
+ else delete process.env.PATH;
6266
+ await rm(cwd, { recursive: true, force: true });
6267
+ }
6268
+ });
6269
+
5916
6270
  it("allows worker Stop when the Stop nudge helper cannot deliver", async () => {
5917
6271
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-helper-fail-"));
5918
6272
  const prevTeamWorker = process.env.OMX_TEAM_WORKER;
@@ -6843,8 +7197,287 @@ exit 0
6843
7197
  }
6844
7198
  });
6845
7199
 
6846
- it("does not block on stale ralplan skill-active when canonical run-state is terminal", async () => {
6847
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-terminal-ralplan-run-"));
7200
+ it("does not block when canonical root ralplan state is inactive but session ralplan state is stale active", async () => {
7201
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-session-ralplan-root-inactive-"));
7202
+ try {
7203
+ const stateDir = join(cwd, ".omx", "state");
7204
+ const sessionId = "sess-stop-stale-session-ralplan";
7205
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
7206
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
7207
+ await writeJson(join(stateDir, "skill-active-state.json"), {
7208
+ active: false,
7209
+ skill: "ralplan",
7210
+ phase: "reviewing",
7211
+ active_skills: [],
7212
+ });
7213
+ await writeJson(join(stateDir, "ralplan-state.json"), {
7214
+ active: false,
7215
+ mode: "ralplan",
7216
+ current_phase: "complete",
7217
+ session_id: sessionId,
7218
+ });
7219
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
7220
+ active: true,
7221
+ skill: "ralplan",
7222
+ phase: "planning",
7223
+ session_id: sessionId,
7224
+ active_skills: [{
7225
+ skill: "ralplan",
7226
+ phase: "planning",
7227
+ active: true,
7228
+ session_id: sessionId,
7229
+ }],
7230
+ });
7231
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
7232
+ active: true,
7233
+ mode: "ralplan",
7234
+ current_phase: "planning",
7235
+ session_id: sessionId,
7236
+ });
7237
+
7238
+ const result = await dispatchCodexNativeHook(
7239
+ {
7240
+ hook_event_name: "Stop",
7241
+ cwd,
7242
+ session_id: sessionId,
7243
+ },
7244
+ { cwd },
7245
+ );
7246
+
7247
+ assert.equal(result.omxEventName, "stop");
7248
+ assert.equal(result.outputJson, null);
7249
+ } finally {
7250
+ await rm(cwd, { recursive: true, force: true });
7251
+ }
7252
+ });
7253
+
7254
+ it("keeps blocking current session ralplan when root inactive ralplan state belongs to another session", async () => {
7255
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-other-session-"));
7256
+ try {
7257
+ const stateDir = join(cwd, ".omx", "state");
7258
+ const sessionId = "sess-stop-current-active-ralplan";
7259
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
7260
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
7261
+ await writeJson(join(stateDir, "skill-active-state.json"), {
7262
+ active: false,
7263
+ skill: "ralplan",
7264
+ phase: "complete",
7265
+ session_id: "sess-stop-old-ralplan",
7266
+ active_skills: [],
7267
+ });
7268
+ await writeJson(join(stateDir, "ralplan-state.json"), {
7269
+ active: false,
7270
+ mode: "ralplan",
7271
+ current_phase: "complete",
7272
+ session_id: "sess-stop-old-ralplan",
7273
+ });
7274
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
7275
+ active: true,
7276
+ skill: "ralplan",
7277
+ phase: "planning",
7278
+ session_id: sessionId,
7279
+ active_skills: [{
7280
+ skill: "ralplan",
7281
+ phase: "planning",
7282
+ active: true,
7283
+ session_id: sessionId,
7284
+ }],
7285
+ });
7286
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
7287
+ active: true,
7288
+ mode: "ralplan",
7289
+ current_phase: "planning",
7290
+ session_id: sessionId,
7291
+ });
7292
+
7293
+ const result = await dispatchCodexNativeHook(
7294
+ {
7295
+ hook_event_name: "Stop",
7296
+ cwd,
7297
+ session_id: sessionId,
7298
+ },
7299
+ { cwd },
7300
+ );
7301
+
7302
+ assert.equal(result.omxEventName, "stop");
7303
+ assert.equal(result.outputJson?.decision, "block");
7304
+ assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
7305
+ } finally {
7306
+ await rm(cwd, { recursive: true, force: true });
7307
+ }
7308
+ });
7309
+
7310
+ it("keeps blocking current session ralplan when root inactive ralplan state is unscoped", async () => {
7311
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-unscoped-"));
7312
+ try {
7313
+ const stateDir = join(cwd, ".omx", "state");
7314
+ const sessionId = "sess-stop-unscoped-root-current-active";
7315
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
7316
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
7317
+ await writeJson(join(stateDir, "skill-active-state.json"), {
7318
+ active: false,
7319
+ skill: "ralplan",
7320
+ phase: "complete",
7321
+ active_skills: [],
7322
+ });
7323
+ await writeJson(join(stateDir, "ralplan-state.json"), {
7324
+ active: false,
7325
+ mode: "ralplan",
7326
+ current_phase: "complete",
7327
+ });
7328
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
7329
+ active: true,
7330
+ skill: "ralplan",
7331
+ phase: "planning",
7332
+ session_id: sessionId,
7333
+ active_skills: [{
7334
+ skill: "ralplan",
7335
+ phase: "planning",
7336
+ active: true,
7337
+ session_id: sessionId,
7338
+ }],
7339
+ });
7340
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
7341
+ active: true,
7342
+ mode: "ralplan",
7343
+ current_phase: "planning",
7344
+ session_id: sessionId,
7345
+ });
7346
+
7347
+ const result = await dispatchCodexNativeHook(
7348
+ {
7349
+ hook_event_name: "Stop",
7350
+ cwd,
7351
+ session_id: sessionId,
7352
+ },
7353
+ { cwd },
7354
+ );
7355
+
7356
+ assert.equal(result.omxEventName, "stop");
7357
+ assert.equal(result.outputJson?.decision, "block");
7358
+ assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
7359
+ } finally {
7360
+ await rm(cwd, { recursive: true, force: true });
7361
+ }
7362
+ });
7363
+
7364
+ it("does not block stale session ralplan when root ralplan is terminal and another root skill is active", async () => {
7365
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-ralplan-other-root-skill-"));
7366
+ try {
7367
+ const stateDir = join(cwd, ".omx", "state");
7368
+ const sessionId = "sess-stop-stale-ralplan-other-root-skill";
7369
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
7370
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
7371
+ await writeJson(join(stateDir, "skill-active-state.json"), {
7372
+ active: true,
7373
+ skill: "deep-interview",
7374
+ phase: "intent-first",
7375
+ session_id: sessionId,
7376
+ active_skills: [{
7377
+ skill: "deep-interview",
7378
+ phase: "intent-first",
7379
+ active: true,
7380
+ session_id: sessionId,
7381
+ }],
7382
+ });
7383
+ await writeJson(join(stateDir, "ralplan-state.json"), {
7384
+ active: false,
7385
+ mode: "ralplan",
7386
+ current_phase: "complete",
7387
+ session_id: sessionId,
7388
+ });
7389
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
7390
+ active: true,
7391
+ skill: "ralplan",
7392
+ phase: "planning",
7393
+ session_id: sessionId,
7394
+ active_skills: [{
7395
+ skill: "ralplan",
7396
+ phase: "planning",
7397
+ active: true,
7398
+ session_id: sessionId,
7399
+ }],
7400
+ });
7401
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
7402
+ active: true,
7403
+ mode: "ralplan",
7404
+ current_phase: "planning",
7405
+ session_id: sessionId,
7406
+ });
7407
+
7408
+ const result = await dispatchCodexNativeHook(
7409
+ {
7410
+ hook_event_name: "Stop",
7411
+ cwd,
7412
+ session_id: sessionId,
7413
+ },
7414
+ { cwd },
7415
+ );
7416
+
7417
+ assert.equal(result.omxEventName, "stop");
7418
+ assert.equal(result.outputJson, null);
7419
+ } finally {
7420
+ await rm(cwd, { recursive: true, force: true });
7421
+ }
7422
+ });
7423
+
7424
+ it("keeps blocking session ralplan when canonical root state is not inactive", async () => {
7425
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-active-"));
7426
+ try {
7427
+ const stateDir = join(cwd, ".omx", "state");
7428
+ const sessionId = "sess-stop-session-ralplan-root-active";
7429
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
7430
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
7431
+ await writeJson(join(stateDir, "skill-active-state.json"), {
7432
+ active: true,
7433
+ skill: "ralplan",
7434
+ phase: "planning",
7435
+ session_id: sessionId,
7436
+ active_skills: [{
7437
+ skill: "ralplan",
7438
+ phase: "planning",
7439
+ active: true,
7440
+ session_id: sessionId,
7441
+ }],
7442
+ });
7443
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
7444
+ active: true,
7445
+ skill: "ralplan",
7446
+ phase: "planning",
7447
+ session_id: sessionId,
7448
+ active_skills: [{
7449
+ skill: "ralplan",
7450
+ phase: "planning",
7451
+ active: true,
7452
+ session_id: sessionId,
7453
+ }],
7454
+ });
7455
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
7456
+ active: true,
7457
+ mode: "ralplan",
7458
+ current_phase: "planning",
7459
+ session_id: sessionId,
7460
+ });
7461
+
7462
+ const result = await dispatchCodexNativeHook(
7463
+ {
7464
+ hook_event_name: "Stop",
7465
+ cwd,
7466
+ session_id: sessionId,
7467
+ },
7468
+ { cwd },
7469
+ );
7470
+
7471
+ assert.equal(result.omxEventName, "stop");
7472
+ assert.equal(result.outputJson?.decision, "block");
7473
+ assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
7474
+ } finally {
7475
+ await rm(cwd, { recursive: true, force: true });
7476
+ }
7477
+ });
7478
+
7479
+ it("does not block on stale ralplan skill-active when canonical run-state is terminal", async () => {
7480
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-terminal-ralplan-run-"));
6848
7481
  try {
6849
7482
  const stateDir = join(cwd, ".omx", "state");
6850
7483
  const sessionId = "sess-stop-terminal-ralplan";
@@ -7985,6 +8618,47 @@ exit 0
7985
8618
  }
7986
8619
  });
7987
8620
 
8621
+ it("allows Stop from stale orphaned session-scoped Ralph starting iteration zero state", async () => {
8622
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-orphan-starting-ralph-"));
8623
+ try {
8624
+ const stateDir = join(cwd, ".omx", "state");
8625
+ const sessionId = "sess-stale-orphan-ralph";
8626
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
8627
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: sessionId, cwd });
8628
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
8629
+ active: true,
8630
+ mode: "ralph",
8631
+ current_phase: "starting",
8632
+ iteration: 0,
8633
+ session_id: sessionId,
8634
+ updated_at: "2000-01-01T00:00:00.000Z",
8635
+ });
8636
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
8637
+ active: true,
8638
+ skill: "ralph",
8639
+ phase: "starting",
8640
+ session_id: sessionId,
8641
+ active_skills: [{ skill: "ralph", phase: "starting", active: true, session_id: sessionId }],
8642
+ });
8643
+
8644
+ const result = await dispatchCodexNativeHook(
8645
+ {
8646
+ hook_event_name: "Stop",
8647
+ cwd,
8648
+ session_id: sessionId,
8649
+ thread_id: "thread-verifier-terminal",
8650
+ last_assistant_message: "APPROVE: read-only verifier evidence is fresh and sufficient.",
8651
+ },
8652
+ { cwd },
8653
+ );
8654
+
8655
+ assert.equal(result.omxEventName, "stop");
8656
+ assert.equal(result.outputJson, null);
8657
+ } finally {
8658
+ await rm(cwd, { recursive: true, force: true });
8659
+ }
8660
+ });
8661
+
7988
8662
  it("blocks Stop on visible active session-scoped Ralph starting state and reports its path", async () => {
7989
8663
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-visible-starting-ralph-"));
7990
8664
  try {
@@ -8027,6 +8701,138 @@ exit 0
8027
8701
  }
8028
8702
  });
8029
8703
 
8704
+ it("retires prompt-seeded Ralph starting state when canonical Ralph already completed with audit", async () => {
8705
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-shadowed-starting-"));
8706
+ try {
8707
+ const stateDir = join(cwd, ".omx", "state");
8708
+ const nativeSessionId = "native-hook-seed";
8709
+ const canonicalSessionId = "omx-runtime-session";
8710
+ await mkdir(join(stateDir, "sessions", nativeSessionId), { recursive: true });
8711
+ await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
8712
+ await writeJson(join(stateDir, "session.json"), {
8713
+ session_id: canonicalSessionId,
8714
+ cwd,
8715
+ });
8716
+ await writeJson(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), {
8717
+ active: true,
8718
+ mode: "ralph",
8719
+ current_phase: "starting",
8720
+ session_id: nativeSessionId,
8721
+ iteration: 0,
8722
+ task_slug: "mvp-h-local-method-preflight-execution",
8723
+ started_at: "2026-05-14T07:00:00.000Z",
8724
+ });
8725
+ await writeJson(join(stateDir, "sessions", nativeSessionId, "skill-active-state.json"), {
8726
+ active: true,
8727
+ skill: "ralph",
8728
+ phase: "starting",
8729
+ session_id: nativeSessionId,
8730
+ active_skills: [{ skill: "ralph", phase: "starting", active: true, session_id: nativeSessionId }],
8731
+ });
8732
+ await writeJson(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), {
8733
+ active: false,
8734
+ mode: "ralph",
8735
+ current_phase: "complete",
8736
+ session_id: canonicalSessionId,
8737
+ completed_at: "2026-05-14T07:30:00.000Z",
8738
+ completion_audit: {
8739
+ passed: true,
8740
+ prompt_to_artifact_checklist: ["task evidence mapped"],
8741
+ verification_evidence: ["fresh verification evidence recorded"],
8742
+ },
8743
+ });
8744
+
8745
+ const result = await dispatchCodexNativeHook(
8746
+ {
8747
+ hook_event_name: "Stop",
8748
+ cwd,
8749
+ session_id: nativeSessionId,
8750
+ },
8751
+ { cwd },
8752
+ );
8753
+
8754
+ assert.equal(result.omxEventName, "stop");
8755
+ assert.equal(result.outputJson, null);
8756
+ const retiredState = JSON.parse(await readFile(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), "utf-8"));
8757
+ assert.equal(retiredState.active, false);
8758
+ assert.equal(retiredState.current_phase, "complete");
8759
+ assert.equal(retiredState.stop_reason, "shadowed_by_completed_canonical_ralph");
8760
+ assert.equal(retiredState.shadowed_by_completed_canonical_ralph.session_id, canonicalSessionId);
8761
+ } finally {
8762
+ await rm(cwd, { recursive: true, force: true });
8763
+ }
8764
+ });
8765
+
8766
+ it("does not retire prompt-seeded Ralph starting state from a completed canonical Ralph owned by another thread", async () => {
8767
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-shadowed-thread-mismatch-"));
8768
+ try {
8769
+ const stateDir = join(cwd, ".omx", "state");
8770
+ const nativeSessionId = "native-hook-seed";
8771
+ const canonicalSessionId = "omx-runtime-session";
8772
+ await mkdir(join(stateDir, "sessions", nativeSessionId), { recursive: true });
8773
+ await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
8774
+ await writeJson(join(stateDir, "session.json"), {
8775
+ session_id: canonicalSessionId,
8776
+ cwd,
8777
+ });
8778
+ await writeJson(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), {
8779
+ active: true,
8780
+ mode: "ralph",
8781
+ current_phase: "starting",
8782
+ session_id: nativeSessionId,
8783
+ iteration: 0,
8784
+ task_slug: "mvp-h-local-method-preflight-execution",
8785
+ started_at: "2026-05-14T07:00:00.000Z",
8786
+ });
8787
+ await writeJson(join(stateDir, "sessions", nativeSessionId, "skill-active-state.json"), {
8788
+ active: true,
8789
+ skill: "ralph",
8790
+ phase: "starting",
8791
+ session_id: nativeSessionId,
8792
+ active_skills: [{ skill: "ralph", phase: "starting", active: true, session_id: nativeSessionId }],
8793
+ });
8794
+ await writeJson(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), {
8795
+ active: false,
8796
+ mode: "ralph",
8797
+ current_phase: "complete",
8798
+ session_id: canonicalSessionId,
8799
+ owner_codex_thread_id: "thread-A",
8800
+ completed_at: "2026-05-14T07:30:00.000Z",
8801
+ completion_audit: {
8802
+ passed: true,
8803
+ prompt_to_artifact_checklist: ["task evidence mapped"],
8804
+ verification_evidence: ["fresh verification evidence recorded"],
8805
+ },
8806
+ });
8807
+
8808
+ const result = await dispatchCodexNativeHook(
8809
+ {
8810
+ hook_event_name: "Stop",
8811
+ cwd,
8812
+ session_id: nativeSessionId,
8813
+ thread_id: "thread-B",
8814
+ },
8815
+ { cwd },
8816
+ );
8817
+
8818
+ assert.equal(result.omxEventName, "stop");
8819
+ assert.deepEqual(result.outputJson, {
8820
+ decision: "block",
8821
+ reason:
8822
+ "OMX Ralph is still active (phase: starting; state: .omx/state/sessions/native-hook-seed/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
8823
+ stopReason: "ralph_starting",
8824
+ systemMessage:
8825
+ "OMX Ralph is still active (phase: starting; state: .omx/state/sessions/native-hook-seed/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
8826
+ });
8827
+ const preservedState = JSON.parse(await readFile(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), "utf-8"));
8828
+ assert.equal(preservedState.active, true);
8829
+ assert.equal(preservedState.current_phase, "starting");
8830
+ assert.equal(preservedState.stop_reason, undefined);
8831
+ } finally {
8832
+ await rm(cwd, { recursive: true, force: true });
8833
+ }
8834
+ });
8835
+
8030
8836
  it("does not block Stop from another session-scoped Ralph state when an explicit session_id has no active Ralph state", async () => {
8031
8837
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-explicit-session-ralph-"));
8032
8838
  try {