oh-my-codex 0.15.2 → 0.15.3

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 (166) hide show
  1. package/Cargo.lock +5 -5
  2. package/Cargo.toml +1 -1
  3. package/dist/agents/__tests__/native-config.test.js +33 -0
  4. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  5. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js +9 -1
  6. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js.map +1 -1
  7. package/dist/cli/__tests__/doctor-context-window-warning.test.d.ts +2 -0
  8. package/dist/cli/__tests__/doctor-context-window-warning.test.d.ts.map +1 -0
  9. package/dist/cli/__tests__/doctor-context-window-warning.test.js +122 -0
  10. package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -0
  11. package/dist/cli/__tests__/doctor-warning-copy.test.js +2 -2
  12. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  13. package/dist/cli/__tests__/exec.test.js +1 -0
  14. package/dist/cli/__tests__/exec.test.js.map +1 -1
  15. package/dist/cli/__tests__/explore.test.js +40 -17
  16. package/dist/cli/__tests__/explore.test.js.map +1 -1
  17. package/dist/cli/__tests__/index.test.js +141 -8
  18. package/dist/cli/__tests__/index.test.js.map +1 -1
  19. package/dist/cli/__tests__/mcp-serve.test.js +27 -1
  20. package/dist/cli/__tests__/mcp-serve.test.js.map +1 -1
  21. package/dist/cli/__tests__/ralph.test.js +59 -1
  22. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  23. package/dist/cli/__tests__/setup-scope.test.js +2 -1
  24. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  25. package/dist/cli/__tests__/team.test.js +55 -10
  26. package/dist/cli/__tests__/team.test.js.map +1 -1
  27. package/dist/cli/doctor.d.ts.map +1 -1
  28. package/dist/cli/doctor.js +46 -3
  29. package/dist/cli/doctor.js.map +1 -1
  30. package/dist/cli/index.d.ts +16 -1
  31. package/dist/cli/index.d.ts.map +1 -1
  32. package/dist/cli/index.js +126 -15
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/mcp-serve.d.ts +1 -0
  35. package/dist/cli/mcp-serve.d.ts.map +1 -1
  36. package/dist/cli/mcp-serve.js +8 -0
  37. package/dist/cli/mcp-serve.js.map +1 -1
  38. package/dist/cli/ralph.d.ts +2 -0
  39. package/dist/cli/ralph.d.ts.map +1 -1
  40. package/dist/cli/ralph.js +17 -1
  41. package/dist/cli/ralph.js.map +1 -1
  42. package/dist/cli/team.d.ts +4 -0
  43. package/dist/cli/team.d.ts.map +1 -1
  44. package/dist/cli/team.js +47 -22
  45. package/dist/cli/team.js.map +1 -1
  46. package/dist/config/__tests__/generator-idempotent.test.js +27 -5
  47. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  48. package/dist/config/generator.d.ts +11 -2
  49. package/dist/config/generator.d.ts.map +1 -1
  50. package/dist/config/generator.js +114 -58
  51. package/dist/config/generator.js.map +1 -1
  52. package/dist/hooks/__tests__/agents-overlay.test.js +59 -0
  53. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  54. package/dist/hooks/__tests__/anti-slop-workflow.test.js +109 -18
  55. package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
  56. package/dist/hooks/agents-overlay.d.ts.map +1 -1
  57. package/dist/hooks/agents-overlay.js +21 -0
  58. package/dist/hooks/agents-overlay.js.map +1 -1
  59. package/dist/hud/__tests__/index.test.js +30 -14
  60. package/dist/hud/__tests__/index.test.js.map +1 -1
  61. package/dist/openclaw/__tests__/dispatcher.test.js +1 -1
  62. package/dist/openclaw/__tests__/dispatcher.test.js.map +1 -1
  63. package/dist/pipeline/__tests__/stages.test.js +398 -14
  64. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  65. package/dist/pipeline/stages/team-exec.d.ts +8 -4
  66. package/dist/pipeline/stages/team-exec.d.ts.map +1 -1
  67. package/dist/pipeline/stages/team-exec.js +198 -13
  68. package/dist/pipeline/stages/team-exec.js.map +1 -1
  69. package/dist/planning/__tests__/artifacts.test.js +246 -1
  70. package/dist/planning/__tests__/artifacts.test.js.map +1 -1
  71. package/dist/planning/artifact-names.d.ts +13 -0
  72. package/dist/planning/artifact-names.d.ts.map +1 -0
  73. package/dist/planning/artifact-names.js +108 -0
  74. package/dist/planning/artifact-names.js.map +1 -0
  75. package/dist/planning/artifacts.d.ts +22 -1
  76. package/dist/planning/artifacts.d.ts.map +1 -1
  77. package/dist/planning/artifacts.js +165 -50
  78. package/dist/planning/artifacts.js.map +1 -1
  79. package/dist/ralph/__tests__/persistence.test.js +21 -1
  80. package/dist/ralph/__tests__/persistence.test.js.map +1 -1
  81. package/dist/ralph/persistence.d.ts.map +1 -1
  82. package/dist/ralph/persistence.js +6 -4
  83. package/dist/ralph/persistence.js.map +1 -1
  84. package/dist/scripts/__tests__/codex-native-hook.test.js +352 -2
  85. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  86. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  87. package/dist/scripts/codex-native-hook.js +85 -6
  88. package/dist/scripts/codex-native-hook.js.map +1 -1
  89. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  90. package/dist/scripts/codex-native-pre-post.js +123 -0
  91. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  92. package/dist/scripts/notify-hook/team-worker-posttooluse.js +1 -1
  93. package/dist/scripts/notify-hook/team-worker-posttooluse.js.map +1 -1
  94. package/dist/scripts/notify-hook.js +1 -1
  95. package/dist/scripts/notify-hook.js.map +1 -1
  96. package/dist/scripts/sync-plugin-mirror.d.ts +1 -0
  97. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  98. package/dist/scripts/sync-plugin-mirror.js +8 -2
  99. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  100. package/dist/state/__tests__/skill-active.test.js +41 -0
  101. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  102. package/dist/team/__tests__/api-interop.test.js +220 -0
  103. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  104. package/dist/team/__tests__/model-contract.test.js +40 -9
  105. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  106. package/dist/team/__tests__/repo-aware-decomposition.test.js +41 -0
  107. package/dist/team/__tests__/repo-aware-decomposition.test.js.map +1 -1
  108. package/dist/team/__tests__/runtime-cli.test.js +24 -0
  109. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  110. package/dist/team/__tests__/runtime.test.js +446 -67
  111. package/dist/team/__tests__/runtime.test.js.map +1 -1
  112. package/dist/team/__tests__/state.test.js +13 -0
  113. package/dist/team/__tests__/state.test.js.map +1 -1
  114. package/dist/team/__tests__/team-identity.test.d.ts +2 -0
  115. package/dist/team/__tests__/team-identity.test.d.ts.map +1 -0
  116. package/dist/team/__tests__/team-identity.test.js +166 -0
  117. package/dist/team/__tests__/team-identity.test.js.map +1 -0
  118. package/dist/team/__tests__/tmux-session.test.js +55 -1
  119. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  120. package/dist/team/__tests__/worker-bootstrap.test.js +12 -0
  121. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  122. package/dist/team/api-interop.d.ts +1 -0
  123. package/dist/team/api-interop.d.ts.map +1 -1
  124. package/dist/team/api-interop.js +159 -129
  125. package/dist/team/api-interop.js.map +1 -1
  126. package/dist/team/delivery-log.d.ts +1 -1
  127. package/dist/team/delivery-log.d.ts.map +1 -1
  128. package/dist/team/delivery-log.js.map +1 -1
  129. package/dist/team/repo-aware-decomposition.d.ts +3 -0
  130. package/dist/team/repo-aware-decomposition.d.ts.map +1 -1
  131. package/dist/team/repo-aware-decomposition.js +2 -0
  132. package/dist/team/repo-aware-decomposition.js.map +1 -1
  133. package/dist/team/runtime-cli.d.ts +32 -2
  134. package/dist/team/runtime-cli.d.ts.map +1 -1
  135. package/dist/team/runtime-cli.js +78 -26
  136. package/dist/team/runtime-cli.js.map +1 -1
  137. package/dist/team/runtime.d.ts +1 -1
  138. package/dist/team/runtime.d.ts.map +1 -1
  139. package/dist/team/runtime.js +338 -35
  140. package/dist/team/runtime.js.map +1 -1
  141. package/dist/team/state.d.ts +9 -0
  142. package/dist/team/state.d.ts.map +1 -1
  143. package/dist/team/state.js +21 -0
  144. package/dist/team/state.js.map +1 -1
  145. package/dist/team/team-identity.d.ts +26 -0
  146. package/dist/team/team-identity.d.ts.map +1 -0
  147. package/dist/team/team-identity.js +169 -0
  148. package/dist/team/team-identity.js.map +1 -0
  149. package/dist/team/tmux-session.d.ts +18 -0
  150. package/dist/team/tmux-session.d.ts.map +1 -1
  151. package/dist/team/tmux-session.js +61 -1
  152. package/dist/team/tmux-session.js.map +1 -1
  153. package/dist/team/worker-bootstrap.d.ts +2 -0
  154. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  155. package/dist/team/worker-bootstrap.js +10 -1
  156. package/dist/team/worker-bootstrap.js.map +1 -1
  157. package/package.json +1 -1
  158. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  159. package/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md +30 -5
  160. package/skills/ai-slop-cleaner/SKILL.md +30 -5
  161. package/src/scripts/__tests__/codex-native-hook.test.ts +398 -2
  162. package/src/scripts/codex-native-hook.ts +115 -5
  163. package/src/scripts/codex-native-pre-post.ts +121 -0
  164. package/src/scripts/notify-hook/team-worker-posttooluse.ts +1 -1
  165. package/src/scripts/notify-hook.ts +1 -1
  166. package/src/scripts/sync-plugin-mirror.ts +11 -2
