oh-my-codex 0.12.3 → 0.12.4

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 (176) hide show
  1. package/Cargo.lock +5 -5
  2. package/Cargo.toml +1 -1
  3. package/README.md +2 -0
  4. package/dist/cli/__tests__/index.test.js +73 -12
  5. package/dist/cli/__tests__/index.test.js.map +1 -1
  6. package/dist/cli/__tests__/launch-fallback.test.js +8 -27
  7. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  8. package/dist/cli/__tests__/mcp-parity.test.d.ts +2 -0
  9. package/dist/cli/__tests__/mcp-parity.test.d.ts.map +1 -0
  10. package/dist/cli/__tests__/mcp-parity.test.js +111 -0
  11. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -0
  12. package/dist/cli/__tests__/nested-help-routing.test.js +13 -0
  13. package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
  14. package/dist/cli/__tests__/package-bin-contract.test.js +6 -1
  15. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  16. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.d.ts +2 -0
  17. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.d.ts.map +1 -0
  18. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +189 -0
  19. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -0
  20. package/dist/cli/__tests__/setup-scope.test.js +48 -0
  21. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  22. package/dist/cli/__tests__/state.test.d.ts +2 -0
  23. package/dist/cli/__tests__/state.test.d.ts.map +1 -0
  24. package/dist/cli/__tests__/state.test.js +46 -0
  25. package/dist/cli/__tests__/state.test.js.map +1 -0
  26. package/dist/cli/__tests__/team.test.js +238 -2
  27. package/dist/cli/__tests__/team.test.js.map +1 -1
  28. package/dist/cli/__tests__/uninstall.test.js +37 -2
  29. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  30. package/dist/cli/index.d.ts +6 -13
  31. package/dist/cli/index.d.ts.map +1 -1
  32. package/dist/cli/index.js +47 -60
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/mcp-parity.d.ts +22 -0
  35. package/dist/cli/mcp-parity.d.ts.map +1 -0
  36. package/dist/cli/mcp-parity.js +227 -0
  37. package/dist/cli/mcp-parity.js.map +1 -0
  38. package/dist/cli/setup.d.ts.map +1 -1
  39. package/dist/cli/setup.js +5 -2
  40. package/dist/cli/setup.js.map +1 -1
  41. package/dist/cli/state.d.ts +8 -0
  42. package/dist/cli/state.d.ts.map +1 -0
  43. package/dist/cli/state.js +71 -0
  44. package/dist/cli/state.js.map +1 -0
  45. package/dist/cli/team.d.ts.map +1 -1
  46. package/dist/cli/team.js +6 -5
  47. package/dist/cli/team.js.map +1 -1
  48. package/dist/cli/uninstall.d.ts.map +1 -1
  49. package/dist/cli/uninstall.js +18 -4
  50. package/dist/cli/uninstall.js.map +1 -1
  51. package/dist/config/__tests__/codex-hooks.test.d.ts +2 -0
  52. package/dist/config/__tests__/codex-hooks.test.d.ts.map +1 -0
  53. package/dist/config/__tests__/codex-hooks.test.js +53 -0
  54. package/dist/config/__tests__/codex-hooks.test.js.map +1 -0
  55. package/dist/config/codex-hooks.d.ts +16 -7
  56. package/dist/config/codex-hooks.d.ts.map +1 -1
  57. package/dist/config/codex-hooks.js +134 -2
  58. package/dist/config/codex-hooks.js.map +1 -1
  59. package/dist/hooks/__tests__/keyword-detector.test.js +6 -0
  60. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  61. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  62. package/dist/hooks/keyword-detector.js +6 -0
  63. package/dist/hooks/keyword-detector.js.map +1 -1
  64. package/dist/hud/__tests__/reconcile.test.d.ts +2 -0
  65. package/dist/hud/__tests__/reconcile.test.d.ts.map +1 -0
  66. package/dist/hud/__tests__/reconcile.test.js +83 -0
  67. package/dist/hud/__tests__/reconcile.test.js.map +1 -0
  68. package/dist/hud/__tests__/render.test.js +43 -0
  69. package/dist/hud/__tests__/render.test.js.map +1 -1
  70. package/dist/hud/constants.d.ts +2 -1
  71. package/dist/hud/constants.d.ts.map +1 -1
  72. package/dist/hud/constants.js +2 -1
  73. package/dist/hud/constants.js.map +1 -1
  74. package/dist/hud/index.d.ts +4 -1
  75. package/dist/hud/index.d.ts.map +1 -1
  76. package/dist/hud/index.js +11 -5
  77. package/dist/hud/index.js.map +1 -1
  78. package/dist/hud/reconcile.d.ts +23 -0
  79. package/dist/hud/reconcile.d.ts.map +1 -0
  80. package/dist/hud/reconcile.js +71 -0
  81. package/dist/hud/reconcile.js.map +1 -0
  82. package/dist/hud/render.d.ts +6 -1
  83. package/dist/hud/render.d.ts.map +1 -1
  84. package/dist/hud/render.js +77 -3
  85. package/dist/hud/render.js.map +1 -1
  86. package/dist/hud/tmux.d.ts +26 -0
  87. package/dist/hud/tmux.d.ts.map +1 -0
  88. package/dist/hud/tmux.js +126 -0
  89. package/dist/hud/tmux.js.map +1 -0
  90. package/dist/mcp/bootstrap.d.ts.map +1 -1
  91. package/dist/mcp/bootstrap.js +16 -6
  92. package/dist/mcp/bootstrap.js.map +1 -1
  93. package/dist/mcp/code-intel-server.d.ts +298 -0
  94. package/dist/mcp/code-intel-server.d.ts.map +1 -1
  95. package/dist/mcp/code-intel-server.js +9 -5
  96. package/dist/mcp/code-intel-server.js.map +1 -1
  97. package/dist/mcp/memory-server.d.ts +195 -1
  98. package/dist/mcp/memory-server.d.ts.map +1 -1
  99. package/dist/mcp/memory-server.js +9 -5
  100. package/dist/mcp/memory-server.js.map +1 -1
  101. package/dist/mcp/trace-server.d.ts +51 -0
  102. package/dist/mcp/trace-server.d.ts.map +1 -1
  103. package/dist/mcp/trace-server.js +9 -5
  104. package/dist/mcp/trace-server.js.map +1 -1
  105. package/dist/scripts/__tests__/codex-native-hook.test.js +455 -8
  106. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  107. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  108. package/dist/scripts/codex-native-hook.js +159 -52
  109. package/dist/scripts/codex-native-hook.js.map +1 -1
  110. package/dist/scripts/codex-native-pre-post.d.ts +5 -0
  111. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  112. package/dist/scripts/codex-native-pre-post.js +86 -0
  113. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  114. package/dist/scripts/notify-hook/operational-events.d.ts.map +1 -1
  115. package/dist/scripts/notify-hook/operational-events.js +7 -2
  116. package/dist/scripts/notify-hook/operational-events.js.map +1 -1
  117. package/dist/state/__tests__/operations-ralph-phase.test.d.ts +2 -0
  118. package/dist/state/__tests__/operations-ralph-phase.test.d.ts.map +1 -0
  119. package/dist/state/__tests__/operations-ralph-phase.test.js +82 -0
  120. package/dist/state/__tests__/operations-ralph-phase.test.js.map +1 -0
  121. package/dist/state/__tests__/operations.test.d.ts +2 -0
  122. package/dist/state/__tests__/operations.test.d.ts.map +1 -0
  123. package/dist/state/__tests__/operations.test.js +200 -0
  124. package/dist/state/__tests__/operations.test.js.map +1 -0
  125. package/dist/state/__tests__/path-traversal.test.d.ts +2 -0
  126. package/dist/state/__tests__/path-traversal.test.d.ts.map +1 -0
  127. package/dist/state/__tests__/path-traversal.test.js +49 -0
  128. package/dist/state/__tests__/path-traversal.test.js.map +1 -0
  129. package/dist/state/operations.d.ts +11 -0
  130. package/dist/state/operations.d.ts.map +1 -0
  131. package/dist/state/operations.js +233 -0
  132. package/dist/state/operations.js.map +1 -0
  133. package/dist/team/__tests__/api-interop.test.js +24 -2
  134. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  135. package/dist/team/__tests__/delivery-e2e-smoke.test.js +9 -1
  136. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  137. package/dist/team/__tests__/runtime-cli.test.js +45 -0
  138. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  139. package/dist/team/__tests__/runtime.test.js +191 -66
  140. package/dist/team/__tests__/runtime.test.js.map +1 -1
  141. package/dist/team/__tests__/tmux-session.test.js +33 -0
  142. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  143. package/dist/team/api-interop.d.ts.map +1 -1
  144. package/dist/team/api-interop.js +2 -1
  145. package/dist/team/api-interop.js.map +1 -1
  146. package/dist/team/runtime-cli.d.ts.map +1 -1
  147. package/dist/team/runtime-cli.js +21 -2
  148. package/dist/team/runtime-cli.js.map +1 -1
  149. package/dist/team/runtime.d.ts +8 -0
  150. package/dist/team/runtime.d.ts.map +1 -1
  151. package/dist/team/runtime.js +179 -78
  152. package/dist/team/runtime.js.map +1 -1
  153. package/dist/team/state/dispatch.d.ts.map +1 -1
  154. package/dist/team/state/dispatch.js +9 -0
  155. package/dist/team/state/dispatch.js.map +1 -1
  156. package/dist/team/tmux-session.js +3 -3
  157. package/dist/team/tmux-session.js.map +1 -1
  158. package/dist/team/worktree.d.ts +2 -0
  159. package/dist/team/worktree.d.ts.map +1 -1
  160. package/dist/team/worktree.js +7 -1
  161. package/dist/team/worktree.js.map +1 -1
  162. package/dist/utils/__tests__/paths.test.js +76 -1
  163. package/dist/utils/__tests__/paths.test.js.map +1 -1
  164. package/dist/utils/paths.d.ts +6 -0
  165. package/dist/utils/paths.d.ts.map +1 -1
  166. package/dist/utils/paths.js +14 -0
  167. package/dist/utils/paths.js.map +1 -1
  168. package/dist/verification/__tests__/ci-rust-gates.test.js +59 -11
  169. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  170. package/dist/verification/__tests__/ralph-persistence-gate.test.js +1 -4
  171. package/dist/verification/__tests__/ralph-persistence-gate.test.js.map +1 -1
  172. package/package.json +6 -1
  173. package/src/scripts/__tests__/codex-native-hook.test.ts +600 -8
  174. package/src/scripts/codex-native-hook.ts +236 -60
  175. package/src/scripts/codex-native-pre-post.ts +104 -0
  176. package/src/scripts/notify-hook/operational-events.ts +6 -2
