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
@@ -67,7 +67,7 @@ if [[ "$cmd" == "display-message" ]]; then
67
67
  exit 0
68
68
  fi
69
69
  if [[ "$cmd" == "capture-pane" ]]; then
70
- echo "› ready"
70
+ ${options.busyLeader ? 'echo "• Working… (esc to interrupt)"' : 'echo "› ready"'}
71
71
  exit 0
72
72
  fi
73
73
  if [[ "$cmd" == "send-keys" ]]; then
@@ -1409,6 +1409,67 @@ describe("codex native hook dispatch", () => {
1409
1409
  await rm(cwd, { recursive: true, force: true });
1410
1410
  }
1411
1411
  });
1412
+ it("does not repeat Stop block when the last autoresearch-goal completion attempt reported objective mismatch", async () => {
1413
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autoresearch-mismatch-reported-stop-"));
1414
+ try {
1415
+ await writeJson(join(cwd, ".omx", "goals", "autoresearch", "mismatched-mission", "mission.json"), {
1416
+ version: 1,
1417
+ workflow: "autoresearch-goal",
1418
+ slug: "mismatched-mission",
1419
+ topic: "Passing research bound to another Codex goal",
1420
+ status: "passed",
1421
+ });
1422
+ await writeJson(join(cwd, ".omx", "goals", "autoresearch", "mismatched-mission", "completion.json"), {
1423
+ verdict: "pass",
1424
+ passed: true,
1425
+ });
1426
+ const result = await dispatchCodexNativeHook({
1427
+ hook_event_name: "Stop",
1428
+ cwd,
1429
+ session_id: "sess-autoresearch-mismatch-reported-stop",
1430
+ thread_id: "thread-autoresearch-mismatch-reported-stop",
1431
+ last_assistant_message: [
1432
+ "I called get_goal and ran omx autoresearch-goal complete --slug mismatched-mission --codex-goal-json /tmp/snapshot.json.",
1433
+ "The autoresearch-goal completion failed with Codex goal objective mismatch, so I will not repeat the same complete command blindly in this thread.",
1434
+ ].join("\n"),
1435
+ }, { cwd });
1436
+ assert.notEqual(result.outputJson?.decision, "block");
1437
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /autoresearch-goal complete --slug mismatched-mission/);
1438
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
1439
+ }
1440
+ finally {
1441
+ await rm(cwd, { recursive: true, force: true });
1442
+ }
1443
+ });
1444
+ it("still blocks later autoresearch-goal completion claims after an objective mismatch if no mismatch is reported in the final answer", async () => {
1445
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autoresearch-mismatch-later-retry-stop-"));
1446
+ try {
1447
+ await writeJson(join(cwd, ".omx", "goals", "autoresearch", "retryable-mission", "mission.json"), {
1448
+ version: 1,
1449
+ workflow: "autoresearch-goal",
1450
+ slug: "retryable-mission",
1451
+ topic: "Passing research that can still retry with the correct snapshot",
1452
+ status: "passed",
1453
+ });
1454
+ await writeJson(join(cwd, ".omx", "goals", "autoresearch", "retryable-mission", "completion.json"), {
1455
+ verdict: "pass",
1456
+ passed: true,
1457
+ });
1458
+ const result = await dispatchCodexNativeHook({
1459
+ hook_event_name: "Stop",
1460
+ cwd,
1461
+ session_id: "sess-autoresearch-mismatch-later-retry-stop",
1462
+ thread_id: "thread-autoresearch-mismatch-later-retry-stop",
1463
+ last_assistant_message: "Autoresearch goal complete; next call update_goal({status: \"complete\"}).",
1464
+ }, { cwd });
1465
+ assert.equal(result.outputJson?.decision, "block");
1466
+ assert.match(JSON.stringify(result.outputJson), /get_goal snapshot reconciliation/);
1467
+ assert.match(JSON.stringify(result.outputJson), /omx autoresearch-goal complete --slug retryable-mission/);
1468
+ }
1469
+ finally {
1470
+ await rm(cwd, { recursive: true, force: true });
1471
+ }
1472
+ });
1412
1473
  it("treats workflow keywords in native subagent prompt text as literal delegation text", async () => {
1413
1474
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-subagent-keyword-literal-"));
1414
1475
  try {
@@ -2370,6 +2431,44 @@ exit 0
2370
2431
  await rm(cwd, { recursive: true, force: true });
2371
2432
  }
2372
2433
  });
