oh-my-codex 0.12.2 → 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 +62 -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 +20 -8
  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 +481 -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 +171 -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 +227 -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 +203 -85
  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 +636 -8
  174. package/src/scripts/codex-native-hook.ts +249 -60
  175. package/src/scripts/codex-native-pre-post.ts +104 -0
  176. package/src/scripts/notify-hook/operational-events.ts +6 -2
@@ -2,12 +2,22 @@ import { execFileSync } from "child_process";
2
2
  import { existsSync, readFileSync } from "fs";
3
3
  import { mkdir, readFile, readdir, writeFile } from "fs/promises";
4
4
  import { join, resolve } from "path";
5
- import { readModeState } from "../modes/base.js";
6
- import { getReadScopedStateDirs } from "../mcp/state-paths.js";
5
+ import { readModeState, updateModeState } from "../modes/base.js";
6
+ import {
7
+ listActiveSkills,
8
+ readVisibleSkillActiveState,
9
+ } from "../state/skill-active.js";
7
10
  import { readSubagentSessionSummary } from "../subagents/tracker.js";
8
11
  import { resolveCanonicalTeamStateRoot } from "../team/state-root.js";
9
- import { readTeamManifestV2, readTeamPhase } from "../team/state.js";
10
- import { omxNotepadPath, omxProjectMemoryPath } from "../utils/paths.js";
12
+ import {
13
+ appendTeamEvent,
14
+ readTeamLeaderAttention,
15
+ readTeamManifestV2,
16
+ readTeamPhase,
17
+ writeTeamLeaderAttention,
18
+ writeTeamPhase,
19
+ } from "../team/state.js";
20
+ import { omxNotepadPath, omxProjectMemoryPath, omxStateDir } from "../utils/paths.js";
11
21
  import {
12
22
  detectPrimaryKeyword,
13
23
  recordSkillActivation,
@@ -15,14 +25,13 @@ import {
15
25
  } from "../hooks/keyword-detector.js";
16
26
  import {
17
27
  detectStallPattern,
18
- isDeepInterviewInputLockActive,
19
- isDeepInterviewStateActive,
20
28
  loadAutoNudgeConfig,
21
29
  normalizeAutoNudgeSignatureText,
22
30
  } from "./notify-hook/auto-nudge.js";
23
31
  import {
24
32
  buildNativePostToolUseOutput,
25
33
  buildNativePreToolUseOutput,
34
+ detectMcpTransportFailure,
26
35
  } from "./codex-native-pre-post.js";
27
36
  import {
28
37
  buildNativeHookEvent,
@@ -30,6 +39,7 @@ import {
30
39
  import type { HookEventEnvelope } from "../hooks/extensibility/types.js";
31
40
  import { dispatchHookEvent } from "../hooks/extensibility/dispatcher.js";
32
41
  import { writeSessionStart } from "../hooks/session.js";
42
+ import { reconcileHudForPromptSubmit } from "../hud/reconcile.js";
33
43
 
34
44
  type CodexHookEventName =
35
45
  | "SessionStart"
@@ -154,19 +164,6 @@ async function readJsonIfExists(path: string): Promise<Record<string, unknown> |
154
164
  }
155
165
  }
156
166
 
157
- async function readScopedJsonState(
158
- fileName: string,
159
- cwd: string,
160
- sessionId?: string,
161
- ): Promise<Record<string, unknown> | null> {
162
- const dirs = await getReadScopedStateDirs(cwd, sessionId);
163
- for (const dir of dirs) {
164
- const candidate = await readJsonIfExists(join(dir, fileName));
165
- if (candidate) return candidate;
166
- }
167
- return null;
168
- }
169
-
170
167
  function isNonTerminalPhase(value: unknown): boolean {
171
168
  const phase = safeString(value).trim().toLowerCase();
172
169
  return phase !== "" && !TERMINAL_MODE_PHASES.has(phase);
@@ -178,26 +175,28 @@ function formatPhase(value: unknown, fallback = "active"): string {
178
175
  }
179
176
 
180
177
  async function readActiveRalphState(stateDir: string): Promise<Record<string, unknown> | null> {
178
+ const sessionInfo = await readJsonIfExists(join(stateDir, "session.json"));
179
+ const currentOmxSessionId = safeString(sessionInfo?.session_id).trim();
180
+ if (currentOmxSessionId) {
181
+ const sessionScoped = await readJsonIfExists(
182
+ join(stateDir, "sessions", currentOmxSessionId, "ralph-state.json"),
183
+ );
184
+ if (
185
+ sessionScoped?.active === true
186
+ && !TERMINAL_RALPH_PHASES.has(
187
+ safeString(sessionScoped.current_phase).trim().toLowerCase(),
188
+ )
189
+ ) {
190
+ return sessionScoped;
191
+ }
192
+ }
193
+
181
194
  const direct = await readJsonIfExists(join(stateDir, "ralph-state.json"));
182
195
  if (direct?.active === true && !TERMINAL_RALPH_PHASES.has(safeString(direct.current_phase).trim().toLowerCase())) {
183
196
  return direct;
184
197
  }
185
198
 
186
- const sessionInfo = await readJsonIfExists(join(stateDir, "session.json"));
187
- const currentOmxSessionId = safeString(sessionInfo?.session_id).trim();
188
- if (!currentOmxSessionId) return null;
189
-
190
- const sessionScoped = await readJsonIfExists(
191
- join(stateDir, "sessions", currentOmxSessionId, "ralph-state.json"),
192
- );
193
- if (
194
- sessionScoped?.active === true
195
- && !TERMINAL_RALPH_PHASES.has(
196
- safeString(sessionScoped.current_phase).trim().toLowerCase(),
197
- )
198
- ) {
199
- return sessionScoped;
200
- }
199
+ if (currentOmxSessionId) return null;
201
200
 
202
201
  const sessionsRoot = join(stateDir, "sessions");
203
202
  if (!existsSync(sessionsRoot)) return null;
@@ -429,6 +428,19 @@ function buildAdditionalContextMessage(prompt: string, skillState?: SkillActiveS
429
428
  const match = detectPrimaryKeyword(prompt);
430
429
  if (!match) return null;
431
430
 
431
+ if (match.skill === "team") {
432
+ const initializedStateMessage = skillState?.initialized_mode && skillState.initialized_state_path
433
+ ? `skill: ${skillState.initialized_mode} activated and initial state initialized at ${skillState.initialized_state_path}; write subsequent updates via omx_state MCP.`
434
+ : null;
435
+ return [
436
+ `OMX native UserPromptSubmit detected workflow keyword "${match.keyword}" -> ${match.skill}.`,
437
+ initializedStateMessage,
438
+ "Use the durable OMX team runtime via `omx team ...` for coordinated execution; do not replace it with in-process fanout.",
439
+ "If you need help, run `omx team --help`.",
440
+ "Follow AGENTS.md routing and preserve ralplan/ralph execution gates.",
441
+ ].filter(Boolean).join(" ");
442
+ }
443
+
432
444
  if (skillState?.initialized_mode && skillState.initialized_state_path) {
433
445
  return [
434
446
  `OMX native UserPromptSubmit detected workflow keyword "${match.keyword}" -> ${match.skill}.`,
@@ -608,22 +620,100 @@ function readPayloadTurnId(payload: CodexHookPayload): string {
608
620
 
609
621
  async function isDeepInterviewSuppressedForStop(
610
622
  cwd: string,
611
- stateDir: string,
612
623
  sessionId: string,
624
+ threadId: string,
613
625
  ): Promise<boolean> {
614
- if (await isDeepInterviewStateActive(stateDir)) return true;
615
- if (await isDeepInterviewInputLockActive(stateDir)) return true;
616
-
617
- const scopedModeState = sessionId
618
- ? await readScopedJsonState("deep-interview-state.json", cwd, sessionId)
619
- : null;
626
+ const scopedModeState = await readStopSessionPinnedState("deep-interview-state.json", cwd, sessionId);
620
627
  if (scopedModeState?.active === true) return true;
621
628
 
622
- const scopedSkillState = sessionId
623
- ? await readScopedJsonState("skill-active-state.json", cwd, sessionId)
629
+ const canonicalState = await readVisibleSkillActiveState(cwd, sessionId);
630
+ const deepInterviewEntry = canonicalState
631
+ ? listActiveSkills(canonicalState).find((entry) => (
632
+ entry.skill === "deep-interview"
633
+ && matchesSkillStopContext(entry, canonicalState, sessionId, threadId)
634
+ ))
624
635
  : null;
625
- if (!scopedSkillState || scopedSkillState.active !== true) return false;
626
- return safeString(scopedSkillState.skill).trim() === "deep-interview";
636
+ if (
637
+ deepInterviewEntry
638
+ && safeObject(canonicalState?.input_lock).active === true
639
+ ) {
640
+ return true;
641
+ }
642
+
643
+ return (await readBlockingSkillForStop(cwd, sessionId, threadId, "deep-interview")) !== null;
644
+ }
645
+
646
+ async function readStopSessionPinnedState(
647
+ fileName: string,
648
+ cwd: string,
649
+ sessionId: string,
650
+ ): Promise<Record<string, unknown> | null> {
651
+ const stateDir = omxStateDir(cwd);
652
+ const statePath = sessionId
653
+ ? join(stateDir, "sessions", sessionId, fileName)
654
+ : join(stateDir, fileName);
655
+ return readJsonIfExists(statePath);
656
+ }
657
+
658
+ function matchesSkillStopContext(
659
+ entry: { session_id?: string; thread_id?: string },
660
+ state: { session_id?: string; thread_id?: string },
661
+ sessionId: string,
662
+ threadId: string,
663
+ ): boolean {
664
+ const entrySessionId = safeString(entry.session_id ?? state.session_id).trim();
665
+ const entryThreadId = safeString(entry.thread_id ?? state.thread_id).trim();
666
+ if (sessionId && entrySessionId && entrySessionId !== sessionId) return false;
667
+ if (sessionId && !entrySessionId && threadId && entryThreadId && entryThreadId !== threadId) {
668
+ return false;
669
+ }
670
+ return true;
671
+ }
672
+
673
+ async function readBlockingSkillForStop(
674
+ cwd: string,
675
+ sessionId: string,
676
+ threadId: string,
677
+ requiredSkill?: string,
678
+ ): Promise<{ skill: string; phase: string } | null> {
679
+ const canonicalState = await readVisibleSkillActiveState(cwd, sessionId);
680
+ const visibleEntries = canonicalState ? listActiveSkills(canonicalState) : [];
681
+ const candidateSkills = requiredSkill
682
+ ? [requiredSkill]
683
+ : [...SKILL_STOP_BLOCKERS];
684
+
685
+ for (const skill of candidateSkills) {
686
+ const modeState = await readStopSessionPinnedState(`${skill}-state.json`, cwd, sessionId);
687
+ if (!modeState || modeState.active !== true) continue;
688
+
689
+ const phase = formatPhase(
690
+ modeState.current_phase,
691
+ formatPhase(
692
+ visibleEntries.find((entry) => entry.skill === skill)?.phase,
693
+ "planning",
694
+ ),
695
+ );
696
+ if (TERMINAL_MODE_PHASES.has(phase.toLowerCase()) || phase === "completing") {
697
+ continue;
698
+ }
699
+
700
+ if (!canonicalState) {
701
+ return { skill, phase };
702
+ }
703
+
704
+ const blocker = visibleEntries.find((entry) => (
705
+ entry.skill === skill
706
+ && matchesSkillStopContext(entry, canonicalState, sessionId, threadId)
707
+ ));
708
+ if (!blocker) continue;
709
+
710
+ return {
711
+ skill,
712
+ phase: formatPhase(modeState.current_phase ?? blocker.phase ?? canonicalState.phase, "planning"),
713
+ };
714
+ }
715
+
716
+ return null;
627
717
  }
628
718
 
629
719
  function buildRepeatableStopSignature(
@@ -753,17 +843,8 @@ async function buildSkillStopOutput(
753
843
  sessionId: string,
754
844
  threadId: string,
755
845
  ): Promise<Record<string, unknown> | null> {
756
- const state = await readScopedJsonState("skill-active-state.json", cwd, sessionId);
757
- if (!state || state.active !== true) return null;
758
- const stateSessionId = safeString(state.session_id).trim();
759
- const stateThreadId = safeString(state.thread_id).trim();
760
- if (sessionId && stateSessionId && stateSessionId !== sessionId) return null;
761
- if (sessionId && !stateSessionId && threadId && stateThreadId && stateThreadId !== threadId) {
762
- return null;
763
- }
764
- const skill = safeString(state.skill).trim();
765
- const phase = formatPhase(state.phase, "planning");
766
- if (!SKILL_STOP_BLOCKERS.has(skill) || phase === "completing") return null;
846
+ const blocker = await readBlockingSkillForStop(cwd, sessionId, threadId);
847
+ if (!blocker) return null;
767
848
 
768
849
  const subagentSummary = await readSubagentSessionSummary(cwd, sessionId).catch(() => null);
769
850
  if (subagentSummary && subagentSummary.activeSubagentThreadIds.length > 0) {
@@ -772,12 +853,116 @@ async function buildSkillStopOutput(
772
853
 
773
854
  return {
774
855
  decision: "block",
775
- reason: `OMX skill ${skill} is still active (phase: ${phase}); continue until the current ${skill} workflow reaches a terminal state.`,
776
- stopReason: `skill_${skill}_${phase}`,
777
- systemMessage: `OMX skill ${skill} is still active (phase: ${phase}).`,
856
+ reason: `OMX skill ${blocker.skill} is still active (phase: ${blocker.phase}); continue until the current ${blocker.skill} workflow reaches a terminal state.`,
857
+ stopReason: `skill_${blocker.skill}_${blocker.phase}`,
858
+ systemMessage: `OMX skill ${blocker.skill} is still active (phase: ${blocker.phase}).`,
778
859
  };
779
860
  }
780
861
 
862
+ async function findActiveTeamForTransportFailure(
863
+ cwd: string,
864
+ sessionId: string,
865
+ ): Promise<{ teamName: string; phase: string } | null> {
866
+ const teamState = await readModeState("team", cwd);
867
+ if (teamState?.active === true) {
868
+ const teamName = safeString(teamState.team_name).trim();
869
+ const coarsePhase = formatPhase(teamState.current_phase);
870
+ if (teamName) {
871
+ const canonicalPhase = (await readTeamPhase(teamName, cwd))?.current_phase ?? coarsePhase;
872
+ if (isNonTerminalPhase(canonicalPhase)) {
873
+ return { teamName, phase: formatPhase(canonicalPhase) };
874
+ }
875
+ }
876
+ }
877
+
878
+ return await findCanonicalActiveTeamForSession(cwd, sessionId);
879
+ }
880
+
881
+ async function markTeamTransportFailure(
882
+ cwd: string,
883
+ payload: CodexHookPayload,
884
+ ): Promise<void> {
885
+ const sessionId = readPayloadSessionId(payload);
886
+ const activeTeam = await findActiveTeamForTransportFailure(cwd, sessionId);
887
+ if (!activeTeam) return;
888
+
889
+ const nowIso = new Date().toISOString();
890
+ const existingPhase = await readTeamPhase(activeTeam.teamName, cwd);
891
+ const currentPhase = existingPhase?.current_phase ?? activeTeam.phase;
892
+ if (!isNonTerminalPhase(currentPhase)) return;
893
+
894
+ await writeTeamPhase(
895
+ activeTeam.teamName,
896
+ {
897
+ current_phase: "failed",
898
+ max_fix_attempts: existingPhase?.max_fix_attempts ?? 3,
899
+ current_fix_attempt: existingPhase?.current_fix_attempt ?? 0,
900
+ transitions: [
901
+ ...(existingPhase?.transitions ?? []),
902
+ {
903
+ from: formatPhase(currentPhase),
904
+ to: "failed",
905
+ at: nowIso,
906
+ reason: "mcp_transport_dead",
907
+ },
908
+ ],
909
+ updated_at: nowIso,
910
+ },
911
+ cwd,
912
+ );
913
+
914
+ const existingAttention = await readTeamLeaderAttention(activeTeam.teamName, cwd);
915
+ await writeTeamLeaderAttention(
916
+ activeTeam.teamName,
917
+ {
918
+ team_name: activeTeam.teamName,
919
+ updated_at: nowIso,
920
+ source: "notify_hook",
921
+ leader_decision_state: existingAttention?.leader_decision_state ?? "still_actionable",
922
+ leader_attention_pending: true,
923
+ leader_attention_reason: "mcp_transport_dead",
924
+ attention_reasons: [
925
+ ...new Set([...(existingAttention?.attention_reasons ?? []), "mcp_transport_dead"]),
926
+ ],
927
+ leader_stale: existingAttention?.leader_stale ?? false,
928
+ leader_session_active: existingAttention?.leader_session_active ?? true,
929
+ leader_session_id: existingAttention?.leader_session_id ?? (sessionId || null),
930
+ leader_session_stopped_at: existingAttention?.leader_session_stopped_at ?? null,
931
+ unread_leader_message_count: existingAttention?.unread_leader_message_count ?? 0,
932
+ work_remaining: existingAttention?.work_remaining ?? true,
933
+ stalled_for_ms: existingAttention?.stalled_for_ms ?? null,
934
+ },
935
+ cwd,
936
+ );
937
+
938
+ await appendTeamEvent(
939
+ activeTeam.teamName,
940
+ {
941
+ type: "leader_attention",
942
+ worker: "leader-fixed",
943
+ reason: "mcp_transport_dead",
944
+ metadata: {
945
+ phase_before: formatPhase(currentPhase),
946
+ },
947
+ },
948
+ cwd,
949
+ ).catch(() => {});
950
+
951
+ try {
952
+ await updateModeState(
953
+ "team",
954
+ {
955
+ current_phase: "failed",
956
+ error: "mcp_transport_dead",
957
+ last_turn_at: nowIso,
958
+ },
959
+ cwd,
960
+ );
961
+ } catch {
962
+ // Canonical team state already carries the preserved failure for coarse-state-missing sessions.
963
+ }
964
+ }
965
+
781
966
  async function buildStopHookOutput(
782
967
  payload: CodexHookPayload,
783
968
  cwd: string,
@@ -831,7 +1016,7 @@ async function buildStopHookOutput(
831
1016
  if (!stopHookActive && skillOutput) return skillOutput;
832
1017
  }
833
1018
 
834
- const deepInterviewActive = await isDeepInterviewSuppressedForStop(cwd, stateDir, sessionId);
1019
+ const deepInterviewActive = await isDeepInterviewSuppressedForStop(cwd, sessionId, threadId);
835
1020
  const lastAssistantMessage = safeString(
836
1021
  payload.last_assistant_message ?? payload.lastAssistantMessage,
837
1022
  );
@@ -909,6 +1094,7 @@ export async function dispatchCodexNativeHook(
909
1094
  turnId,
910
1095
  });
911
1096
  }
1097
+ await reconcileHudForPromptSubmit(cwd).catch(() => {});
912
1098
  }
913
1099
 
914
1100
  if (omxEventName) {
@@ -941,6 +1127,9 @@ export async function dispatchCodexNativeHook(
941
1127
  } else if (hookEventName === "PreToolUse") {
942
1128
  outputJson = buildNativePreToolUseOutput(payload);
943
1129
  } else if (hookEventName === "PostToolUse") {
1130
+ if (detectMcpTransportFailure(payload)) {
1131
+ await markTeamTransportFailure(cwd, payload);
1132
+ }
944
1133
  outputJson = buildNativePostToolUseOutput(payload);
945
1134
  } else if (hookEventName === "Stop") {
946
1135
  outputJson = await buildStopHookOutput(payload, cwd, stateDir);
@@ -21,6 +21,11 @@ export interface NormalizedPostToolUsePayload {
21
21
  stderrText: string;
22
22
  }
23
23
 
24
+ export interface McpTransportFailureSignal {
25
+ toolName: string;
26
+ summary: string;
27
+ }
28
+
24
29
  function safeString(value: unknown): string {
25
30
  return typeof value === "string" ? value : "";
26
31
  }
@@ -101,6 +106,88 @@ function matchesDestructiveFixture(command: string): boolean {
101
106
  return /^\s*rm\s+-rf\s+dist(?:\s|$)/.test(command);
102
107
  }
103
108
 
109
+ function isMcpLikeToolName(toolName: string): boolean {
110
+ return /^(mcp__|omx_(?:state|memory|trace|code_intel)\b|state_|project_memory_|notepad_|trace_)/i.test(toolName);
111
+ }
112
+
113
+ const MCP_TRANSPORT_FAILURE_PATTERNS = [
114
+ /transport (?:closed|error|failed)/i,
115
+ /server disconnected/i,
116
+ /connection (?:closed|reset|lost)/i,
117
+ /\beconnreset\b/i,
118
+ /\bepipe\b/i,
119
+ /broken pipe/i,
120
+ /stream ended unexpectedly/i,
121
+ /stdio .*closed/i,
122
+ /pipe closed/i,
123
+ /mcp(?: server)? .*closed/i,
124
+ ];
125
+
126
+ type OmxParityCommand =
127
+ | "state"
128
+ | "notepad"
129
+ | "project-memory"
130
+ | "trace"
131
+ | "code-intel";
132
+
133
+ export function detectMcpTransportFailure(
134
+ payload: CodexHookPayload,
135
+ ): McpTransportFailureSignal | null {
136
+ const normalized = normalizePostToolUsePayload(payload);
137
+ const combined = [
138
+ normalized.stderrText,
139
+ normalized.stdoutText,
140
+ safeString(normalized.parsedToolResponse?.error),
141
+ safeString(normalized.parsedToolResponse?.message),
142
+ safeString(normalized.parsedToolResponse?.details),
143
+ ]
144
+ .filter(Boolean)
145
+ .join("\n")
146
+ .trim();
147
+
148
+ const mcpContextDetected = isMcpLikeToolName(normalized.toolName)
149
+ || /\bmcp\b/i.test(combined)
150
+ || /\bomx-(?:state|memory|trace|code-intel)-server\b/i.test(combined);
151
+ if (!mcpContextDetected) return null;
152
+ if (!combined) return null;
153
+ if (!MCP_TRANSPORT_FAILURE_PATTERNS.some((pattern) => pattern.test(combined))) {
154
+ return null;
155
+ }
156
+
157
+ return {
158
+ toolName: normalized.toolName,
159
+ summary: combined,
160
+ };
161
+ }
162
+
163
+ function resolveOmxParityTarget(toolName: string): { command: OmxParityCommand; tool: string } | null {
164
+ const match = toolName.match(/^mcp__omx_(state|memory|trace|code_intel)__([a-z0-9_]+)$/i);
165
+ if (!match) return null;
166
+
167
+ const [, server, tool] = match;
168
+ if (server === "state") return { command: "state", tool };
169
+ if (server === "trace") return { command: "trace", tool };
170
+ if (server === "code_intel") return { command: "code-intel", tool };
171
+ if (server === "memory" && tool.startsWith("notepad_")) {
172
+ return { command: "notepad", tool };
173
+ }
174
+ if (server === "memory" && tool.startsWith("project_memory_")) {
175
+ return { command: "project-memory", tool };
176
+ }
177
+ return null;
178
+ }
179
+
180
+ function shellSingleQuote(value: string): string {
181
+ return `'${value.replace(/'/g, `'\"'\"'`)}'`;
182
+ }
183
+
184
+ function buildOmxParityFallbackCommand(payload: CodexHookPayload, toolName: string): string | null {
185
+ const target = resolveOmxParityTarget(toolName);
186
+ if (!target) return null;
187
+ const input = safeObject(payload.tool_input) ?? {};
188
+ return `omx ${target.command} ${target.tool} --input ${shellSingleQuote(JSON.stringify(input))} --json`;
189
+ }
190
+
104
191
  export function buildNativePreToolUseOutput(
105
192
  payload: CodexHookPayload,
106
193
  ): Record<string, unknown> | null {
@@ -124,6 +211,23 @@ function containsHardFailure(text: string): boolean {
124
211
  export function buildNativePostToolUseOutput(
125
212
  payload: CodexHookPayload,
126
213
  ): Record<string, unknown> | null {
214
+ const mcpTransportFailure = detectMcpTransportFailure(payload);
215
+ if (mcpTransportFailure) {
216
+ const fallbackCommand = buildOmxParityFallbackCommand(payload, mcpTransportFailure.toolName);
217
+ const fallbackText = fallbackCommand
218
+ ? `Retry via CLI parity with \`${fallbackCommand}\`.`
219
+ : "Retry via the matching OMX CLI parity surface instead of retrying the MCP transport blindly.";
220
+ return {
221
+ decision: "block",
222
+ 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.",
223
+ hookSpecificOutput: {
224
+ hookEventName: "PostToolUse",
225
+ additionalContext:
226
+ `Clear MCP transport-death signal detected. Preserve current team/runtime state. ${fallbackText} 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.`,
227
+ },
228
+ };
229
+ }
230
+
127
231
  const normalized = normalizePostToolUsePayload(payload);
128
232
  if (!normalized.isBash) return null;
129
233
 
@@ -70,8 +70,12 @@ function buildTmuxSessionName(cwd: any, sessionId: any): string {
70
70
  const branch = gitValue(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
71
71
  const branchToken = branch ? sanitizeTmuxToken(branch) : 'detached';
72
72
  const sessionToken = sanitizeTmuxToken(safeString(sessionId).replace(/^omx-/, ''));
73
- const name = `omx-${dirToken}-${branchToken}-${sessionToken}`;
74
- return name.length > 120 ? name.slice(0, 120) : name;
73
+ const prefix = `omx-${dirToken}-${branchToken}`;
74
+ const name = `${prefix}-${sessionToken}`;
75
+ if (name.length <= 120) return name;
76
+ const prefixBudget = Math.max(4, 120 - sessionToken.length - 1);
77
+ const trimmedPrefix = prefix.slice(0, prefixBudget).replace(/-+$/g, '');
78
+ return `${trimmedPrefix}-${sessionToken}`.slice(0, 120);
75
79
  }
76
80
 
77
81
  export function resolveOperationalSessionName(cwd: any, sessionId = '', sessionName = ''): string | undefined {