@@ -2420,6 +2420,147 @@ esac
2420
2420
  }
2421
2421
  });
2422
2422
 
2423
+ it("warns on PreToolUse for vague sloppy fallback implementation framing", async () => {
2424
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-warn-"));
2425
+ try {
2426
+ const result = await dispatchCodexNativeHook(
2427
+ {
2428
+ hook_event_name: "PreToolUse",
2429
+ cwd,
2430
+ tool_name: "Bash",
2431
+ tool_use_id: "tool-slop-warn",
2432
+ tool_input: {
2433
+ command: [
2434
+ "cat > src/runtime.ts <<'EOF'",
2435
+ "export function loadRuntime() {",
2436
+ " // implement a quick hack fallback if it fails",
2437
+ " return process.env.RUNTIME || 'local';",
2438
+ "}",
2439
+ "EOF",
2440
+ ].join("\n"),
2441
+ },
2442
+ },
2443
+ { cwd },
2444
+ );
2445
+
2446
+ assert.equal(result.omxEventName, "pre-tool-use");
2447
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, undefined);
2448
+ assert.equal((result.outputJson as { hookSpecificOutput?: { hookEventName?: string } } | null)?.hookSpecificOutput?.hookEventName, "PreToolUse");
2449
+ assert.match(JSON.stringify(result.outputJson), /don't make potential slop/);
2450
+ assert.match(JSON.stringify(result.outputJson), /architect/);
2451
+ assert.match(JSON.stringify(result.outputJson), /environment issue/);
2452
+ } finally {
2453
+ await rm(cwd, { recursive: true, force: true });
2454
+ }
2455
+ });
2456
+
2457
+ it("does not warn on PreToolUse for read-only fallback text inspection", async () => {
2458
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-readonly-"));
2459
+ try {
2460
+ const result = await dispatchCodexNativeHook(
2461
+ {
2462
+ hook_event_name: "PreToolUse",
2463
+ cwd,
2464
+ tool_name: "Bash",
2465
+ tool_use_id: "tool-slop-readonly",
2466
+ tool_input: { command: "rg \"quick hack fallback if it fails\" src docs" },
2467
+ },
2468
+ { cwd },
2469
+ );
2470
+
2471
+ assert.equal(result.omxEventName, "pre-tool-use");
2472
+ assert.equal(result.outputJson, null);
2473
+ } finally {
2474
+ await rm(cwd, { recursive: true, force: true });
2475
+ }
2476
+ });
2477
+
2478
+ it("warns when a read-only command is chained before sloppy fallback writes", async () => {
2479
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-chained-write-"));
2480
+ try {
2481
+ const result = await dispatchCodexNativeHook(
2482
+ {
2483
+ hook_event_name: "PreToolUse",
2484
+ cwd,
2485
+ tool_name: "Bash",
2486
+ tool_use_id: "tool-slop-chained-write",
2487
+ tool_input: {
2488
+ command: [
2489
+ "rg foo src && cat > src/runtime.ts <<EOF",
2490
+ "export function loadRuntime() {",
2491
+ " // implement quick hack fallback if it fails",
2492
+ " return 'local';",
2493
+ "}",
2494
+ "EOF",
2495
+ ].join("\n"),
2496
+ },
2497
+ },
2498
+ { cwd },
2499
+ );
2500
+
2501
+ assert.equal(result.omxEventName, "pre-tool-use");
2502
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, undefined);
2503
+ assert.equal((result.outputJson as { hookSpecificOutput?: { hookEventName?: string } } | null)?.hookSpecificOutput?.hookEventName, "PreToolUse");
2504
+ assert.match(JSON.stringify(result.outputJson), /don't make potential slop/);
2505
+ } finally {
2506
+ await rm(cwd, { recursive: true, force: true });
2507
+ }
2508
+ });
2509
+
2510
+ it("does not warn on PreToolUse for grounded compatibility fallback code", async () => {
2511
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-grounded-"));
2512
+ try {
2513
+ const result = await dispatchCodexNativeHook(
2514
+ {
2515
+ hook_event_name: "PreToolUse",
2516
+ cwd,
2517
+ tool_name: "Bash",
2518
+ tool_use_id: "tool-slop-grounded",
2519
+ tool_input: {
2520
+ command: [
2521
+ "cat > src/compat.ts <<'EOF'",
2522
+ "export function resolveCompatMode() {",
2523
+ " // temporary fallback because legacy compatibility needs fail-safe startup behavior",
2524
+ " return 'legacy';",
2525
+ "}",
2526
+ "// Tested: npm test",
2527
+ "EOF",
2528
+ ].join("\n"),
2529
+ },
2530
+ },
2531
+ { cwd },
2532
+ );
2533
+
2534
+ assert.equal(result.omxEventName, "pre-tool-use");
2535
+ assert.equal(result.outputJson, null);
2536
+ } finally {
2537
+ await rm(cwd, { recursive: true, force: true });
2538
+ }
2539
+ });
2540
+
2541
+ it("keeps git commit Lore enforcement ahead of sloppy fallback advisory", async () => {
2542
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-slop-git-priority-"));
2543
+ try {
2544
+ const result = await dispatchCodexNativeHook(
2545
+ {
2546
+ hook_event_name: "PreToolUse",
2547
+ cwd,
2548
+ tool_name: "Bash",
2549
+ tool_use_id: "tool-slop-git-priority",
2550
+ tool_input: { command: 'git commit -m "quick hack fallback if it fails"' },
2551
+ },
2552
+ { cwd },
2553
+ );
2554
+
2555
+ assert.equal(result.omxEventName, "pre-tool-use");
2556
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block");
2557
+ assert.match(JSON.stringify(result.outputJson), /Lore protocol/);
2558
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /don't make potential slop/);
2559
+ } finally {
2560
+ await rm(cwd, { recursive: true, force: true });
2561
+ }
2562
+ });
2563
+
2423
2564
  it("blocks PreToolUse git commit with supported response shape when the inline message is not Lore-compliant", async () => {
2424
2565
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-git-commit-invalid-"));
2425
2566
  try {
@@ -3946,7 +4087,7 @@ esac
3946
4087
  team_state_root: join(cwd, ".omx", "state"),
3947
4088
  });
3948
4089
  await writeJson(join(workerDir, "status.json"), {
3949
- state: "idle",
4090
+ state: "working",
3950
4091
  current_task_id: "1",
3951
4092
  updated_at: new Date().toISOString(),
3952
4093
  });
@@ -3990,6 +4131,72 @@ esac
3990
4131
  }