2434
+ it("does not block Bash commands that only mention omx question in quoted arguments", async () => {
2435
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-quoted-mention-"));
2436
+ try {
2437
+ const result = await dispatchCodexNativeHook({
2438
+ hook_event_name: "PreToolUse",
2439
+ cwd,
2440
+ tool_name: "Bash",
2441
+ tool_use_id: "tool-question-quoted-mention",
2442
+ tool_input: {
2443
+ command: `omx ultragoal create-goals --brief "Deep interview says omx question failed in tmux"`,
2444
+ },
2445
+ }, { cwd });
2446
+ assert.equal(result.omxEventName, "pre-tool-use");
2447
+ assert.equal(result.outputJson, null);
2448
+ }
2449
+ finally {
2450
+ await rm(cwd, { recursive: true, force: true });
2451
+ }
2452
+ });
2453
+ it("does not block Bash heredocs that only document omx question text", async () => {
2454
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-heredoc-mention-"));
2455
+ try {
2456
+ const result = await dispatchCodexNativeHook({
2457
+ hook_event_name: "PreToolUse",
2458
+ cwd,
2459
+ tool_name: "Bash",
2460
+ tool_use_id: "tool-question-heredoc-mention",
2461
+ tool_input: {
2462
+ command: `cat > issue-notes.md <<'EOF'\nomx question failed in the attached tmux pane\nEOF`,
2463
+ },
2464
+ }, { cwd });
2465
+ assert.equal(result.omxEventName, "pre-tool-use");
2466
+ assert.equal(result.outputJson, null);
2467
+ }
2468
+ finally {
2469
+ await rm(cwd, { recursive: true, force: true });
2470
+ }
2471
+ });
2373
2472
  it("allows Bash omx question when the command preserves the leader-pane return hint", async () => {
2374
2473
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-question-allow-"));
2375
2474
  try {
@@ -4052,6 +4151,82 @@ exit 0
4052
4151
  await rm(cwd, { recursive: true, force: true });
4053
4152
  }
4054
4153
  });
4154
+ it("does not treat non-MCP source output containing detector constants as MCP transport death", async () => {
4155
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-read-mcp-source-"));
4156
+ try {
4157
+ const result = await dispatchCodexNativeHook({
4158
+ hook_event_name: "PostToolUse",
4159
+ cwd,
4160
+ tool_name: "Read",
4161
+ tool_use_id: "tool-read-mcp-source",
4162
+ tool_input: { file_path: "src/scripts/codex-native-pre-post.ts" },
4163
+ tool_response: "const MCP_TRANSPORT_FAILURE_PATTERNS = [/transport closed/i, /server disconnected/i];\nconst context = /\\bmcp\\b/i;",
4164
+ }, { cwd });
4165
+ assert.equal(result.omxEventName, "post-tool-use");
4166
+ assert.equal(result.outputJson, null);
4167
+ }
4168
+ finally {
4169
+ await rm(cwd, { recursive: true, force: true });
4170
+ }
4171
+ });
4172
+ it("does not treat non-MCP docs stdout mentioning closed MCP transport as transport death", async () => {
4173
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-docs-mcp-log-"));
4174
+ try {
4175
+ const result = await dispatchCodexNativeHook({
4176
+ hook_event_name: "PostToolUse",
4177
+ cwd,
4178
+ tool_name: "ShellOutput",
4179
+ tool_use_id: "tool-docs-mcp-log",
4180
+ tool_response: JSON.stringify({
4181
+ stdout: "Troubleshooting note: MCP transport closed after the server disconnected in an old log.",
4182
+ stderr: "",
4183
+ }),
4184
+ }, { cwd });
4185
+ assert.equal(result.omxEventName, "post-tool-use");
4186
+ assert.equal(result.outputJson, null);
4187
+ }
4188
+ finally {
4189
+ await rm(cwd, { recursive: true, force: true });
4190
+ }
4191
+ });
4192
+ it("does not MCP-block non-MCP command output with unrelated stderr and MCP transport stdout", async () => {
4193
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-nonmcp-mixed-output-"));
4194
+ try {
4195
+ const result = await dispatchCodexNativeHook({
4196
+ hook_event_name: "PostToolUse",
4197
+ cwd,
4198
+ tool_name: "ShellOutput",
4199
+ tool_use_id: "tool-nonmcp-mixed-output",
4200
+ tool_response: JSON.stringify({
4201
+ stdout: "captured log line: MCP transport closed before response",
4202
+ stderr: "grep: fixture.txt: No such file or directory",
4203
+ }),
4204
+ }, { cwd });
4205
+ assert.equal(result.omxEventName, "post-tool-use");
4206
+ assert.equal(result.outputJson, null);
4207
+ }
4208
+ finally {
4209
+ await rm(cwd, { recursive: true, force: true });
4210
+ }
4211
+ });
4212
+ it("still blocks MCP-like raw transport failures", async () => {
4213
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-raw-transport-"));
4214
+ try {
4215
+ const result = await dispatchCodexNativeHook({
4216
+ hook_event_name: "PostToolUse",
4217
+ cwd,
4218
+ tool_name: "mcp__omx_state__state_write",
4219
+ tool_use_id: "tool-mcp-raw-transport",
4220
+ tool_response: "transport closed after server disconnected",
4221
+ }, { cwd });
4222
+ assert.equal(result.omxEventName, "post-tool-use");
4223
+ assert.equal(result.outputJson?.decision, "block");
4224
+ assert.match(String(result.outputJson?.reason || ""), /lost its transport\/server connection/);
4225
+ }
4226
+ finally {
4227
+ await rm(cwd, { recursive: true, force: true });
4228
+ }
4229
+ });
4055
4230
  it("returns PostToolUse MCP transport fallback guidance for clear MCP transport death", async () => {
4056
4231
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-transport-"));
4057
4232
  try {
@@ -4366,6 +4541,51 @@ exit 0
4366
4541
  await rm(cwd, { recursive: true, force: true });
4367
4542
  }
4368
4543
  });