@@ -1,18 +1,41 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { execFileSync } from "node:child_process";
3
3
  import { existsSync } from "node:fs";
4
- import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
5
5
  import { tmpdir } from "node:os";
6
6
  import { dirname, join } from "node:path";
7
- import { describe, it } from "node:test";
7
+ import { afterEach, beforeEach, describe, it } from "node:test";
8
8
  import { buildManagedCodexHooksConfig } from "../../config/codex-hooks.js";
9
- import { initTeamState } from "../../team/state.js";
9
+ import { initTeamState, readTeamLeaderAttention, readTeamPhase, } from "../../team/state.js";
10
10
  import { dispatchCodexNativeHook, mapCodexHookEventToOmxEvent, resolveSessionOwnerPidFromAncestry, } from "../codex-native-hook.js";
11
11
  async function writeJson(path, value) {
12
12
  await mkdir(dirname(path), { recursive: true }).catch(() => { });
13
13
  await writeFile(path, JSON.stringify(value, null, 2));
14
14
  }
15
15
  const TEAM_STOP_COMMIT_GUIDANCE = " If system-generated worker auto-checkpoint commits exist, rewrite them into Lore-format final commits before merge/finalization.";
16
+ const TEAM_ENV_KEYS = [
17
+ "OMX_TEAM_WORKER",
18
+ "OMX_TEAM_STATE_ROOT",
19
+ "OMX_TEAM_LEADER_CWD",
20
+ ];
21
+ const priorTeamEnv = new Map();
22
+ beforeEach(() => {
23
+ priorTeamEnv.clear();
24
+ for (const key of TEAM_ENV_KEYS) {
25
+ priorTeamEnv.set(key, process.env[key]);
26
+ delete process.env[key];
27
+ }
28
+ });
29
+ afterEach(() => {
30
+ for (const key of TEAM_ENV_KEYS) {
31
+ const value = priorTeamEnv.get(key);
32
+ if (typeof value === "string")
33
+ process.env[key] = value;
34
+ else
35
+ delete process.env[key];
36
+ }
37
+ priorTeamEnv.clear();
38
+ });
16
39
  describe("codex native hook config", () => {
17
40
  it("builds the expected managed hooks.json shape", () => {
18
41
  const config = buildManagedCodexHooksConfig("/tmp/omx");
@@ -26,6 +49,10 @@ describe("codex native hook config", () => {
26
49
  const preToolUse = config.hooks.PreToolUse[0];
27
50
  assert.equal(preToolUse.matcher, "Bash");
28
51
  assert.match(String(preToolUse.hooks?.[0]?.command || ""), /codex-native-hook\.js"?$/);
52
+ const postToolUse = config.hooks.PostToolUse[0];
53
+ assert.equal(postToolUse.matcher, undefined);
54
+ assert.match(String(postToolUse.hooks?.[0]?.command || ""), /codex-native-hook\.js"?$/);
55
+ assert.equal(postToolUse.hooks?.[0]?.statusMessage, "Running OMX tool review");
29
56
  const stop = config.hooks.Stop[0];
30
57
  assert.equal(stop.hooks?.[0]?.timeout, 30);
31
58
  });
@@ -158,6 +185,27 @@ describe("codex native hook dispatch", () => {
158
185
  await rm(cwd, { recursive: true, force: true });
159
186
  }
160
187
  });
188
+ it("does not emit UserPromptSubmit routing context for unknown $tokens", async () => {
189
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-unknown-token-"));
190
+ try {
191
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
192
+ const result = await dispatchCodexNativeHook({
193
+ hook_event_name: "UserPromptSubmit",
194
+ cwd,
195
+ session_id: "sess-unknown-1",
196
+ thread_id: "thread-unknown-1",
197
+ turn_id: "turn-unknown-1",
198
+ prompt: "$maer-thinking 다시 설명해봐",
199
+ }, { cwd });
200
+ assert.equal(result.omxEventName, "keyword-detector");
201
+ assert.equal(result.skillState, null);
202
+ assert.equal(result.outputJson, null);
203
+ assert.equal(existsSync(join(cwd, ".omx", "state", "skill-active-state.json")), false);
204
+ }
205
+ finally {
206
+ await rm(cwd, { recursive: true, force: true });
207
+ }
208
+ });
161
209
  it("nudges $team prompt-submit routing toward omx team runtime usage", async () => {
162
210
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-team-"));
163
211
  try {
@@ -184,6 +232,71 @@ describe("codex native hook dispatch", () => {
184
232
  await rm(cwd, { recursive: true, force: true });
185
233
  }
186
234
  });
235
+ it("runs prompt-submit HUD reconciliation as a best-effort tmux-only side effect", async () => {
236
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-reconcile-"));
237
+ const originalTmux = process.env.TMUX;
238
+ const originalTmuxPane = process.env.TMUX_PANE;
239
+ const originalPath = process.env.PATH;
240
+ const originalArgv = process.argv;
241
+ try {
242
+ process.env.TMUX = "1";
243
+ process.env.TMUX_PANE = "%1";
244
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
245
+ await writeFile(join(cwd, ".omx", "hud-config.json"), JSON.stringify({ preset: "focused", git: { display: "branch" } }, null, 2));
246
+ const binDir = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-reconcile-bin-"));
247
+ const tmuxLog = join(cwd, "tmux.log");
248
+ await writeFile(join(binDir, "tmux"), `#!/usr/bin/env bash
249
+ set -euo pipefail
250
+ printf '%s\\n' "$*" >> ${JSON.stringify(tmuxLog)}
251
+ case "$1" in
252
+ list-panes)
253
+ printf '%%1\\tcodex\\tcodex\\n'
254
+ ;;
255
+ display-message)
256
+ printf '80\\t24\\n'
257
+ ;;
258
+ split-window)
259
+ printf '%%9\\n'
260
+ ;;
261
+ resize-pane)
262
+ ;;
263
+ esac
264
+ `);
265
+ await chmod(join(binDir, "tmux"), 0o755);
266
+ process.env.PATH = `${binDir}:${originalPath}`;
267
+ process.argv = [originalArgv[0] || 'node', '/tmp/codex-host-binary'];
268
+ const result = await dispatchCodexNativeHook({
269
+ hook_event_name: "UserPromptSubmit",
270
+ cwd,
271
+ session_id: "sess-hud-1",
272
+ prompt: "$ralplan prepare plan",
273
+ }, { cwd });
274
+ assert.equal(result.omxEventName, "keyword-detector");
275
+ const tmuxCalls = await readFile(tmuxLog, "utf-8");
276
+ assert.match(tmuxCalls, /list-panes/);
277
+ assert.match(tmuxCalls, /split-window/);
278
+ assert.match(tmuxCalls, /resize-pane -t %9 -y 3/);
279
+ assert.match(tmuxCalls, /dist\/cli\/omx\.js' hud --watch --preset=focused/);
280
+ assert.doesNotMatch(tmuxCalls, /\/tmp\/codex-host-binary' hud --watch/);
281
+ }
282
+ finally {
283
+ if (originalTmux === undefined) {
284
+ delete process.env.TMUX;
285
+ }
286
+ else {
287
+ process.env.TMUX = originalTmux;
288
+ }
289
+ if (originalTmuxPane === undefined) {
290
+ delete process.env.TMUX_PANE;
291
+ }
292
+ else {
293
+ process.env.TMUX_PANE = originalTmuxPane;
294
+ }
295
+ process.env.PATH = originalPath;
296
+ process.argv = originalArgv;
297
+ await rm(cwd, { recursive: true, force: true });
298
+ }
299
+ });
187
300
  it("returns a destructive-command caution on PreToolUse for rm -rf dist", async () => {
188
301
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-danger-"));
189
302
  try {
@@ -248,6 +361,81 @@ describe("codex native hook dispatch", () => {
248
361
  await rm(cwd, { recursive: true, force: true });
249
362
  }
250
363
  });
364
+ it("returns PostToolUse MCP transport fallback guidance for clear MCP transport death", async () => {
365
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-transport-"));
366
+ try {
367
+ const result = await dispatchCodexNativeHook({
368
+ hook_event_name: "PostToolUse",
369
+ cwd,
370
+ tool_name: "mcp__omx_state__state_write",
371
+ tool_use_id: "tool-mcp-transport",
372
+ tool_input: { mode: "team", active: true },
373
+ tool_response: "{\"error\":\"MCP transport closed\",\"details\":\"stdio pipe closed before response\"}",
374
+ }, { cwd });
375
+ assert.equal(result.omxEventName, "post-tool-use");
376
+ const output = result.outputJson;
377
+ assert.equal(output?.decision, "block");
378
+ assert.equal(output?.reason, "The MCP tool appears to have lost its transport/server connection. Preserve state, debug the transport failure, and use OMX CLI/file-backed fallbacks instead of retrying blindly.");
379
+ const additionalContext = String(output?.hookSpecificOutput?.additionalContext ?? "");
380
+ assert.match(additionalContext, /omx state state_write --input/);
381
+ assert.match(additionalContext, /plain Node stdio processes/i);
382
+ assert.match(additionalContext, /read-stall-state/);
383
+ assert.match(additionalContext, /OMX_MCP_TRANSPORT_DEBUG=1/);
384
+ }
385
+ finally {
386
+ await rm(cwd, { recursive: true, force: true });
387
+ }
388
+ });
389
+ it("does not classify non-transport MCP failures as transport death", async () => {
390
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-nontransport-"));
391
+ try {
392
+ const result = await dispatchCodexNativeHook({
393
+ hook_event_name: "PostToolUse",
394
+ cwd,
395
+ tool_name: "mcp__omx_state__state_write",
396
+ tool_use_id: "tool-mcp-nontransport",
397
+ tool_input: { active: true },
398
+ tool_response: "{\"error\":\"validation failed\",\"details\":\"mode is required\"}",
399
+ }, { cwd });
400
+ assert.equal(result.omxEventName, "post-tool-use");
401
+ assert.equal(result.outputJson, null);
402
+ }
403
+ finally {
404
+ await rm(cwd, { recursive: true, force: true });
405
+ }
406
+ });
407
+ it("marks active team state failed on MCP transport death without deleting team state", async () => {
408
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-team-mcp-transport-"));
409
+ const previousCwd = process.cwd();
410
+ try {
411
+ process.chdir(cwd);
412
+ await initTeamState("transport-team", "task", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-transport" });
413
+ await writeJson(join(cwd, ".omx", "state", "team-state.json"), {
414
+ active: true,
415
+ team_name: "transport-team",
416
+ current_phase: "team-exec",
417
+ });
418
+ await dispatchCodexNativeHook({
419
+ hook_event_name: "PostToolUse",
420
+ cwd,
421
+ session_id: "sess-transport",
422
+ tool_name: "mcp__omx_state__state_write",
423
+ tool_use_id: "tool-mcp-transport-team",
424
+ tool_input: { mode: "team", active: true },
425
+ tool_response: "{\"error\":\"MCP transport closed\",\"details\":\"stdio pipe closed before response\"}",
426
+ }, { cwd });
427
+ const phase = await readTeamPhase("transport-team", cwd);
428
+ const attention = await readTeamLeaderAttention("transport-team", cwd);
429
+ assert.equal(phase?.current_phase, "failed");
430
+ assert.equal(attention?.leader_attention_reason, "mcp_transport_dead");
431
+ assert.equal(attention?.leader_attention_pending, true);
432
+ assert.equal(existsSync(join(cwd, ".omx", "state", "team", "transport-team")), true);
433
+ }
434
+ finally {
435
+ process.chdir(previousCwd);
436
+ await rm(cwd, { recursive: true, force: true });
437
+ }
438
+ });
251
439
  it("treats stderr-only informative non-zero output as reviewable instead of a generic failure", async () => {
252
440
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-informative-stderr-"));
253
441
  try {
@@ -298,6 +486,39 @@ describe("codex native hook dispatch", () => {
298
486
  await rm(cwd, { recursive: true, force: true });
299
487
  }
300
488
  });
489
+ it("returns MCP transport-death guidance and preserves failed team state", async () => {
490
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-dead-"));
491
+ try {
492
+ await initTeamState("mcp-transport-dead-team", "transport failure fallback", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-mcp-dead" });
493
+ const result = await dispatchCodexNativeHook({
494
+ hook_event_name: "PostToolUse",
495
+ cwd,
496
+ session_id: "sess-mcp-dead",
497
+ tool_name: "mcp__omx_state__state_write",
498
+ tool_use_id: "tool-mcp-dead",
499
+ tool_response: JSON.stringify({
500
+ error: "transport closed",
501
+ message: "MCP server disconnected",
502
+ }),
503
+ }, { cwd });
504
+ assert.equal(result.omxEventName, "post-tool-use");
505
+ assert.equal(result.outputJson?.decision, "block");
506
+ assert.match(String(result.outputJson?.reason || ""), /lost its transport\/server connection/);
507
+ const hookSpecificOutput = result.outputJson?.hookSpecificOutput;
508
+ assert.equal(hookSpecificOutput?.hookEventName, "PostToolUse");
509
+ assert.match(String(hookSpecificOutput?.additionalContext || ""), /Retry via CLI parity with `omx state state_write --input '\{\}' --json`\./);
510
+ assert.match(String(hookSpecificOutput?.additionalContext || ""), /omx team api read-stall-state/);
511
+ const phase = JSON.parse(await readFile(join(cwd, ".omx", "state", "team", "mcp-transport-dead-team", "phase.json"), "utf-8"));
512
+ assert.equal(phase.current_phase, "failed");
513
+ assert.equal(phase.transitions?.at(-1)?.reason, "mcp_transport_dead");
514
+ const attention = JSON.parse(await readFile(join(cwd, ".omx", "state", "team", "mcp-transport-dead-team", "leader-attention.json"), "utf-8"));
515
+ assert.equal(attention.leader_attention_reason, "mcp_transport_dead");
516
+ assert.ok(attention.attention_reasons?.includes("mcp_transport_dead"));
517
+ }
518
+ finally {
519
+ await rm(cwd, { recursive: true, force: true });
520
+ }
521
+ });
301
522
  it("stays silent on neutral successful PostToolUse output", async () => {
302
523
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-neutral-"));
303
524
  try {
@@ -316,6 +537,45 @@ describe("codex native hook dispatch", () => {
316
537
  await rm(cwd, { recursive: true, force: true });
317
538
  }
318
539
  });
540
+ it("returns CLI fallback guidance and preserves failed team state on clear MCP transport death", async () => {
541
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-transport-"));
542
+ try {
543
+ await initTeamState("transport-team", "transport failure fallback", "executor", 1, cwd, undefined, { ...process.env, OMX_SESSION_ID: "sess-stop-mcp-transport" });
544
+ await writeJson(join(cwd, ".omx", "state", "team-state.json"), {
545
+ active: true,
546
+ team_name: "transport-team",
547
+ current_phase: "team-exec",
548
+ });
549
+ const result = await dispatchCodexNativeHook({
550
+ hook_event_name: "PostToolUse",
551
+ cwd,
552
+ session_id: "sess-stop-mcp-transport",
553
+ tool_name: "mcp__omx_state__state_write",
554
+ tool_use_id: "tool-mcp-fail",
555
+ tool_input: { mode: "team", active: true },
556
+ tool_response: JSON.stringify({
557
+ error: "MCP transport closed unexpectedly",
558
+ exit_code: 1,
559
+ }),
560
+ }, { cwd });
561
+ assert.equal(result.omxEventName, "post-tool-use");
562
+ assert.deepEqual(result.outputJson, {
563
+ decision: "block",
564
+ reason: "The MCP tool appears to have lost its transport/server connection. Preserve state, debug the transport failure, and use OMX CLI/file-backed fallbacks instead of retrying blindly.",
565
+ hookSpecificOutput: {
566
+ hookEventName: "PostToolUse",
567
+ additionalContext: "Clear MCP transport-death signal detected. Preserve current team/runtime state. Retry via CLI parity with `omx state state_write --input '{\"mode\":\"team\",\"active\":true}' --json`. OMX MCP servers are plain Node stdio processes, so they still shut down when stdin/transport closes. If this happened during team runtime, inspect first with `omx team status <team>` or `omx team api read-stall-state --input '{\"team_name\":\"<team>\"}' --json`, and only force cleanup after capturing needed state. For root-cause debugging, rerun with `OMX_MCP_TRANSPORT_DEBUG=1` to log why the stdio transport closed.",
568
+ },
569
+ });
570
+ const phase = await readTeamPhase("transport-team", cwd);
571
+ const attention = await readTeamLeaderAttention("transport-team", cwd);
572
+ assert.equal(phase?.current_phase, "failed");
573
+ assert.equal(attention?.leader_attention_reason, "mcp_transport_dead");
574
+ }
575
+ finally {
576
+ await rm(cwd, { recursive: true, force: true });
577
+ }
578
+ });
319
579
  it("returns Stop continuation output while Autopilot is active", async () => {
320
580
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-"));
321
581
  try {
@@ -676,7 +936,7 @@ describe("codex native hook dispatch", () => {
676
936
  await rm(cwd, { recursive: true, force: true });
677
937
  }
678
938
  });
679
- it("returns Stop continuation output for active ralplan skill without active subagents", async () => {
939
+ it("returns Stop continuation output for active ralplan skill with matching active mode state and without active subagents", async () => {
680
940
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-"));
681
941
  try {
682
942
  const stateDir = join(cwd, ".omx", "state");
@@ -687,6 +947,10 @@ describe("codex native hook dispatch", () => {
687
947
  skill: "ralplan",
688
948
  phase: "planning",
689
949
  });
950
+ await writeJson(join(stateDir, "sessions", "sess-stop-skill", "ralplan-state.json"), {
951
+ active: true,
952
+ current_phase: "planning",
953
+ });
690
954
  const result = await dispatchCodexNativeHook({
691
955
  hook_event_name: "Stop",
692
956
  cwd,
@@ -704,6 +968,36 @@ describe("codex native hook dispatch", () => {
704
968
  await rm(cwd, { recursive: true, force: true });
705
969
  }
706
970
  });
971
+ it("does not block on stale ralplan skill-active state when the matching mode state is absent", async () => {
972
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-skill-"));
973
+ try {
974
+ const stateDir = join(cwd, ".omx", "state");
975
+ await mkdir(join(stateDir, "sessions", "sess-stop-stale-skill"), { recursive: true });
976
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-stale-skill" });
977
+ await writeJson(join(stateDir, "sessions", "sess-stop-stale-skill", "skill-active-state.json"), {
978
+ active: true,
979
+ skill: "ralplan",
980
+ phase: "planning",
981
+ session_id: "sess-stop-stale-skill",
982
+ active_skills: [{
983
+ skill: "ralplan",
984
+ phase: "planning",
985
+ active: true,
986
+ session_id: "sess-stop-stale-skill",
987
+ }],
988
+ });
989
+ const result = await dispatchCodexNativeHook({
990
+ hook_event_name: "Stop",
991
+ cwd,
992
+ session_id: "sess-stop-stale-skill",
993
+ }, { cwd });
994
+ assert.equal(result.omxEventName, "stop");
995
+ assert.equal(result.outputJson, null);
996
+ }
997
+ finally {
998
+ await rm(cwd, { recursive: true, force: true });
999
+ }
1000
+ });
707
1001
  it("does not block on active ralplan skill when subagents are still active", async () => {
708
1002
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-subagent-"));
709
1003
  try {
@@ -715,6 +1009,10 @@ describe("codex native hook dispatch", () => {
715
1009
  skill: "ralplan",
716
1010
  phase: "planning",
717
1011
  });
1012
+ await writeJson(join(stateDir, "sessions", "sess-stop-skill-subagent", "ralplan-state.json"), {
1013
+ active: true,
1014
+ current_phase: "planning",
1015
+ });
718
1016
  await writeJson(join(stateDir, "subagent-tracking.json"), {
719
1017
  schemaVersion: 1,
720
1018
  sessions: {
@@ -753,7 +1051,30 @@ describe("codex native hook dispatch", () => {
753
1051
  await rm(cwd, { recursive: true, force: true });
754
1052
  }
755
1053
  });
756
- it("returns Stop continuation output for active deep-interview skill without active subagents", async () => {
1054
+ it("does not block on stale root ralplan skill when the explicit session-scoped canonical skill state is absent", async () => {
1055
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-skill-"));
1056
+ try {
1057
+ const stateDir = join(cwd, ".omx", "state");
1058
+ await mkdir(stateDir, { recursive: true });
1059
+ await writeJson(join(stateDir, "skill-active-state.json"), {
1060
+ active: true,
1061
+ skill: "ralplan",
1062
+ phase: "planning",
1063
+ });
1064
+ const result = await dispatchCodexNativeHook({
1065
+ hook_event_name: "Stop",
1066
+ cwd,
1067
+ session_id: "sess-stop-stale-root-skill",
1068
+ thread_id: "thread-stop-stale-root-skill",
1069
+ }, { cwd });
1070
+ assert.equal(result.omxEventName, "stop");
1071
+ assert.equal(result.outputJson, null);
1072
+ }
1073
+ finally {
1074
+ await rm(cwd, { recursive: true, force: true });
1075
+ }
1076
+ });
1077
+ it("returns Stop continuation output for active deep-interview skill with matching active mode state and without active subagents", async () => {
757
1078
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-"));
758
1079
  try {
759
1080
  const stateDir = join(cwd, ".omx", "state");
@@ -764,6 +1085,10 @@ describe("codex native hook dispatch", () => {
764
1085
  skill: "deep-interview",
765
1086
  phase: "planning",
766
1087
  });
1088
+ await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview", "deep-interview-state.json"), {
1089
+ active: true,
1090
+ current_phase: "planning",
1091
+ });
767
1092
  const result = await dispatchCodexNativeHook({
768
1093
  hook_event_name: "Stop",
769
1094
  cwd,
@@ -830,6 +1155,30 @@ describe("codex native hook dispatch", () => {
830
1155
  await rm(cwd, { recursive: true, force: true });
831
1156
  }
832
1157
  });
1158
+ it("does not block Stop from stale session-scoped Ralph state that belongs to another session", async () => {
1159
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-session-ralph-"));
1160
+ try {
1161
+ const stateDir = join(cwd, ".omx", "state");
1162
+ await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
1163
+ await mkdir(join(stateDir, "sessions", "sess-stale"), { recursive: true });
1164
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
1165
+ await writeJson(join(stateDir, "sessions", "sess-stale", "ralph-state.json"), {
1166
+ active: true,
1167
+ current_phase: "starting",
1168
+ session_id: "sess-stale",
1169
+ });
1170
+ const result = await dispatchCodexNativeHook({
1171
+ hook_event_name: "Stop",
1172
+ cwd,
1173
+ session_id: "sess-current",
1174
+ }, { cwd });
1175
+ assert.equal(result.omxEventName, "stop");
1176
+ assert.equal(result.outputJson, null);
1177
+ }
1178
+ finally {
1179
+ await rm(cwd, { recursive: true, force: true });
1180
+ }
1181
+ });
833
1182
  it("does not re-block Ralph when Stop already continued once", async () => {
834
1183
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-once-"));
835
1184
  try {
@@ -956,6 +1305,10 @@ describe("codex native hook dispatch", () => {
956
1305
  message: "Deep interview is active; auto-approval shortcuts are blocked until the interview finishes.",
957
1306
  },
958
1307
  });
1308
+ await writeJson(join(stateDir, "sessions", "sess-stop-auto-lock", "deep-interview-state.json"), {
1309
+ active: true,
1310
+ current_phase: "planning",
1311
+ });
959
1312
  const result = await dispatchCodexNativeHook({
960
1313
  hook_event_name: "Stop",
961
1314
  cwd,
@@ -1003,7 +1356,7 @@ describe("codex native hook dispatch", () => {
1003
1356
  await rm(cwd, { recursive: true, force: true });
1004
1357
  }
1005
1358
  });
1006
- it("suppresses native auto-nudge when deep-interview mode state is active without a scoped skill state", async () => {
1359
+ it("suppresses native auto-nudge when root deep-interview mode state is active without an explicit session", async () => {
1007
1360
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-deep-interview-mode-"));
1008
1361
  try {
1009
1362
  const stateDir = join(cwd, ".omx", "state");
@@ -1016,8 +1369,6 @@ describe("codex native hook dispatch", () => {
1016
1369
  const result = await dispatchCodexNativeHook({
1017
1370
  hook_event_name: "Stop",
1018
1371
  cwd,
1019
- session_id: "sess-stop-auto-mode",
1020
- thread_id: "thread-stop-auto-mode",
1021
1372
  turn_id: "turn-stop-auto-mode-1",
1022
1373
  last_assistant_message: "Would you like me to continue with the next step?",
1023
1374
  }, { cwd });
@@ -1028,6 +1379,102 @@ describe("codex native hook dispatch", () => {
1028
1379
  await rm(cwd, { recursive: true, force: true });
1029
1380
  }
1030
1381
  });
1382
+ it("does not suppress native auto-nudge from stale root deep-interview mode state when the explicit session-scoped mode state is absent", async () => {
1383
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-stale-root-mode-"));
1384
+ try {
1385
+ const stateDir = join(cwd, ".omx", "state");
1386
+ await mkdir(stateDir, { recursive: true });
1387
+ await writeJson(join(stateDir, "deep-interview-state.json"), {
1388
+ active: true,
1389
+ mode: "deep-interview",
1390
+ current_phase: "intent-first",
1391
+ });
1392
+ const result = await dispatchCodexNativeHook({
1393
+ hook_event_name: "Stop",
1394
+ cwd,
1395
+ session_id: "sess-stop-auto-stale-root-mode",
1396
+ thread_id: "thread-stop-auto-stale-root-mode",
1397
+ turn_id: "turn-stop-auto-stale-root-mode-1",
1398
+ last_assistant_message: "Would you like me to continue with the next step?",
1399
+ }, { cwd });
1400
+ assert.equal(result.omxEventName, "stop");
1401
+ assert.deepEqual(result.outputJson, {
1402
+ decision: "block",
1403
+ reason: "yes, proceed",
1404
+ stopReason: "auto_nudge",
1405
+ systemMessage: "OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
1406
+ });
1407
+ }
1408
+ finally {
1409
+ await rm(cwd, { recursive: true, force: true });
1410
+ }
1411
+ });
1412
+ it("does not suppress native auto-nudge from stale root deep-interview skill state when the explicit session-scoped canonical skill state is absent", async () => {
1413
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-stale-root-skill-"));
1414
+ try {
1415
+ const stateDir = join(cwd, ".omx", "state");
1416
+ await mkdir(stateDir, { recursive: true });
1417
+ await writeJson(join(stateDir, "skill-active-state.json"), {
1418
+ active: true,
1419
+ skill: "deep-interview",
1420
+ phase: "planning",
1421
+ });
1422
+ const result = await dispatchCodexNativeHook({
1423
+ hook_event_name: "Stop",
1424
+ cwd,
1425
+ session_id: "sess-stop-auto-stale-root-skill",
1426
+ thread_id: "thread-stop-auto-stale-root-skill",
1427
+ turn_id: "turn-stop-auto-stale-root-skill-1",
1428
+ last_assistant_message: "Would you like me to continue with the next step?",
1429
+ }, { cwd });
1430
+ assert.equal(result.omxEventName, "stop");
1431
+ assert.deepEqual(result.outputJson, {
1432
+ decision: "block",
1433
+ reason: "yes, proceed",
1434
+ stopReason: "auto_nudge",
1435
+ systemMessage: "OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
1436
+ });
1437
+ }
1438
+ finally {
1439
+ await rm(cwd, { recursive: true, force: true });
1440
+ }
1441
+ });
1442
+ it("does not suppress native auto-nudge from stale root deep-interview input lock when the explicit session-scoped canonical skill state is absent", async () => {
1443
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-stale-root-lock-"));
1444
+ try {
1445
+ const stateDir = join(cwd, ".omx", "state");
1446
+ await mkdir(stateDir, { recursive: true });
1447
+ await writeJson(join(stateDir, "skill-active-state.json"), {
1448
+ active: true,
1449
+ skill: "deep-interview",
1450
+ phase: "planning",
1451
+ input_lock: {
1452
+ active: true,
1453
+ scope: "deep-interview-auto-approval",
1454
+ blocked_inputs: ["yes", "proceed"],
1455
+ message: "Deep interview is active; auto-approval shortcuts are blocked until the interview finishes.",
1456
+ },
1457
+ });
1458
+ const result = await dispatchCodexNativeHook({
1459
+ hook_event_name: "Stop",
1460
+ cwd,
1461
+ session_id: "sess-stop-auto-stale-root-lock",
1462
+ thread_id: "thread-stop-auto-stale-root-lock",
1463
+ turn_id: "turn-stop-auto-stale-root-lock-1",
1464
+ last_assistant_message: "Would you like me to continue with the next step?",
1465
+ }, { cwd });
1466
+ assert.equal(result.omxEventName, "stop");
1467
+ assert.deepEqual(result.outputJson, {
1468
+ decision: "block",
1469
+ reason: "yes, proceed",
1470
+ stopReason: "auto_nudge",
1471
+ systemMessage: "OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
1472
+ });
1473
+ }
1474
+ finally {
1475
+ await rm(cwd, { recursive: true, force: true });
1476
+ }
1477
+ });
1031
1478
  it("re-fires team Stop output for a later fresh Stop reply while the team is still active", async () => {
1032
1479
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-refire-"));
1033
1480
  try {