3991
4132
  });
3992
4133
 
4134
+ it("does not block Stop as a team-worker task failure when worker status is already terminal", async () => {
4135
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-terminal-stale-"));
4136
+ const prevTeamWorker = process.env.OMX_TEAM_WORKER;
4137
+ const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
4138
+ const prevLeaderCwd = process.env.OMX_TEAM_LEADER_CWD;
4139
+ try {
4140
+ await initTeamState(
4141
+ "worker-stale-team",
4142
+ "worker stale stop fallback",
4143
+ "executor",
4144
+ 1,
4145
+ cwd,
4146
+ undefined,
4147
+ { ...process.env, OMX_SESSION_ID: "sess-stop-team-worker-stale" },
4148
+ );
4149
+ const stateDir = join(cwd, ".omx", "state");
4150
+ const workerCwd = join(cwd, ".omx", "team", "worker-stale-team", "worktrees", "worker-1");
4151
+ const workerDir = join(stateDir, "team", "worker-stale-team", "workers", "worker-1");
4152
+ await mkdir(workerCwd, { recursive: true });
4153
+ await writeJson(join(workerDir, "identity.json"), {
4154
+ name: "worker-1",
4155
+ index: 1,
4156
+ role: "executor",
4157
+ assigned_tasks: ["1"],
4158
+ worktree_path: workerCwd,
4159
+ team_state_root: stateDir,
4160
+ });
4161
+ await writeJson(join(workerDir, "status.json"), {
4162
+ state: "done",
4163
+ current_task_id: "1",
4164
+ updated_at: new Date().toISOString(),
4165
+ });
4166
+ await writeJson(join(stateDir, "team", "worker-stale-team", "tasks", "task-1.json"), {
4167
+ id: "1",
4168
+ subject: "stale hook task",
4169
+ description: "stale task should not trap terminal worker Stop",
4170
+ status: "in_progress",
4171
+ owner: "worker-1",
4172
+ created_at: new Date().toISOString(),
4173
+ });
4174
+
4175
+ process.env.OMX_TEAM_WORKER = "worker-stale-team/worker-1";
4176
+ process.env.OMX_TEAM_STATE_ROOT = stateDir;
4177
+ process.env.OMX_TEAM_LEADER_CWD = cwd;
4178
+
4179
+ const result = await dispatchCodexNativeHook(
4180
+ {
4181
+ hook_event_name: "Stop",
4182
+ cwd: workerCwd,
4183
+ session_id: "sess-stop-team-worker-stale",
4184
+ },
4185
+ { cwd: workerCwd },
4186
+ );
4187
+
4188
+ assert.equal((result.outputJson as { stopReason?: string } | null)?.stopReason, "team_team-exec");
4189
+ } finally {
4190
+ if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
4191
+ else delete process.env.OMX_TEAM_WORKER;
4192
+ if (typeof prevTeamStateRoot === "string") process.env.OMX_TEAM_STATE_ROOT = prevTeamStateRoot;
4193
+ else delete process.env.OMX_TEAM_STATE_ROOT;
4194
+ if (typeof prevLeaderCwd === "string") process.env.OMX_TEAM_LEADER_CWD = prevLeaderCwd;
4195
+ else delete process.env.OMX_TEAM_LEADER_CWD;
4196
+ await rm(cwd, { recursive: true, force: true });
4197
+ }
4198
+ });
4199
+
3993
4200
  it("suppresses identical team worker Stop replays but re-blocks fresh turns and task state changes", async () => {
3994
4201
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-repeat-"));
3995
4202
  try {
@@ -4016,7 +4223,7 @@ esac
4016
4223
  team_state_root: stateDir,
4017
4224
  });
4018
4225
  await writeJson(join(workerDir, "status.json"), {
4019
- state: "idle",
4226
+ state: "working",
4020
4227
  current_task_id: "1",
4021
4228
  updated_at: new Date().toISOString(),
4022
4229
  });
@@ -4648,6 +4855,100 @@ esac
4648
4855
  }