4544
+ it("requires Autopilot code review after a compact-boundary Stop exemption", async () => {
4545
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-review-compact-"));
4546
+ try {
4547
+ const stateDir = join(cwd, ".omx", "state");
4548
+ const sessionId = "sess-stop-autopilot-review-compact";
4549
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
4550
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
4551
+ active: true,
4552
+ mode: "autopilot",
4553
+ current_phase: "code-review",
4554
+ state: {
4555
+ phase_cycle: ["ralplan", "ralph", "code-review"],
4556
+ handoff_artifacts: {
4557
+ ralplan: ".omx/plans/prd-issue-2366.md",
4558
+ ralph: { verification: ["npm test"] },
4559
+ code_review: null,
4560
+ },
4561
+ review_verdict: null,
4562
+ },
4563
+ });
4564
+ const compactBoundary = await dispatchCodexNativeHook({
4565
+ hook_event_name: "Stop",
4566
+ cwd,
4567
+ session_id: sessionId,
4568
+ stop_reason: "context compact",
4569
+ }, { cwd });
4570
+ const resumedStop = await dispatchCodexNativeHook({
4571
+ hook_event_name: "Stop",
4572
+ cwd,
4573
+ session_id: sessionId,
4574
+ }, { cwd });
4575
+ assert.equal(compactBoundary.omxEventName, "stop");
4576
+ assert.equal(compactBoundary.outputJson, null);
4577
+ assert.equal(resumedStop.omxEventName, "stop");
4578
+ assert.deepEqual(resumedStop.outputJson, {
4579
+ decision: "block",
4580
+ reason: "OMX autopilot is still active (phase: code-review); continue the task and gather fresh verification evidence before stopping.",
4581
+ stopReason: "autopilot_code-review",
4582
+ systemMessage: "OMX autopilot is still active (phase: code-review). Run the required $code-review step before completing or clearing Autopilot state.",
4583
+ });
4584
+ }
4585
+ finally {
4586
+ await rm(cwd, { recursive: true, force: true });
4587
+ }
4588
+ });
4369
4589
  it("suppresses duplicate Autopilot planning Stop replays so stale planning state cannot loop indefinitely", async () => {
4370
4590
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-planning-replay-"));
4371
4591
  try {
@@ -4858,6 +5078,87 @@ exit 0
4858
5078
  await rm(cwd, { recursive: true, force: true });
4859
5079
  }
4860
5080
  });
