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,12 +1,16 @@
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 {
10
+ initTeamState,
11
+ readTeamLeaderAttention,
12
+ readTeamPhase,
13
+ } from "../../team/state.js";
10
14
  import {
11
15
  dispatchCodexNativeHook,
12
16
  mapCodexHookEventToOmxEvent,
@@ -21,6 +25,31 @@ async function writeJson(path: string, value: unknown): Promise<void> {
21
25
  const TEAM_STOP_COMMIT_GUIDANCE =
22
26
  " If system-generated worker auto-checkpoint commits exist, rewrite them into Lore-format final commits before merge/finalization.";
23
27
 
28
+ const TEAM_ENV_KEYS = [
29
+ "OMX_TEAM_WORKER",
30
+ "OMX_TEAM_STATE_ROOT",
31
+ "OMX_TEAM_LEADER_CWD",
32
+ ] as const;
33
+
34
+ const priorTeamEnv = new Map<(typeof TEAM_ENV_KEYS)[number], string | undefined>();
35
+
36
+ beforeEach(() => {
37
+ priorTeamEnv.clear();
38
+ for (const key of TEAM_ENV_KEYS) {
39
+ priorTeamEnv.set(key, process.env[key]);
40
+ delete process.env[key];
41
+ }
42
+ });
43
+
44
+ afterEach(() => {
45
+ for (const key of TEAM_ENV_KEYS) {
46
+ const value = priorTeamEnv.get(key);
47
+ if (typeof value === "string") process.env[key] = value;
48
+ else delete process.env[key];
49
+ }
50
+ priorTeamEnv.clear();
51
+ });
52
+
24
53
  describe("codex native hook config", () => {
25
54
  it("builds the expected managed hooks.json shape", () => {
26
55
  const config = buildManagedCodexHooksConfig("/tmp/omx");
@@ -42,6 +71,17 @@ describe("codex native hook config", () => {
42
71
  /codex-native-hook\.js"?$/,
43
72
  );
44
73
 
74
+ const postToolUse = config.hooks.PostToolUse[0] as {
75
+ matcher?: string;
76
+ hooks?: Array<Record<string, unknown>>;
77
+ };
78
+ assert.equal(postToolUse.matcher, undefined);
79
+ assert.match(
80
+ String(postToolUse.hooks?.[0]?.command || ""),
81
+ /codex-native-hook\.js"?$/,
82
+ );
83
+ assert.equal(postToolUse.hooks?.[0]?.statusMessage, "Running OMX tool review");
84
+
45
85
  const stop = config.hooks.Stop[0] as {
46
86
  hooks?: Array<Record<string, unknown>>;
47
87
  };
@@ -210,6 +250,31 @@ describe("codex native hook dispatch", () => {
210
250
  }
211
251
  });
212
252
 
253
+ it("does not emit UserPromptSubmit routing context for unknown $tokens", async () => {
254
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-unknown-token-"));
255
+ try {
256
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
257
+ const result = await dispatchCodexNativeHook(
258
+ {
259
+ hook_event_name: "UserPromptSubmit",
260
+ cwd,
261
+ session_id: "sess-unknown-1",
262
+ thread_id: "thread-unknown-1",
263
+ turn_id: "turn-unknown-1",
264
+ prompt: "$maer-thinking 다시 설명해봐",
265
+ },
266
+ { cwd },
267
+ );
268
+
269
+ assert.equal(result.omxEventName, "keyword-detector");
270
+ assert.equal(result.skillState, null);
271
+ assert.equal(result.outputJson, null);
272
+ assert.equal(existsSync(join(cwd, ".omx", "state", "skill-active-state.json")), false);
273
+ } finally {
274
+ await rm(cwd, { recursive: true, force: true });
275
+ }
276
+ });
277
+
213
278
  it("nudges $team prompt-submit routing toward omx team runtime usage", async () => {
214
279
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-team-"));
215
280
  try {
@@ -246,6 +311,81 @@ describe("codex native hook dispatch", () => {
246
311
  }
247
312
  });
248
313
 
314
+ it("runs prompt-submit HUD reconciliation as a best-effort tmux-only side effect", async () => {
315
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-reconcile-"));
316
+ const originalTmux = process.env.TMUX;
317
+ const originalTmuxPane = process.env.TMUX_PANE;
318
+ const originalPath = process.env.PATH;
319
+ const originalArgv = process.argv;
320
+ try {
321
+ process.env.TMUX = "1";
322
+ process.env.TMUX_PANE = "%1";
323
+ await mkdir(join(cwd, ".omx", "state"), { recursive: true });
324
+ await writeFile(
325
+ join(cwd, ".omx", "hud-config.json"),
326
+ JSON.stringify({ preset: "focused", git: { display: "branch" } }, null, 2),
327
+ );
328
+
329
+ const binDir = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-reconcile-bin-"));
330
+ const tmuxLog = join(cwd, "tmux.log");
331
+ await writeFile(
332
+ join(binDir, "tmux"),
333
+ `#!/usr/bin/env bash
334
+ set -euo pipefail
335
+ printf '%s\\n' "$*" >> ${JSON.stringify(tmuxLog)}
336
+ case "$1" in
337
+ list-panes)
338
+ printf '%%1\\tcodex\\tcodex\\n'
339
+ ;;
340
+ display-message)
341
+ printf '80\\t24\\n'
342
+ ;;
343
+ split-window)
344
+ printf '%%9\\n'
345
+ ;;
346
+ resize-pane)
347
+ ;;
348
+ esac
349
+ `,
350
+ );
351
+ await chmod(join(binDir, "tmux"), 0o755);
352
+ process.env.PATH = `${binDir}:${originalPath}`;
353
+ process.argv = [originalArgv[0] || 'node', '/tmp/codex-host-binary'];
354
+
355
+ const result = await dispatchCodexNativeHook(
356
+ {
357
+ hook_event_name: "UserPromptSubmit",
358
+ cwd,
359
+ session_id: "sess-hud-1",
360
+ prompt: "$ralplan prepare plan",
361
+ },
362
+ { cwd },
363
+ );
364
+
365
+ assert.equal(result.omxEventName, "keyword-detector");
366
+ const tmuxCalls = await readFile(tmuxLog, "utf-8");
367
+ assert.match(tmuxCalls, /list-panes/);
368
+ assert.match(tmuxCalls, /split-window/);
369
+ assert.match(tmuxCalls, /resize-pane -t %9 -y 3/);
370
+ assert.match(tmuxCalls, /dist\/cli\/omx\.js' hud --watch --preset=focused/);
371
+ assert.doesNotMatch(tmuxCalls, /\/tmp\/codex-host-binary' hud --watch/);
372
+ } finally {
373
+ if (originalTmux === undefined) {
374
+ delete process.env.TMUX;
375
+ } else {
376
+ process.env.TMUX = originalTmux;
377
+ }
378
+ if (originalTmuxPane === undefined) {
379
+ delete process.env.TMUX_PANE;
380
+ } else {
381
+ process.env.TMUX_PANE = originalTmuxPane;
382
+ }
383
+ process.env.PATH = originalPath;
384
+ process.argv = originalArgv;
385
+ await rm(cwd, { recursive: true, force: true });
386
+ }
387
+ });
388
+
249
389
  it("returns a destructive-command caution on PreToolUse for rm -rf dist", async () => {
250
390
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-danger-"));
251
391
  try {
@@ -324,6 +464,123 @@ describe("codex native hook dispatch", () => {
324
464
  }
325
465
  });
326
466
 
467
+ it("returns PostToolUse MCP transport fallback guidance for clear MCP transport death", async () => {
468
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-transport-"));
469
+ try {
470
+ const result = await dispatchCodexNativeHook(
471
+ {
472
+ hook_event_name: "PostToolUse",
473
+ cwd,
474
+ tool_name: "mcp__omx_state__state_write",
475
+ tool_use_id: "tool-mcp-transport",
476
+ tool_input: { mode: "team", active: true },
477
+ tool_response: "{\"error\":\"MCP transport closed\",\"details\":\"stdio pipe closed before response\"}",
478
+ },
479
+ { cwd },
480
+ );
481
+
482
+ assert.equal(result.omxEventName, "post-tool-use");
483
+ const output = result.outputJson as {
484
+ decision?: string;
485
+ reason?: string;
486
+ hookSpecificOutput?: { additionalContext?: string };
487
+ } | null;
488
+ assert.equal(output?.decision, "block");
489
+ assert.equal(
490
+ output?.reason,
491
+ "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.",
492
+ );
493
+ const additionalContext = String(
494
+ output?.hookSpecificOutput?.additionalContext ?? "",
495
+ );
496
+ assert.match(
497
+ additionalContext,
498
+ /omx state state_write --input/,
499
+ );
500
+ assert.match(
501
+ additionalContext,
502
+ /plain Node stdio processes/i,
503
+ );
504
+ assert.match(
505
+ additionalContext,
506
+ /read-stall-state/,
507
+ );
508
+ assert.match(
509
+ additionalContext,
510
+ /OMX_MCP_TRANSPORT_DEBUG=1/,
511
+ );
512
+ } finally {
513
+ await rm(cwd, { recursive: true, force: true });
514
+ }
515
+ });
516
+
517
+ it("does not classify non-transport MCP failures as transport death", async () => {
518
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-nontransport-"));
519
+ try {
520
+ const result = await dispatchCodexNativeHook(
521
+ {
522
+ hook_event_name: "PostToolUse",
523
+ cwd,
524
+ tool_name: "mcp__omx_state__state_write",
525
+ tool_use_id: "tool-mcp-nontransport",
526
+ tool_input: { active: true },
527
+ tool_response: "{\"error\":\"validation failed\",\"details\":\"mode is required\"}",
528
+ },
529
+ { cwd },
530
+ );
531
+
532
+ assert.equal(result.omxEventName, "post-tool-use");
533
+ assert.equal(result.outputJson, null);
534
+ } finally {
535
+ await rm(cwd, { recursive: true, force: true });
536
+ }
537
+ });
538
+
539
+ it("marks active team state failed on MCP transport death without deleting team state", async () => {
540
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-team-mcp-transport-"));
541
+ const previousCwd = process.cwd();
542
+ try {
543
+ process.chdir(cwd);
544
+ await initTeamState(
545
+ "transport-team",
546
+ "task",
547
+ "executor",
548
+ 1,
549
+ cwd,
550
+ undefined,
551
+ { ...process.env, OMX_SESSION_ID: "sess-transport" },
552
+ );
553
+ await writeJson(join(cwd, ".omx", "state", "team-state.json"), {
554
+ active: true,
555
+ team_name: "transport-team",
556
+ current_phase: "team-exec",
557
+ });
558
+
559
+ await dispatchCodexNativeHook(
560
+ {
561
+ hook_event_name: "PostToolUse",
562
+ cwd,
563
+ session_id: "sess-transport",
564
+ tool_name: "mcp__omx_state__state_write",
565
+ tool_use_id: "tool-mcp-transport-team",
566
+ tool_input: { mode: "team", active: true },
567
+ tool_response: "{\"error\":\"MCP transport closed\",\"details\":\"stdio pipe closed before response\"}",
568
+ },
569
+ { cwd },
570
+ );
571
+
572
+ const phase = await readTeamPhase("transport-team", cwd);
573
+ const attention = await readTeamLeaderAttention("transport-team", cwd);
574
+ assert.equal(phase?.current_phase, "failed");
575
+ assert.equal(attention?.leader_attention_reason, "mcp_transport_dead");
576
+ assert.equal(attention?.leader_attention_pending, true);
577
+ assert.equal(existsSync(join(cwd, ".omx", "state", "team", "transport-team")), true);
578
+ } finally {
579
+ process.chdir(previousCwd);
580
+ await rm(cwd, { recursive: true, force: true });
581
+ }
582
+ });
583
+
327
584
  it("treats stderr-only informative non-zero output as reviewable instead of a generic failure", async () => {
328
585
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-informative-stderr-"));
329
586
  try {
@@ -384,6 +641,67 @@ describe("codex native hook dispatch", () => {
384
641
  }
385
642
  });
386
643
 
644
+ it("returns MCP transport-death guidance and preserves failed team state", async () => {
645
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-dead-"));
646
+ try {
647
+ await initTeamState(
648
+ "mcp-transport-dead-team",
649
+ "transport failure fallback",
650
+ "executor",
651
+ 1,
652
+ cwd,
653
+ undefined,
654
+ { ...process.env, OMX_SESSION_ID: "sess-mcp-dead" },
655
+ );
656
+
657
+ const result = await dispatchCodexNativeHook(
658
+ {
659
+ hook_event_name: "PostToolUse",
660
+ cwd,
661
+ session_id: "sess-mcp-dead",
662
+ tool_name: "mcp__omx_state__state_write",
663
+ tool_use_id: "tool-mcp-dead",
664
+ tool_response: JSON.stringify({
665
+ error: "transport closed",
666
+ message: "MCP server disconnected",
667
+ }),
668
+ },
669
+ { cwd },
670
+ );
671
+
672
+ assert.equal(result.omxEventName, "post-tool-use");
673
+ assert.equal(result.outputJson?.decision, "block");
674
+ assert.match(String(result.outputJson?.reason || ""), /lost its transport\/server connection/);
675
+ const hookSpecificOutput = result.outputJson?.hookSpecificOutput as {
676
+ hookEventName?: string;
677
+ additionalContext?: string;
678
+ } | undefined;
679
+ assert.equal(hookSpecificOutput?.hookEventName, "PostToolUse");
680
+ assert.match(
681
+ String(hookSpecificOutput?.additionalContext || ""),
682
+ /Retry via CLI parity with `omx state state_write --input '\{\}' --json`\./,
683
+ );
684
+ assert.match(
685
+ String(hookSpecificOutput?.additionalContext || ""),
686
+ /omx team api read-stall-state/,
687
+ );
688
+
689
+ const phase = JSON.parse(
690
+ await readFile(join(cwd, ".omx", "state", "team", "mcp-transport-dead-team", "phase.json"), "utf-8"),
691
+ ) as { current_phase?: string; transitions?: Array<{ reason?: string }> };
692
+ assert.equal(phase.current_phase, "failed");
693
+ assert.equal(phase.transitions?.at(-1)?.reason, "mcp_transport_dead");
694
+
695
+ const attention = JSON.parse(
696
+ await readFile(join(cwd, ".omx", "state", "team", "mcp-transport-dead-team", "leader-attention.json"), "utf-8"),
697
+ ) as { leader_attention_reason?: string; attention_reasons?: string[] };
698
+ assert.equal(attention.leader_attention_reason, "mcp_transport_dead");
699
+ assert.ok(attention.attention_reasons?.includes("mcp_transport_dead"));
700
+ } finally {
701
+ await rm(cwd, { recursive: true, force: true });
702
+ }
703
+ });
704
+
387
705
  it("stays silent on neutral successful PostToolUse output", async () => {
388
706
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-neutral-"));
389
707
  try {
@@ -406,6 +724,60 @@ describe("codex native hook dispatch", () => {
406
724
  }
407
725
  });
408
726
 
727
+ it("returns CLI fallback guidance and preserves failed team state on clear MCP transport death", async () => {
728
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-posttool-mcp-transport-"));
729
+ try {
730
+ await initTeamState(
731
+ "transport-team",
732
+ "transport failure fallback",
733
+ "executor",
734
+ 1,
735
+ cwd,
736
+ undefined,
737
+ { ...process.env, OMX_SESSION_ID: "sess-stop-mcp-transport" },
738
+ );
739
+ await writeJson(join(cwd, ".omx", "state", "team-state.json"), {
740
+ active: true,
741
+ team_name: "transport-team",
742
+ current_phase: "team-exec",
743
+ });
744
+
745
+ const result = await dispatchCodexNativeHook(
746
+ {
747
+ hook_event_name: "PostToolUse",
748
+ cwd,
749
+ session_id: "sess-stop-mcp-transport",
750
+ tool_name: "mcp__omx_state__state_write",
751
+ tool_use_id: "tool-mcp-fail",
752
+ tool_input: { mode: "team", active: true },
753
+ tool_response: JSON.stringify({
754
+ error: "MCP transport closed unexpectedly",
755
+ exit_code: 1,
756
+ }),
757
+ },
758
+ { cwd },
759
+ );
760
+
761
+ assert.equal(result.omxEventName, "post-tool-use");
762
+ assert.deepEqual(result.outputJson, {
763
+ decision: "block",
764
+ 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.",
765
+ hookSpecificOutput: {
766
+ hookEventName: "PostToolUse",
767
+ additionalContext:
768
+ "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.",
769
+ },
770
+ });
771
+
772
+ const phase = await readTeamPhase("transport-team", cwd);
773
+ const attention = await readTeamLeaderAttention("transport-team", cwd);
774
+ assert.equal(phase?.current_phase, "failed");
775
+ assert.equal(attention?.leader_attention_reason, "mcp_transport_dead");
776
+ } finally {
777
+ await rm(cwd, { recursive: true, force: true });
778
+ }
779
+ });
780
+
409
781
  it("returns Stop continuation output while Autopilot is active", async () => {
410
782
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-"));
411
783
  try {
@@ -893,7 +1265,7 @@ describe("codex native hook dispatch", () => {
893
1265
  }
894
1266
  });
895
1267
 
896
- it("returns Stop continuation output for active ralplan skill without active subagents", async () => {
1268
+ it("returns Stop continuation output for active ralplan skill with matching active mode state and without active subagents", async () => {
897
1269
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-"));
898
1270
  try {
899
1271
  const stateDir = join(cwd, ".omx", "state");
@@ -904,6 +1276,10 @@ describe("codex native hook dispatch", () => {
904
1276
  skill: "ralplan",
905
1277
  phase: "planning",
906
1278
  });
1279
+ await writeJson(join(stateDir, "sessions", "sess-stop-skill", "ralplan-state.json"), {
1280
+ active: true,
1281
+ current_phase: "planning",
1282
+ });
907
1283
 
908
1284
  const result = await dispatchCodexNativeHook(
909
1285
  {
@@ -927,6 +1303,41 @@ describe("codex native hook dispatch", () => {
927
1303
  }
928
1304
  });
929
1305
 
1306
+ it("does not block on stale ralplan skill-active state when the matching mode state is absent", async () => {
1307
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-skill-"));
1308
+ try {
1309
+ const stateDir = join(cwd, ".omx", "state");
1310
+ await mkdir(join(stateDir, "sessions", "sess-stop-stale-skill"), { recursive: true });
1311
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-stale-skill" });
1312
+ await writeJson(join(stateDir, "sessions", "sess-stop-stale-skill", "skill-active-state.json"), {
1313
+ active: true,
1314
+ skill: "ralplan",
1315
+ phase: "planning",
1316
+ session_id: "sess-stop-stale-skill",
1317
+ active_skills: [{
1318
+ skill: "ralplan",
1319
+ phase: "planning",
1320
+ active: true,
1321
+ session_id: "sess-stop-stale-skill",
1322
+ }],
1323
+ });
1324
+
1325
+ const result = await dispatchCodexNativeHook(
1326
+ {
1327
+ hook_event_name: "Stop",
1328
+ cwd,
1329
+ session_id: "sess-stop-stale-skill",
1330
+ },
1331
+ { cwd },
1332
+ );
1333
+
1334
+ assert.equal(result.omxEventName, "stop");
1335
+ assert.equal(result.outputJson, null);
1336
+ } finally {
1337
+ await rm(cwd, { recursive: true, force: true });
1338
+ }
1339
+ });
1340
+
930
1341
  it("does not block on active ralplan skill when subagents are still active", async () => {
931
1342
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-subagent-"));
932
1343
  try {
@@ -938,6 +1349,10 @@ describe("codex native hook dispatch", () => {
938
1349
  skill: "ralplan",
939
1350
  phase: "planning",
940
1351
  });
1352
+ await writeJson(join(stateDir, "sessions", "sess-stop-skill-subagent", "ralplan-state.json"), {
1353
+ active: true,
1354
+ current_phase: "planning",
1355
+ });
941
1356
  await writeJson(join(stateDir, "subagent-tracking.json"), {
942
1357
  schemaVersion: 1,
943
1358
  sessions: {
@@ -981,7 +1396,35 @@ describe("codex native hook dispatch", () => {
981
1396
  }
982
1397
  });
983
1398
 
984
- it("returns Stop continuation output for active deep-interview skill without active subagents", async () => {
1399
+ it("does not block on stale root ralplan skill when the explicit session-scoped canonical skill state is absent", async () => {
1400
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-skill-"));
1401
+ try {
1402
+ const stateDir = join(cwd, ".omx", "state");
1403
+ await mkdir(stateDir, { recursive: true });
1404
+ await writeJson(join(stateDir, "skill-active-state.json"), {
1405
+ active: true,
1406
+ skill: "ralplan",
1407
+ phase: "planning",
1408
+ });
1409
+
1410
+ const result = await dispatchCodexNativeHook(
1411
+ {
1412
+ hook_event_name: "Stop",
1413
+ cwd,
1414
+ session_id: "sess-stop-stale-root-skill",
1415
+ thread_id: "thread-stop-stale-root-skill",
1416
+ },
1417
+ { cwd },
1418
+ );
1419
+
1420
+ assert.equal(result.omxEventName, "stop");
1421
+ assert.equal(result.outputJson, null);
1422
+ } finally {
1423
+ await rm(cwd, { recursive: true, force: true });
1424
+ }
1425
+ });
1426
+
1427
+ it("returns Stop continuation output for active deep-interview skill with matching active mode state and without active subagents", async () => {
985
1428
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-deep-interview-"));
986
1429
  try {
987
1430
  const stateDir = join(cwd, ".omx", "state");
@@ -992,6 +1435,10 @@ describe("codex native hook dispatch", () => {
992
1435
  skill: "deep-interview",
993
1436
  phase: "planning",
994
1437
  });
1438
+ await writeJson(join(stateDir, "sessions", "sess-stop-deep-interview", "deep-interview-state.json"), {
1439
+ active: true,
1440
+ current_phase: "planning",
1441
+ });
995
1442
 
996
1443
  const result = await dispatchCodexNativeHook(
997
1444
  {
@@ -1079,6 +1526,35 @@ describe("codex native hook dispatch", () => {
1079
1526
  }
1080
1527
  });
1081
1528
 
1529
+ it("does not block Stop from stale session-scoped Ralph state that belongs to another session", async () => {
1530
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-session-ralph-"));
1531
+ try {
1532
+ const stateDir = join(cwd, ".omx", "state");
1533
+ await mkdir(join(stateDir, "sessions", "sess-current"), { recursive: true });
1534
+ await mkdir(join(stateDir, "sessions", "sess-stale"), { recursive: true });
1535
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-current" });
1536
+ await writeJson(join(stateDir, "sessions", "sess-stale", "ralph-state.json"), {
1537
+ active: true,
1538
+ current_phase: "starting",
1539
+ session_id: "sess-stale",
1540
+ });
1541
+
1542
+ const result = await dispatchCodexNativeHook(
1543
+ {
1544
+ hook_event_name: "Stop",
1545
+ cwd,
1546
+ session_id: "sess-current",
1547
+ },
1548
+ { cwd },
1549
+ );
1550
+
1551
+ assert.equal(result.omxEventName, "stop");
1552
+ assert.equal(result.outputJson, null);
1553
+ } finally {
1554
+ await rm(cwd, { recursive: true, force: true });
1555
+ }
1556
+ });
1557
+
1082
1558
  it("does not re-block Ralph when Stop already continued once", async () => {
1083
1559
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-ralph-once-"));
1084
1560
  try {
@@ -1238,6 +1714,10 @@ describe("codex native hook dispatch", () => {
1238
1714
  message: "Deep interview is active; auto-approval shortcuts are blocked until the interview finishes.",
1239
1715
  },
1240
1716
  });
1717
+ await writeJson(join(stateDir, "sessions", "sess-stop-auto-lock", "deep-interview-state.json"), {
1718
+ active: true,
1719
+ current_phase: "planning",
1720
+ });
1241
1721
 
1242
1722
  const result = await dispatchCodexNativeHook(
1243
1723
  {
@@ -1296,7 +1776,7 @@ describe("codex native hook dispatch", () => {
1296
1776
  }
1297
1777
  });
1298
1778
 
1299
- it("suppresses native auto-nudge when deep-interview mode state is active without a scoped skill state", async () => {
1779
+ it("suppresses native auto-nudge when root deep-interview mode state is active without an explicit session", async () => {
1300
1780
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-deep-interview-mode-"));
1301
1781
  try {
1302
1782
  const stateDir = join(cwd, ".omx", "state");
@@ -1311,8 +1791,6 @@ describe("codex native hook dispatch", () => {
1311
1791
  {
1312
1792
  hook_event_name: "Stop",
1313
1793
  cwd,
1314
- session_id: "sess-stop-auto-mode",
1315
- thread_id: "thread-stop-auto-mode",
1316
1794
  turn_id: "turn-stop-auto-mode-1",
1317
1795
  last_assistant_message: "Would you like me to continue with the next step?",
1318
1796
  },
@@ -1326,6 +1804,120 @@ describe("codex native hook dispatch", () => {
1326
1804
  }
1327
1805
  });
1328
1806
 
1807
+ it("does not suppress native auto-nudge from stale root deep-interview mode state when the explicit session-scoped mode state is absent", async () => {
1808
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-stale-root-mode-"));
1809
+ try {
1810
+ const stateDir = join(cwd, ".omx", "state");
1811
+ await mkdir(stateDir, { recursive: true });
1812
+ await writeJson(join(stateDir, "deep-interview-state.json"), {
1813
+ active: true,
1814
+ mode: "deep-interview",
1815
+ current_phase: "intent-first",
1816
+ });
1817
+
1818
+ const result = await dispatchCodexNativeHook(
1819
+ {
1820
+ hook_event_name: "Stop",
1821
+ cwd,
1822
+ session_id: "sess-stop-auto-stale-root-mode",
1823
+ thread_id: "thread-stop-auto-stale-root-mode",
1824
+ turn_id: "turn-stop-auto-stale-root-mode-1",
1825
+ last_assistant_message: "Would you like me to continue with the next step?",
1826
+ },
1827
+ { cwd },
1828
+ );
1829
+
1830
+ assert.equal(result.omxEventName, "stop");
1831
+ assert.deepEqual(result.outputJson, {
1832
+ decision: "block",
1833
+ reason: "yes, proceed",
1834
+ stopReason: "auto_nudge",
1835
+ systemMessage:
1836
+ "OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
1837
+ });
1838
+ } finally {
1839
+ await rm(cwd, { recursive: true, force: true });
1840
+ }
1841
+ });
1842
+
1843
+ 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 () => {
1844
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-stale-root-skill-"));
1845
+ try {
1846
+ const stateDir = join(cwd, ".omx", "state");
1847
+ await mkdir(stateDir, { recursive: true });
1848
+ await writeJson(join(stateDir, "skill-active-state.json"), {
1849
+ active: true,
1850
+ skill: "deep-interview",
1851
+ phase: "planning",
1852
+ });
1853
+
1854
+ const result = await dispatchCodexNativeHook(
1855
+ {
1856
+ hook_event_name: "Stop",
1857
+ cwd,
1858
+ session_id: "sess-stop-auto-stale-root-skill",
1859
+ thread_id: "thread-stop-auto-stale-root-skill",
1860
+ turn_id: "turn-stop-auto-stale-root-skill-1",
1861
+ last_assistant_message: "Would you like me to continue with the next step?",
1862
+ },
1863
+ { cwd },
1864
+ );
1865
+
1866
+ assert.equal(result.omxEventName, "stop");
1867
+ assert.deepEqual(result.outputJson, {
1868
+ decision: "block",
1869
+ reason: "yes, proceed",
1870
+ stopReason: "auto_nudge",
1871
+ systemMessage:
1872
+ "OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
1873
+ });
1874
+ } finally {
1875
+ await rm(cwd, { recursive: true, force: true });
1876
+ }
1877
+ });
1878
+
1879
+ 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 () => {
1880
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-auto-nudge-stale-root-lock-"));
1881
+ try {
1882
+ const stateDir = join(cwd, ".omx", "state");
1883
+ await mkdir(stateDir, { recursive: true });
1884
+ await writeJson(join(stateDir, "skill-active-state.json"), {
1885
+ active: true,
1886
+ skill: "deep-interview",
1887
+ phase: "planning",
1888
+ input_lock: {
1889
+ active: true,
1890
+ scope: "deep-interview-auto-approval",
1891
+ blocked_inputs: ["yes", "proceed"],
1892
+ message: "Deep interview is active; auto-approval shortcuts are blocked until the interview finishes.",
1893
+ },
1894
+ });
1895
+
1896
+ const result = await dispatchCodexNativeHook(
1897
+ {
1898
+ hook_event_name: "Stop",
1899
+ cwd,
1900
+ session_id: "sess-stop-auto-stale-root-lock",
1901
+ thread_id: "thread-stop-auto-stale-root-lock",
1902
+ turn_id: "turn-stop-auto-stale-root-lock-1",
1903
+ last_assistant_message: "Would you like me to continue with the next step?",
1904
+ },
1905
+ { cwd },
1906
+ );
1907
+
1908
+ assert.equal(result.omxEventName, "stop");
1909
+ assert.deepEqual(result.outputJson, {
1910
+ decision: "block",
1911
+ reason: "yes, proceed",
1912
+ stopReason: "auto_nudge",
1913
+ systemMessage:
1914
+ "OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
1915
+ });
1916
+ } finally {
1917
+ await rm(cwd, { recursive: true, force: true });
1918
+ }
1919
+ });
1920
+
1329
1921
  it("re-fires team Stop output for a later fresh Stop reply while the team is still active", async () => {
1330
1922
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-refire-"));
1331
1923
  try {