4649
4856
  });
4650
4857
 
4858
+ it("does not block on stale ralplan skill-active when canonical run-state is terminal", async () => {
4859
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-terminal-ralplan-run-"));
4860
+ try {
4861
+ const stateDir = join(cwd, ".omx", "state");
4862
+ const sessionId = "sess-stop-terminal-ralplan";
4863
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
4864
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
4865
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
4866
+ active: true,
4867
+ skill: "ralplan",
4868
+ phase: "planning",
4869
+ session_id: sessionId,
4870
+ active_skills: [{
4871
+ skill: "ralplan",
4872
+ phase: "planning",
4873
+ active: true,
4874
+ session_id: sessionId,
4875
+ }],
4876
+ });
4877
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
4878
+ active: true,
4879
+ mode: "ralplan",
4880
+ current_phase: "planning",
4881
+ session_id: sessionId,
4882
+ });
4883
+ await writeJson(join(stateDir, "sessions", sessionId, "run-state.json"), {
4884
+ version: 1,
4885
+ mode: "ralplan",
4886
+ active: false,
4887
+ outcome: "finish",
4888
+ lifecycle_outcome: "finished",
4889
+ current_phase: "complete",
4890
+ completed_at: "2026-05-01T00:00:00.000Z",
4891
+ updated_at: "2026-05-01T00:00:00.000Z",
4892
+ });
4893
+
4894
+ const result = await dispatchCodexNativeHook(
4895
+ {
4896
+ hook_event_name: "Stop",
4897
+ cwd,
4898
+ session_id: sessionId,
4899
+ },
4900
+ { cwd },
4901
+ );
4902
+
4903
+ assert.equal(result.omxEventName, "stop");
4904
+ assert.equal(result.outputJson, null);
4905
+ } finally {
4906
+ await rm(cwd, { recursive: true, force: true });
4907
+ }
4908
+ });
4909
+
4910
+ it("does not block on stale ralplan skill-active when pinned mode state belongs to another session", async () => {
4911
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-foreign-ralplan-"));
4912
+ try {
4913
+ const stateDir = join(cwd, ".omx", "state");
4914
+ const sessionId = "sess-stop-current-ralplan";
4915
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
4916
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
4917
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
4918
+ active: true,
4919
+ skill: "ralplan",
4920
+ phase: "planning",
4921
+ session_id: sessionId,
4922
+ active_skills: [{
4923
+ skill: "ralplan",
4924
+ phase: "planning",
4925
+ active: true,
4926
+ session_id: sessionId,
4927
+ }],
4928
+ });
4929
+ await writeJson(join(stateDir, "sessions", sessionId, "ralplan-state.json"), {
4930
+ active: true,
4931
+ mode: "ralplan",
4932
+ current_phase: "planning",
4933
+ session_id: "sess-other-ralplan",
4934
+ });
4935
+
4936
+ const result = await dispatchCodexNativeHook(
4937
+ {
4938
+ hook_event_name: "Stop",
4939
+ cwd,
4940
+ session_id: sessionId,
4941
+ },
4942
+ { cwd },
4943
+ );
4944
+
4945
+ assert.equal(result.omxEventName, "stop");
4946
+ assert.equal(result.outputJson, null);
4947
+ } finally {
4948
+ await rm(cwd, { recursive: true, force: true });
4949
+ }
4950
+ });
4951
+
4651
4952
  it("does not block on active ralplan skill when subagents are still active", async () => {
4652
4953
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-subagent-"));
4653
4954
  try {
@@ -5654,6 +5955,101 @@ esac
5654
5955
  }
5655
5956
  });