5081
+ it("queues worker Stop leader nudge with Tab and submit when leader pane is busy", async () => {
5082
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-busy-leader-"));
5083
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
5084
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
5085
+ const prevPath = process.env.PATH;
5086
+ try {
5087
+ await initTeamState("worker-stop-team-busy-leader", "worker stop busy leader", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-busy-leader" });
5088
+ const fakeBinDir = join(cwd, "fake-bin");
5089
+ const tmuxLogPath = join(cwd, "tmux.log");
5090
+ await mkdir(fakeBinDir, { recursive: true });
5091
+ await writeFile(join(fakeBinDir, "tmux"), buildWorkerStopFakeTmux(tmuxLogPath, { busyLeader: true }));
5092
+ await chmod(join(fakeBinDir, "tmux"), 0o755);
5093
+ const stateDir = join(cwd, ".omx", "state");
5094
+ const teamDir = join(stateDir, "team", "worker-stop-team-busy-leader");
5095
+ const workerDir = join(teamDir, "workers", "worker-1");
5096
+ await writeJson(join(teamDir, "config.json"), {
5097
+ name: "worker-stop-team-busy-leader",
5098
+ tmux_session: "omx-team-worker-stop",
5099
+ leader_pane_id: "%42",
5100
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
5101
+ });
5102
+ await writeJson(join(teamDir, "manifest.v2.json"), {
5103
+ name: "worker-stop-team-busy-leader",
5104
+ tmux_session: "omx-team-worker-stop",
5105
+ leader_pane_id: "%42",
5106
+ workers: [{ name: "worker-1", index: 1, pane_id: "%10" }],
5107
+ });
5108
+ await writeJson(join(workerDir, "identity.json"), {
5109
+ name: "worker-1",
5110
+ index: 1,
5111
+ role: "executor",
5112
+ assigned_tasks: ["1"],
5113
+ worktree_path: cwd,
5114
+ team_state_root: stateDir,
5115
+ });
5116
+ await writeJson(join(workerDir, "status.json"), {
5117
+ state: "done",
5118
+ current_task_id: "1",
5119
+ updated_at: new Date().toISOString(),
5120
+ });
5121
+ await writeJson(join(teamDir, "tasks", "task-1.json"), {
5122
+ id: "1",
5123
+ subject: "hook task",
5124
+ description: "finish hook task",
5125
+ status: "completed",
5126
+ owner: "worker-1",
5127
+ created_at: new Date().toISOString(),
5128
+ });
5129
+ process.env.OMX_TEAM_WORKER = "worker-stop-team-busy-leader/worker-1";
5130
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
5131
+ process.env.PATH = `${fakeBinDir}:${prevPath || ""}`;
5132
+ const result = await dispatchCodexNativeHook({
5133
+ hook_event_name: "Stop",
5134
+ cwd,
5135
+ session_id: "sess-stop-team-worker-busy-leader",
5136
+ }, { cwd });
5137
+ assert.equal(result.outputJson, null);
5138
+ const tmuxLog = await readFile(tmuxLogPath, "utf-8");
5139
+ assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
5140
+ assert.match(tmuxLog, /send-keys -t %42 Tab/);
5141
+ assert.match(tmuxLog, /send-keys -t %42 C-m/);
5142
+ assert.ok(tmuxLog.indexOf("send-keys -t %42 Tab") < tmuxLog.indexOf("send-keys -t %42 C-m"), "busy worker-stop nudge should press Tab before C-m");
5143
+ const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
5144
+ assert.equal(nudgeState.delivery, "queued");
5145
+ }
5146
+ finally {
5147
+ if (typeof prevTeamWorker === "string")
5148
+ process.env.OMX_TEAM_WORKER = prevTeamWorker;
5149
+ else
5150
+ delete process.env.OMX_TEAM_WORKER;
5151
+ if (typeof prevTeamStateRoot === "string")
5152
+ process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
5153
+ else
5154
+ delete process.env.OMX_TEAM_STATE_ROOT;
5155
+ if (typeof prevPath === "string")
5156
+ process.env.PATH = prevPath;
5157
+ else
5158
+ delete process.env.PATH;
5159
+ await rm(cwd, { recursive: true, force: true });
5160
+ }
5161
+ });
4861
5162
  it("allows worker Stop when the Stop nudge helper cannot deliver", async () => {
4862
5163
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-helper-fail-"));
4863
5164
  const prevTeamWorker = process.env.OMX_TEAM_WORKER;
@@ -5622,13 +5923,25 @@ exit 0
5622
5923
  await rm(cwd, { recursive: true, force: true });
5623
5924
  }
5624
5925
  });
5625
- it("does not block on stale ralplan skill-active when canonical run-state is terminal", async () => {
5626
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-terminal-ralplan-run-"));
5926
+ it("does not block when canonical root ralplan state is inactive but session ralplan state is stale active", async () => {
5927
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-session-ralplan-root-inactive-"));
5627
5928
  try {
5628
5929
  const stateDir = join(cwd, ".omx", "state");
5629
- const sessionId = "sess-stop-terminal-ralplan";
5930
+ const sessionId = "sess-stop-stale-session-ralplan";
5630
5931
  await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
5631
5932
  await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
5933
+ await writeJson(join(stateDir, "skill-active-state.json"), {
5934
+ active: false,
5935
+ skill: "ralplan",
5936
+ phase: "reviewing",
5937
+ active_skills: [],
5938
+ });
5939
+ await writeJson(join(stateDir, "ralplan-state.json"), {
5940
+ active: false,
5941
+ mode: "ralplan",
5942
+ current_phase: "complete",
5943
+ session_id: sessionId,
5944
+ });
5632
5945
  await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
5633
5946
  active: true,
5634
5947
  skill: "ralplan",
@@ -5647,16 +5960,6 @@ exit 0
5647
5960
  current_phase: "planning",
5648
5961
  session_id: sessionId,
5649
5962
  });
5650
- await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
5651
- version: 1,
5652
- mode: "ralplan",
5653
- active: false,
5654
- outcome: "finish",
5655
- lifecycle_outcome: "finished",
5656
- current_phase: "complete",
5657
- completed_at: "2026-05-01T00:00:00.000Z",
5658
- updated_at: "2026-05-01T00:00:00.000Z",
5659
- });
5660
5963
  const result = await dispatchCodexNativeHook({
5661
5964
  hook_event_name: "Stop",
5662
5965
  cwd,
@@ -5669,13 +5972,26 @@ exit 0
5669
5972
  await rm(cwd, { recursive: true, force: true });
5670
5973
  }
5671
5974
  });
5672
- it("does not block on stale ralplan skill-active when pinned mode state belongs to another session", async () => {
5673
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-foreign-ralplan-"));
5975
+ it("keeps blocking current session ralplan when root inactive ralplan state belongs to another session", async () => {
5976
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-other-session-"));
5674
5977
  try {
5675
5978
  const stateDir = join(cwd, ".omx", "state");
5676
- const sessionId = "sess-stop-current-ralplan";
5979
+ const sessionId = "sess-stop-current-active-ralplan";
5677
5980
  await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
5678
5981
  await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
5982
+ await writeJson(join(stateDir, "skill-active-state.json"), {
5983
+ active: false,
5984
+ skill: "ralplan",
5985
+ phase: "complete",
5986
+ session_id: "sess-stop-old-ralplan",
5987
+ active_skills: [],
5988
+ });
5989
+ await writeJson(join(stateDir, "ralplan-state.json"), {
5990
+ active: false,
5991
+ mode: "ralplan",
5992
+ current_phase: "complete",
5993
+ session_id: "sess-stop-old-ralplan",
5994
+ });
5679
5995
  await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
5680
5996
  active: true,
5681
5997
  skill: "ralplan",
@@ -5692,7 +6008,7 @@ exit 0
5692
6008
  active: true,
5693
6009
  mode: "ralplan",
5694
6010
  current_phase: "planning",
5695
- session_id: "sess-other-ralplan",
6011
+ session_id: sessionId,
5696
6012
  });
5697
6013
  const result = await dispatchCodexNativeHook({
5698
6014
  hook_event_name: "Stop",
@@ -5700,21 +6016,260 @@ exit 0
5700
6016
  session_id: sessionId,
5701
6017
  }, { cwd });
5702
6018
  assert.equal(result.omxEventName, "stop");
5703
- assert.equal(result.outputJson, null);
6019
+ assert.equal(result.outputJson?.decision, "block");
6020
+ assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
5704
6021
  }
5705
6022
  finally {
5706
6023
  await rm(cwd, { recursive: true, force: true });
5707
6024
  }
5708
6025
  });