5656
5957
 
5958
+ it("does not block a question-only pane from Ralph state owned by another Codex session", async () => {
5959
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-question-pane-"));
5960
+ const previousTmuxPane = process.env.TMUX_PANE;
5961
+ try {
5962
+ const stateDir = join(cwd, ".omx", "state");
5963
+ const questionSessionId = "sess-question-pane";
5964
+ const questionNativeSessionId = "codex-question-pane";
5965
+ await mkdir(join(stateDir, "sessions", questionSessionId), { recursive: true });
5966
+ await writeJson(join(stateDir, "session.json"), {
5967
+ session_id: questionSessionId,
5968
+ native_session_id: questionNativeSessionId,
5969
+ cwd,
5970
+ });
5971
+ await writeJson(join(stateDir, "sessions", questionSessionId, "ralph-state.json"), {
5972
+ active: true,
5973
+ mode: "ralph",
5974
+ current_phase: "executing",
5975
+ session_id: questionSessionId,
5976
+ owner_omx_session_id: "sess-ralph-owner",
5977
+ owner_codex_session_id: "codex-ralph-owner",
5978
+ thread_id: "thread-ralph-owner",
5979
+ tmux_pane_id: "%41",
5980
+ });
5981
+
5982
+ process.env.TMUX_PANE = "%99";
5983
+ const result = await dispatchCodexNativeHook(
5984
+ {
5985
+ hook_event_name: "Stop",
5986
+ cwd,
5987
+ session_id: questionNativeSessionId,
5988
+ thread_id: "thread-question-pane",
5989
+ },
5990
+ { cwd },
5991
+ );
5992
+
5993
+ assert.equal(result.omxEventName, "stop");
5994
+ assert.equal(result.outputJson, null);
5995
+ } finally {
5996
+ if (typeof previousTmuxPane === "string") process.env.TMUX_PANE = previousTmuxPane;
5997
+ else delete process.env.TMUX_PANE;
5998
+ await rm(cwd, { recursive: true, force: true });
5999
+ }
6000
+ });
6001
+
6002
+ it("blocks same-session Ralph Stop continuation when ownership identifiers match", async () => {
6003
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-owned-session-"));
6004
+ const previousTmuxPane = process.env.TMUX_PANE;
6005
+ try {
6006
+ const stateDir = join(cwd, ".omx", "state");
6007
+ const omxSessionId = "sess-ralph-owned";
6008
+ const nativeSessionId = "codex-ralph-owned";
6009
+ await mkdir(join(stateDir, "sessions", omxSessionId), { recursive: true });
6010
+ await writeJson(join(stateDir, "session.json"), {
6011
+ session_id: omxSessionId,
6012
+ native_session_id: nativeSessionId,
6013
+ cwd,
6014
+ });
6015
+ await writeJson(join(stateDir, "sessions", omxSessionId, "ralph-state.json"), {
6016
+ active: true,
6017
+ mode: "ralph",
6018
+ current_phase: "executing",
6019
+ session_id: omxSessionId,
6020
+ owner_omx_session_id: omxSessionId,
6021
+ owner_codex_session_id: nativeSessionId,
6022
+ thread_id: "thread-ralph-owned",
6023
+ tmux_pane_id: "%42",
6024
+ });
6025
+
6026
+ process.env.TMUX_PANE = "%42";
6027
+ const result = await dispatchCodexNativeHook(
6028
+ {
6029
+ hook_event_name: "Stop",
6030
+ cwd,
6031
+ session_id: nativeSessionId,
6032
+ thread_id: "thread-ralph-owned",
6033
+ },
6034
+ { cwd },
6035
+ );
6036
+
6037
+ assert.equal(result.omxEventName, "stop");
6038
+ assert.deepEqual(result.outputJson, {
6039
+ decision: "block",
6040
+ reason:
6041
+ "OMX Ralph is still active (phase: executing; state: .omx/state/sessions/sess-ralph-owned/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
6042
+ stopReason: "ralph_executing",
6043
+ systemMessage:
6044
+ "OMX Ralph is still active (phase: executing; state: .omx/state/sessions/sess-ralph-owned/ralph-state.json); continue the task and gather fresh verification evidence before stopping.",
6045
+ });
6046
+ } finally {
6047
+ if (typeof previousTmuxPane === "string") process.env.TMUX_PANE = previousTmuxPane;
6048
+ else delete process.env.TMUX_PANE;
6049
+ await rm(cwd, { recursive: true, force: true });
6050
+ }
6051
+ });
6052
+
5657
6053
  it("prefers canonical run-state terminal lifecycle before stale session Ralph state during Stop", async () => {
5658
6054
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-canonical-run-state-ralph-"));
5659
6055
  try {
@@ -107,9 +107,10 @@ export interface NativeHookDispatchResult {
107
107
  outputJson: Record<string, unknown> | null;
108
108
  }
109
109
 
110
- const TERMINAL_MODE_PHASES = new Set(["complete", "failed", "cancelled"]);
110
+ const TERMINAL_MODE_PHASES = new Set(["complete", "completed", "failed", "cancelled"]);
111
111
  const SKILL_STOP_BLOCKERS = new Set(["ralplan"]);
112
112
  const TEAM_TERMINAL_TASK_STATUSES = new Set(["completed", "failed"]);
113
+ const TEAM_WORKER_STOP_ACTIVE_STATES = new Set(["working", "blocked"]);
113
114
  const NATIVE_STOP_STATE_FILE = "native-stop-state.json";
114
115
  const STABLE_FINAL_RECOMMENDATION_PATTERNS = [
115
116
  /^\s*(?:launch|release|ship)-?ready\s*:\s*(?:yes|no)\b[^\n\r]*/im,
@@ -444,10 +445,59 @@ interface ActiveRalphStopState {
444
445
  path: string;
445
446
  }
446
447
 
448
+ interface RalphStopOwnershipContext {
449
+ sessionId: string;
450
+ payloadSessionId: string;
451
+ threadId: string;
452
+ currentNativeSessionId: string;
453
+ tmuxPaneId: string;
454
+ }
455
+
447
456
  function isRalphStartingPhase(state: Record<string, unknown>): boolean {
448
457
  return safeString(state.current_phase ?? state.currentPhase).trim().toLowerCase() === "starting";
449
458
  }
450
459
 
460
+ function hasValue(values: string[], value: string): boolean {
461
+ return value !== "" && values.some((candidate) => candidate === value);
462
+ }
463
+
464
+ function activeRalphStateMatchesStopOwner(
465
+ state: Record<string, unknown>,
466
+ context: RalphStopOwnershipContext,
467
+ ): boolean {
468
+ const ownerOmxSessionId = safeString(state.owner_omx_session_id).trim();
469
+ if (ownerOmxSessionId && ownerOmxSessionId !== context.sessionId) {
470
+ return false;
471
+ }
472
+
473
+ const stateSessionId = safeString(state.session_id).trim();
474
+ if (!ownerOmxSessionId && stateSessionId && stateSessionId !== context.sessionId) {
475
+ return false;
476
+ }
477
+
478
+ const codexOwnerSessionId = safeString(state.owner_codex_session_id).trim();
479
+ if (codexOwnerSessionId) {
480
+ const stopCodexSessionIds = [
481
+ context.payloadSessionId,
482
+ context.currentNativeSessionId,
483
+ context.sessionId,
484
+ ].filter(Boolean);
485
+ if (!hasValue(stopCodexSessionIds, codexOwnerSessionId)) return false;
486
+ }
487
+
488
+ const stateThreadId = safeString(state.owner_codex_thread_id ?? state.thread_id).trim();
489
+ if (stateThreadId && context.threadId && stateThreadId !== context.threadId) {
490
+ return false;
491
+ }
492
+
493
+ const statePaneId = safeString(state.tmux_pane_id).trim();
494
+ if (statePaneId && context.tmuxPaneId && statePaneId !== context.tmuxPaneId) {
495
+ return false;
496
+ }
497
+
498
+ return true;
499
+ }
500
+
451
501
  function shouldHonorCanonicalTerminalRunState(
452
502
  runState: Record<string, unknown> | null,
453
503
  mode: string,
@@ -481,6 +531,11 @@ async function isVisibleRalphActiveForSession(cwd: string, sessionId: string): P
481
531
  async function readActiveRalphState(
482
532
  stateDir: string,
483
533
  preferredSessionId?: string,
534
+ ownerContext?: {
535
+ payloadSessionId?: string;
536
+ threadId?: string;
537
+ tmuxPaneId?: string;
538
+ },
484
539
  ): Promise<ActiveRalphStopState | null> {
485
540
  const cwd = resolve(stateDir, "..", "..");
486
541
  const [rawSessionInfo, usableSessionInfo] = await Promise.all([
@@ -488,6 +543,7 @@ async function readActiveRalphState(
488
543
  readUsableSessionState(cwd),
489
544
  ]);
490
545
  const currentOmxSessionId = safeString(usableSessionInfo?.session_id).trim();
546
+ const currentNativeSessionId = safeString(usableSessionInfo?.native_session_id).trim();
491
547
  const staleCurrentSessionId = rawSessionInfo && !isSessionStateUsable(rawSessionInfo, cwd)
492
548
  ? safeString(rawSessionInfo.session_id).trim()
493
549
  : "";
@@ -515,7 +571,17 @@ async function readActiveRalphState(
515
571
  ) {
516
572
  continue;
517
573
  }
518
- if (sessionScoped?.active === true && shouldContinueRun(sessionScoped)) {
574
+ if (
575
+ sessionScoped?.active === true
576
+ && shouldContinueRun(sessionScoped)
577
+ && activeRalphStateMatchesStopOwner(sessionScoped, {
578
+ sessionId,
579
+ payloadSessionId: safeString(ownerContext?.payloadSessionId).trim(),
580
+ threadId: safeString(ownerContext?.threadId).trim(),
581
+ currentNativeSessionId,
582
+ tmuxPaneId: safeString(ownerContext?.tmuxPaneId).trim(),
583
+ })
584
+ ) {
519
585
  return { state: sessionScoped, path: sessionScopedPath };
520
586
  }
521
587
  }
@@ -1131,7 +1197,7 @@ async function resolveTeamStateDirForWorkerContext(
1131
1197
  async function buildTeamWorkerStopOutput(
1132
1198
  cwd: string,
1133
1199
  ): Promise<Record<string, unknown> | null> {
1134
- const workerContext = parseTeamWorkerEnv(safeString(process.env.OMX_TEAM_WORKER));
1200
+ const workerContext = parseTeamWorkerEnv(safeString(process.env.OMX_TEAM_INTERNAL_WORKER || process.env.OMX_TEAM_WORKER));
1135
1201
  if (!workerContext) return null;
1136
1202
 
1137
1203
  const stateDir = await resolveTeamStateDirForWorkerContext(cwd, workerContext);
@@ -1142,6 +1208,9 @@ async function buildTeamWorkerStopOutput(
1142
1208
  readJsonIfExists(join(workerRoot, "status.json")),
1143
1209
  ]);
1144
1210
 
1211
+ const workerState = safeString(status?.state).trim().toLowerCase();
1212
+ if (!TEAM_WORKER_STOP_ACTIVE_STATES.has(workerState)) return null;
1213
+
1145
1214
  const candidateTaskIds = new Set<string>();
1146
1215
  const currentTaskId = safeString(status?.current_task_id).trim();
1147
1216
  if (currentTaskId) candidateTaskIds.add(currentTaskId);
@@ -1171,7 +1240,7 @@ async function buildTeamWorkerStopOutput(
1171
1240
  }
1172
1241
 
1173
1242
  function hasTeamWorkerContext(): boolean {
1174
- return parseTeamWorkerEnv(safeString(process.env.OMX_TEAM_WORKER)) !== null;
1243
+ return parseTeamWorkerEnv(safeString(process.env.OMX_TEAM_INTERNAL_WORKER || process.env.OMX_TEAM_WORKER)) !== null;
1175
1244
  }
1176
1245
 
1177
1246
  function isStopExempt(payload: CodexHookPayload): boolean {
@@ -1366,6 +1435,36 @@ function matchesSkillStopContext(
1366
1435
  return true;
1367
1436
  }
1368
1437
 
1438
+ function modeStateMatchesSkillStopContext(
1439
+ state: Record<string, unknown>,
1440
+ cwd: string,
1441
+ sessionId: string,
1442
+ ): boolean {
1443
+ const stateSessionId = safeString(
1444
+ state.owner_omx_session_id
1445
+ ?? state.session_id
1446
+ ?? state.codex_session_id
1447
+ ?? state.owner_codex_session_id,
1448
+ ).trim();
1449
+ if (sessionId && stateSessionId && stateSessionId !== sessionId) return false;
1450
+
1451
+ const stateCwd = safeString(
1452
+ state.cwd
1453
+ ?? state.workingDirectory
1454
+ ?? state.working_directory
1455
+ ?? state.project_path,
1456
+ ).trim();
1457
+ if (stateCwd) {
1458
+ try {
1459
+ if (resolve(stateCwd) !== resolve(cwd)) return false;
1460
+ } catch {
1461
+ return false;
1462
+ }
1463
+ }
1464
+
1465
+ return true;
1466
+ }
1467
+
1369
1468
  async function readBlockingSkillForStop(
1370
1469
  cwd: string,
1371
1470
  sessionId: string,
@@ -1379,8 +1478,15 @@ async function readBlockingSkillForStop(
1379
1478
  : [...SKILL_STOP_BLOCKERS];
1380
1479
 
1381
1480
  for (const skill of candidateSkills) {
1481
+ const terminalRunState = await readCanonicalTerminalRunStateForStop(cwd, sessionId, skill);
1482
+ if (terminalRunState) continue;
1483
+
1382
1484
  const modeState = await readStopSessionPinnedState(`${skill}-state.json`, cwd, sessionId);
1383
1485
  if (!modeState || modeState.active !== true) continue;
1486
+ if (!modeStateMatchesSkillStopContext(modeState, cwd, sessionId)) continue;
1487
+
1488
+ const modeSnapshot = getRunContinuationSnapshot(modeState);
1489
+ if (modeSnapshot?.terminal === true) continue;
1384
1490
 
1385
1491
  const phase = formatPhase(
1386
1492
  modeState.current_phase,
@@ -1865,7 +1971,11 @@ async function buildStopHookOutput(
1865
1971
  const threadId = readPayloadThreadId(payload);
1866
1972
  const execFollowupOutput = await buildExecFollowupStopOutput(cwd, canonicalSessionId);
1867
1973
  if (execFollowupOutput) return execFollowupOutput;
1868
- const ralphState = await readActiveRalphState(stateDir, canonicalSessionId);
1974
+ const ralphState = await readActiveRalphState(stateDir, canonicalSessionId, {
1975
+ payloadSessionId: sessionId,
1976
+ threadId,
1977
+ tmuxPaneId: safeString(process.env.TMUX_PANE).trim(),
1978
+ });
1869
1979
  if (!ralphState) {
1870
1980
  const autoresearchState = await readActiveAutoresearchState(cwd, canonicalSessionId);
1871
1981
  if (autoresearchState) {