5709
- it("returns an explicit ralplan waiting status while subagents are still active", async () => {
5710
- const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-subagent-"));
6026
+ it("keeps blocking current session ralplan when root inactive ralplan state is unscoped", async () => {
6027
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-unscoped-"));
5711
6028
  try {
5712
6029
  const stateDir = join(cwd, ".omx", "state");
5713
- await mkdir(join(stateDir, "sessions", "sess-stop-skill-subagent"), { recursive: true });
5714
- await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-skill-subagent" });
5715
- await writeJson(join(stateDir, "sessions", "sess-stop-skill-subagent", "skill-active-state.json"), {
5716
- active: true,
5717
- skill: "ralplan",
6030
+ const sessionId = "sess-stop-unscoped-root-current-active";
6031
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
6032
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
6033
+ await writeJson(join(stateDir, "skill-active-state.json"), {
6034
+ active: false,
6035
+ skill: "ralplan",
6036
+ phase: "complete",
6037
+ active_skills: [],
6038
+ });
6039
+ await writeJson(join(stateDir, "ralplan-state.json"), {
6040
+ active: false,
6041
+ mode: "ralplan",
6042
+ current_phase: "complete",
6043
+ });
6044
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
6045
+ active: true,
6046
+ skill: "ralplan",
6047
+ phase: "planning",
6048
+ session_id: sessionId,
6049
+ active_skills: [{
6050
+ skill: "ralplan",
6051
+ phase: "planning",
6052
+ active: true,
6053
+ session_id: sessionId,
6054
+ }],
6055
+ });
6056
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
6057
+ active: true,
6058
+ mode: "ralplan",
6059
+ current_phase: "planning",
6060
+ session_id: sessionId,
6061
+ });
6062
+ const result = await dispatchCodexNativeHook({
6063
+ hook_event_name: "Stop",
6064
+ cwd,
6065
+ session_id: sessionId,
6066
+ }, { cwd });
6067
+ assert.equal(result.omxEventName, "stop");
6068
+ assert.equal(result.outputJson?.decision, "block");
6069
+ assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
6070
+ }
6071
+ finally {
6072
+ await rm(cwd, { recursive: true, force: true });
6073
+ }
6074
+ });
6075
+ it("does not block stale session ralplan when root ralplan is terminal and another root skill is active", async () => {
6076
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-ralplan-other-root-skill-"));
6077
+ try {
6078
+ const stateDir = join(cwd, ".omx", "state");
6079
+ const sessionId = "sess-stop-stale-ralplan-other-root-skill";
6080
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
6081
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
6082
+ await writeJson(join(stateDir, "skill-active-state.json"), {
6083
+ active: true,
6084
+ skill: "deep-interview",
6085
+ phase: "intent-first",
6086
+ session_id: sessionId,
6087
+ active_skills: [{
6088
+ skill: "deep-interview",
6089
+ phase: "intent-first",
6090
+ active: true,
6091
+ session_id: sessionId,
6092
+ }],
6093
+ });
6094
+ await writeJson(join(stateDir, "ralplan-state.json"), {
6095
+ active: false,
6096
+ mode: "ralplan",
6097
+ current_phase: "complete",
6098
+ session_id: sessionId,
6099
+ });
6100
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
6101
+ active: true,
6102
+ skill: "ralplan",
6103
+ phase: "planning",
6104
+ session_id: sessionId,
6105
+ active_skills: [{
6106
+ skill: "ralplan",
6107
+ phase: "planning",
6108
+ active: true,
6109
+ session_id: sessionId,
6110
+ }],
6111
+ });
6112
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
6113
+ active: true,
6114
+ mode: "ralplan",
6115
+ current_phase: "planning",
6116
+ session_id: sessionId,
6117
+ });
6118
+ const result = await dispatchCodexNativeHook({
6119
+ hook_event_name: "Stop",
6120
+ cwd,
6121
+ session_id: sessionId,
6122
+ }, { cwd });
6123
+ assert.equal(result.omxEventName, "stop");
6124
+ assert.equal(result.outputJson, null);
6125
+ }
6126
+ finally {
6127
+ await rm(cwd, { recursive: true, force: true });
6128
+ }
6129
+ });
6130
+ it("keeps blocking session ralplan when canonical root state is not inactive", async () => {
6131
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-session-ralplan-root-active-"));
6132
+ try {
6133
+ const stateDir = join(cwd, ".omx", "state");
6134
+ const sessionId = "sess-stop-session-ralplan-root-active";
6135
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
6136
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
6137
+ await writeJson(join(stateDir, "skill-active-state.json"), {
6138
+ active: true,
6139
+ skill: "ralplan",
6140
+ phase: "planning",
6141
+ session_id: sessionId,
6142
+ active_skills: [{
6143
+ skill: "ralplan",
6144
+ phase: "planning",
6145
+ active: true,
6146
+ session_id: sessionId,
6147
+ }],
6148
+ });
6149
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
6150
+ active: true,
6151
+ skill: "ralplan",
6152
+ phase: "planning",
6153
+ session_id: sessionId,
6154
+ active_skills: [{
6155
+ skill: "ralplan",
6156
+ phase: "planning",
6157
+ active: true,
6158
+ session_id: sessionId,
6159
+ }],
6160
+ });
6161
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
6162
+ active: true,
6163
+ mode: "ralplan",
6164
+ current_phase: "planning",
6165
+ session_id: sessionId,
6166
+ });
6167
+ const result = await dispatchCodexNativeHook({
6168
+ hook_event_name: "Stop",
6169
+ cwd,
6170
+ session_id: sessionId,
6171
+ }, { cwd });
6172
+ assert.equal(result.omxEventName, "stop");
6173
+ assert.equal(result.outputJson?.decision, "block");
6174
+ assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
6175
+ }
6176
+ finally {
6177
+ await rm(cwd, { recursive: true, force: true });
6178
+ }
6179
+ });
6180
+ it("does not block on stale ralplan skill-active when canonical run-state is terminal", async () => {
6181
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-terminal-ralplan-run-"));
6182
+ try {
6183
+ const stateDir = join(cwd, ".omx", "state");
6184
+ const sessionId = "sess-stop-terminal-ralplan";
6185
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
6186
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
6187
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
6188
+ active: true,
6189
+ skill: "ralplan",
6190
+ phase: "planning",
6191
+ session_id: sessionId,
6192
+ active_skills: [{
6193
+ skill: "ralplan",
6194
+ phase: "planning",
6195
+ active: true,
6196
+ session_id: sessionId,
6197
+ }],
6198
+ });
6199
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
6200
+ active: true,
6201
+ mode: "ralplan",
6202
+ current_phase: "planning",
6203
+ session_id: sessionId,
6204
+ });
6205
+ await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
6206
+ version: 1,
6207
+ mode: "ralplan",
6208
+ active: false,
6209
+ outcome: "finish",
6210
+ lifecycle_outcome: "finished",
6211
+ current_phase: "complete",
6212
+ completed_at: "2026-05-01T00:00:00.000Z",
6213
+ updated_at: "2026-05-01T00:00:00.000Z",
6214
+ });
6215
+ const result = await dispatchCodexNativeHook({
6216
+ hook_event_name: "Stop",
6217
+ cwd,
6218
+ session_id: sessionId,
6219
+ }, { cwd });
6220
+ assert.equal(result.omxEventName, "stop");
6221
+ assert.equal(result.outputJson, null);
6222
+ }
6223
+ finally {
6224
+ await rm(cwd, { recursive: true, force: true });
6225
+ }
6226
+ });
6227
+ it("does not block on stale ralplan skill-active when pinned mode state belongs to another session", async () => {
6228
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-foreign-ralplan-"));
6229
+ try {
6230
+ const stateDir = join(cwd, ".omx", "state");
6231
+ const sessionId = "sess-stop-current-ralplan";
6232
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
6233
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
6234
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
6235
+ active: true,
6236
+ skill: "ralplan",
6237
+ phase: "planning",
6238
+ session_id: sessionId,
6239
+ active_skills: [{
6240
+ skill: "ralplan",
6241
+ phase: "planning",
6242
+ active: true,
6243
+ session_id: sessionId,
6244
+ }],
6245
+ });
6246
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
6247
+ active: true,
6248
+ mode: "ralplan",
6249
+ current_phase: "planning",
6250
+ session_id: "sess-other-ralplan",
6251
+ });
6252
+ const result = await dispatchCodexNativeHook({
6253
+ hook_event_name: "Stop",
6254
+ cwd,
6255
+ session_id: sessionId,
6256
+ }, { cwd });
6257
+ assert.equal(result.omxEventName, "stop");
6258
+ assert.equal(result.outputJson, null);
6259
+ }
6260
+ finally {
6261
+ await rm(cwd, { recursive: true, force: true });
6262
+ }
6263
+ });
6264
+ it("returns an explicit ralplan waiting status while subagents are still active", async () => {
6265
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-subagent-"));
6266
+ try {
6267
+ const stateDir = join(cwd, ".omx", "state");
6268
+ await mkdir(join(stateDir, "sessions", "sess-stop-skill-subagent"), { recursive: true });
6269
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-skill-subagent" });
6270
+ await writeJson(join(stateDir, "sessions", "sess-stop-skill-subagent", "skill-active-state.json"), {
6271
+ active: true,
6272
+ skill: "ralplan",
5718
6273
  phase: "planning",
5719
6274
  });
5720
6275
  await writeJson(join(stateDir, "sessions", "sess-stop-skill-subagent", "ralplan-state.json"), {
@@ -6605,6 +7160,42 @@ exit 0
6605
7160
  await rm(cwd, { recursive: true, force: true });
6606
7161
  }
6607
7162
  });
7163
+ it("allows Stop from stale orphaned session-scoped Ralph starting iteration zero state", async () => {
7164
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-orphan-starting-ralph-"));
7165
+ try {
7166
+ const stateDir = join(cwd, ".omx", "state");
7167
+ const sessionId = "sess-stale-orphan-ralph";
7168
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
7169
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId, native_session_id: sessionId, cwd });
7170
+ await writeJson(join(stateDir, "sessions", sessionId, "ralph-state.json"), {
7171
+ active: true,
7172
+ mode: "ralph",
7173
+ current_phase: "starting",
7174
+ iteration: 0,
7175
+ session_id: sessionId,
7176
+ updated_at: "2000-01-01T00:00:00.000Z",
7177
+ });
7178
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
7179
+ active: true,
7180
+ skill: "ralph",
7181
+ phase: "starting",
7182
+ session_id: sessionId,
7183
+ active_skills: [{ skill: "ralph", phase: "starting", active: true, session_id: sessionId }],
7184
+ });
7185
+ const result = await dispatchCodexNativeHook({
7186
+ hook_event_name: "Stop",
7187
+ cwd,
7188
+ session_id: sessionId,
7189
+ thread_id: "thread-verifier-terminal",
7190
+ last_assistant_message: "APPROVE: read-only verifier evidence is fresh and sufficient.",
7191
+ }, { cwd });
7192
+ assert.equal(result.omxEventName, "stop");
7193
+ assert.equal(result.outputJson, null);
7194
+ }
7195
+ finally {
7196
+ await rm(cwd, { recursive: true, force: true });
7197
+ }
7198
+ });
6608
7199
  it("blocks Stop on visible active session-scoped Ralph starting state and reports its path", async () => {
6609
7200
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-visible-starting-ralph-"));
6610
7201
  try {
@@ -6640,6 +7231,126 @@ exit 0
6640
7231
  await rm(cwd, { recursive: true, force: true });
6641
7232
  }
6642
7233
  });
7234
+ it("retires prompt-seeded Ralph starting state when canonical Ralph already completed with audit", async () => {
7235
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-shadowed-starting-"));
7236
+ try {
7237
+ const stateDir = join(cwd, ".omx", "state");
7238
+ const nativeSessionId = "native-hook-seed";
7239
+ const canonicalSessionId = "omx-runtime-session";
7240
+ await mkdir(join(stateDir, "sessions", nativeSessionId), { recursive: true });
7241
+ await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
7242
+ await writeJson(join(stateDir, "session.json"), {
7243
+ session_id: canonicalSessionId,
7244
+ cwd,
7245
+ });
7246
+ await writeJson(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), {
7247
+ active: true,
7248
+ mode: "ralph",
7249
+ current_phase: "starting",
7250
+ session_id: nativeSessionId,
7251
+ iteration: 0,
7252
+ task_slug: "mvp-h-local-method-preflight-execution",
7253
+ started_at: "2026-05-14T07:00:00.000Z",
7254
+ });
7255
+ await writeJson(join(stateDir, "sessions", nativeSessionId, "skill-active-state.json"), {
7256
+ active: true,
7257
+ skill: "ralph",
7258
+ phase: "starting",
7259
+ session_id: nativeSessionId,
7260
+ active_skills: [{ skill: "ralph", phase: "starting", active: true, session_id: nativeSessionId }],
7261
+ });
7262
+ await writeJson(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), {
7263
+ active: false,
7264
+ mode: "ralph",
7265
+ current_phase: "complete",
7266
+ session_id: canonicalSessionId,
7267
+ completed_at: "2026-05-14T07:30:00.000Z",
7268
+ completion_audit: {
7269
+ passed: true,
7270
+ prompt_to_artifact_checklist: ["task evidence mapped"],
7271
+ verification_evidence: ["fresh verification evidence recorded"],
7272
+ },
7273
+ });
7274
+ const result = await dispatchCodexNativeHook({
7275
+ hook_event_name: "Stop",
7276
+ cwd,
7277
+ session_id: nativeSessionId,
7278
+ }, { cwd });
7279
+ assert.equal(result.omxEventName, "stop");
7280
+ assert.equal(result.outputJson, null);
7281
+ const retiredState = JSON.parse(await readFile(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), "utf-8"));
7282
+ assert.equal(retiredState.active, false);
7283
+ assert.equal(retiredState.current_phase, "complete");
7284
+ assert.equal(retiredState.stop_reason, "shadowed_by_completed_canonical_ralph");
7285
+ assert.equal(retiredState.shadowed_by_completed_canonical_ralph.session_id, canonicalSessionId);
7286
+ }
7287
+ finally {
7288
+ await rm(cwd, { recursive: true, force: true });
7289
+ }
7290
+ });
7291
+ it("does not retire prompt-seeded Ralph starting state from a completed canonical Ralph owned by another thread", async () => {
7292
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-shadowed-thread-mismatch-"));
7293
+ try {
7294
+ const stateDir = join(cwd, ".omx", "state");
7295
+ const nativeSessionId = "native-hook-seed";
7296
+ const canonicalSessionId = "omx-runtime-session";
7297
+ await mkdir(join(stateDir, "sessions", nativeSessionId), { recursive: true });
7298
+ await mkdir(join(stateDir, "sessions", canonicalSessionId), { recursive: true });
7299
+ await writeJson(join(stateDir, "session.json"), {
7300
+ session_id: canonicalSessionId,
7301
+ cwd,
7302
+ });
7303
+ await writeJson(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), {
7304
+ active: true,
7305
+ mode: "ralph",
7306
+ current_phase: "starting",
7307
+ session_id: nativeSessionId,
7308
+ iteration: 0,
7309
+ task_slug: "mvp-h-local-method-preflight-execution",
7310
+ started_at: "2026-05-14T07:00:00.000Z",
7311
+ });
7312
+ await writeJson(join(stateDir, "sessions", nativeSessionId, "skill-active-state.json"), {
7313
+ active: true,
7314
+ skill: "ralph",
7315
+ phase: "starting",
7316
+ session_id: nativeSessionId,
7317
+ active_skills: [{ skill: "ralph", phase: "starting", active: true, session_id: nativeSessionId }],
7318
+ });
7319
+ await writeJson(join(stateDir, "sessions", canonicalSessionId, "ralph-state.json"), {
7320
+ active: false,
7321
+ mode: "ralph",
7322
+ current_phase: "complete",
7323
+ session_id: canonicalSessionId,
7324
+ owner_codex_thread_id: "thread-A",
7325
+ completed_at: "2026-05-14T07:30:00.000Z",
7326
+ completion_audit: {
7327
+ passed: true,
7328
+ prompt_to_artifact_checklist: ["task evidence mapped"],
7329
+ verification_evidence: ["fresh verification evidence recorded"],
7330
+ },
7331
+ });
7332
+ const result = await dispatchCodexNativeHook({
7333
+ hook_event_name: "Stop",
7334
+ cwd,
7335
+ session_id: nativeSessionId,
7336
+ thread_id: "thread-B",
7337
+ }, { cwd });
7338
+ assert.equal(result.omxEventName, "stop");
7339
+ assert.deepEqual(result.outputJson, {
7340
+ decision: "block",
7341
+ reason: "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.",
7342
+ stopReason: "ralph_starting",
7343
+ systemMessage: "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.",
7344
+ });
7345
+ const preservedState = JSON.parse(await readFile(join(stateDir, "sessions", nativeSessionId, "ralph-state.json"), "utf-8"));
7346
+ assert.equal(preservedState.active, true);
7347
+ assert.equal(preservedState.current_phase, "starting");
7348
+ assert.equal(preservedState.stop_reason, undefined);
7349
+ }
7350
+ finally {
7351
+ await rm(cwd, { recursive: true, force: true });
7352
+ }
7353
+ });
6643
7354
  it("does not block Stop from another session-scoped Ralph state when an explicit session_id has no active Ralph state", async () => {
6644
7355
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-explicit-session-ralph-"));
6645
7356
  try {