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.
- package/Cargo.lock +5 -5
- package/Cargo.toml +1 -1
- package/README.md +2 -0
- package/dist/cli/__tests__/index.test.js +73 -12
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +8 -27
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-parity.test.d.ts +2 -0
- package/dist/cli/__tests__/mcp-parity.test.d.ts.map +1 -0
- package/dist/cli/__tests__/mcp-parity.test.js +111 -0
- package/dist/cli/__tests__/mcp-parity.test.js.map +1 -0
- package/dist/cli/__tests__/nested-help-routing.test.js +13 -0
- package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
- package/dist/cli/__tests__/package-bin-contract.test.js +6 -1
- package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.d.ts +2 -0
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.d.ts.map +1 -0
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +189 -0
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -0
- package/dist/cli/__tests__/setup-scope.test.js +48 -0
- package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
- package/dist/cli/__tests__/state.test.d.ts +2 -0
- package/dist/cli/__tests__/state.test.d.ts.map +1 -0
- package/dist/cli/__tests__/state.test.js +46 -0
- package/dist/cli/__tests__/state.test.js.map +1 -0
- package/dist/cli/__tests__/team.test.js +238 -2
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +37 -2
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/index.d.ts +6 -13
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +47 -60
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-parity.d.ts +22 -0
- package/dist/cli/mcp-parity.d.ts.map +1 -0
- package/dist/cli/mcp-parity.js +227 -0
- package/dist/cli/mcp-parity.js.map +1 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +5 -2
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/state.d.ts +8 -0
- package/dist/cli/state.d.ts.map +1 -0
- package/dist/cli/state.js +71 -0
- package/dist/cli/state.js.map +1 -0
- package/dist/cli/team.d.ts.map +1 -1
- package/dist/cli/team.js +6 -5
- package/dist/cli/team.js.map +1 -1
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +18 -4
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/config/__tests__/codex-hooks.test.d.ts +2 -0
- package/dist/config/__tests__/codex-hooks.test.d.ts.map +1 -0
- package/dist/config/__tests__/codex-hooks.test.js +53 -0
- package/dist/config/__tests__/codex-hooks.test.js.map +1 -0
- package/dist/config/codex-hooks.d.ts +16 -7
- package/dist/config/codex-hooks.d.ts.map +1 -1
- package/dist/config/codex-hooks.js +134 -2
- package/dist/config/codex-hooks.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +6 -0
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +6 -0
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.d.ts +2 -0
- package/dist/hud/__tests__/reconcile.test.d.ts.map +1 -0
- package/dist/hud/__tests__/reconcile.test.js +83 -0
- package/dist/hud/__tests__/reconcile.test.js.map +1 -0
- package/dist/hud/__tests__/render.test.js +43 -0
- package/dist/hud/__tests__/render.test.js.map +1 -1
- package/dist/hud/constants.d.ts +2 -1
- package/dist/hud/constants.d.ts.map +1 -1
- package/dist/hud/constants.js +2 -1
- package/dist/hud/constants.js.map +1 -1
- package/dist/hud/index.d.ts +4 -1
- package/dist/hud/index.d.ts.map +1 -1
- package/dist/hud/index.js +11 -5
- package/dist/hud/index.js.map +1 -1
- package/dist/hud/reconcile.d.ts +23 -0
- package/dist/hud/reconcile.d.ts.map +1 -0
- package/dist/hud/reconcile.js +71 -0
- package/dist/hud/reconcile.js.map +1 -0
- package/dist/hud/render.d.ts +6 -1
- package/dist/hud/render.d.ts.map +1 -1
- package/dist/hud/render.js +77 -3
- package/dist/hud/render.js.map +1 -1
- package/dist/hud/tmux.d.ts +26 -0
- package/dist/hud/tmux.d.ts.map +1 -0
- package/dist/hud/tmux.js +126 -0
- package/dist/hud/tmux.js.map +1 -0
- package/dist/mcp/bootstrap.d.ts.map +1 -1
- package/dist/mcp/bootstrap.js +16 -6
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/mcp/code-intel-server.d.ts +298 -0
- package/dist/mcp/code-intel-server.d.ts.map +1 -1
- package/dist/mcp/code-intel-server.js +9 -5
- package/dist/mcp/code-intel-server.js.map +1 -1
- package/dist/mcp/memory-server.d.ts +195 -1
- package/dist/mcp/memory-server.d.ts.map +1 -1
- package/dist/mcp/memory-server.js +9 -5
- package/dist/mcp/memory-server.js.map +1 -1
- package/dist/mcp/trace-server.d.ts +51 -0
- package/dist/mcp/trace-server.d.ts.map +1 -1
- package/dist/mcp/trace-server.js +9 -5
- package/dist/mcp/trace-server.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +455 -8
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +159 -52
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/codex-native-pre-post.d.ts +5 -0
- package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
- package/dist/scripts/codex-native-pre-post.js +86 -0
- package/dist/scripts/codex-native-pre-post.js.map +1 -1
- package/dist/scripts/notify-hook/operational-events.d.ts.map +1 -1
- package/dist/scripts/notify-hook/operational-events.js +7 -2
- package/dist/scripts/notify-hook/operational-events.js.map +1 -1
- package/dist/state/__tests__/operations-ralph-phase.test.d.ts +2 -0
- package/dist/state/__tests__/operations-ralph-phase.test.d.ts.map +1 -0
- package/dist/state/__tests__/operations-ralph-phase.test.js +82 -0
- package/dist/state/__tests__/operations-ralph-phase.test.js.map +1 -0
- package/dist/state/__tests__/operations.test.d.ts +2 -0
- package/dist/state/__tests__/operations.test.d.ts.map +1 -0
- package/dist/state/__tests__/operations.test.js +200 -0
- package/dist/state/__tests__/operations.test.js.map +1 -0
- package/dist/state/__tests__/path-traversal.test.d.ts +2 -0
- package/dist/state/__tests__/path-traversal.test.d.ts.map +1 -0
- package/dist/state/__tests__/path-traversal.test.js +49 -0
- package/dist/state/__tests__/path-traversal.test.js.map +1 -0
- package/dist/state/operations.d.ts +11 -0
- package/dist/state/operations.d.ts.map +1 -0
- package/dist/state/operations.js +233 -0
- package/dist/state/operations.js.map +1 -0
- package/dist/team/__tests__/api-interop.test.js +24 -2
- package/dist/team/__tests__/api-interop.test.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js +9 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
- package/dist/team/__tests__/runtime-cli.test.js +45 -0
- package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +191 -66
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +33 -0
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js +2 -1
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/runtime-cli.d.ts.map +1 -1
- package/dist/team/runtime-cli.js +21 -2
- package/dist/team/runtime-cli.js.map +1 -1
- package/dist/team/runtime.d.ts +8 -0
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +179 -78
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/state/dispatch.d.ts.map +1 -1
- package/dist/team/state/dispatch.js +9 -0
- package/dist/team/state/dispatch.js.map +1 -1
- package/dist/team/tmux-session.js +3 -3
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worktree.d.ts +2 -0
- package/dist/team/worktree.d.ts.map +1 -1
- package/dist/team/worktree.js +7 -1
- package/dist/team/worktree.js.map +1 -1
- package/dist/utils/__tests__/paths.test.js +76 -1
- package/dist/utils/__tests__/paths.test.js.map +1 -1
- package/dist/utils/paths.d.ts +6 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +14 -0
- package/dist/utils/paths.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +59 -11
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/dist/verification/__tests__/ralph-persistence-gate.test.js +1 -4
- package/dist/verification/__tests__/ralph-persistence-gate.test.js.map +1 -1
- package/package.json +6 -1
- package/src/scripts/__tests__/codex-native-hook.test.ts +600 -8
- package/src/scripts/codex-native-hook.ts +236 -60
- package/src/scripts/codex-native-pre-post.ts +104 -0
- 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 {
|
|
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 {
|
|
10
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -621,22 +620,100 @@ function readPayloadTurnId(payload: CodexHookPayload): string {
|
|
|
621
620
|
|
|
622
621
|
async function isDeepInterviewSuppressedForStop(
|
|
623
622
|
cwd: string,
|
|
624
|
-
stateDir: string,
|
|
625
623
|
sessionId: string,
|
|
624
|
+
threadId: string,
|
|
626
625
|
): Promise<boolean> {
|
|
627
|
-
|
|
628
|
-
if (await isDeepInterviewInputLockActive(stateDir)) return true;
|
|
629
|
-
|
|
630
|
-
const scopedModeState = sessionId
|
|
631
|
-
? await readScopedJsonState("deep-interview-state.json", cwd, sessionId)
|
|
632
|
-
: null;
|
|
626
|
+
const scopedModeState = await readStopSessionPinnedState("deep-interview-state.json", cwd, sessionId);
|
|
633
627
|
if (scopedModeState?.active === true) return true;
|
|
634
628
|
|
|
635
|
-
const
|
|
636
|
-
|
|
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
|
+
))
|
|
637
635
|
: null;
|
|
638
|
-
if (
|
|
639
|
-
|
|
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;
|
|
640
717
|
}
|
|
641
718
|
|
|
642
719
|
function buildRepeatableStopSignature(
|
|
@@ -766,17 +843,8 @@ async function buildSkillStopOutput(
|
|
|
766
843
|
sessionId: string,
|
|
767
844
|
threadId: string,
|
|
768
845
|
): Promise<Record<string, unknown> | null> {
|
|
769
|
-
const
|
|
770
|
-
if (!
|
|
771
|
-
const stateSessionId = safeString(state.session_id).trim();
|
|
772
|
-
const stateThreadId = safeString(state.thread_id).trim();
|
|
773
|
-
if (sessionId && stateSessionId && stateSessionId !== sessionId) return null;
|
|
774
|
-
if (sessionId && !stateSessionId && threadId && stateThreadId && stateThreadId !== threadId) {
|
|
775
|
-
return null;
|
|
776
|
-
}
|
|
777
|
-
const skill = safeString(state.skill).trim();
|
|
778
|
-
const phase = formatPhase(state.phase, "planning");
|
|
779
|
-
if (!SKILL_STOP_BLOCKERS.has(skill) || phase === "completing") return null;
|
|
846
|
+
const blocker = await readBlockingSkillForStop(cwd, sessionId, threadId);
|
|
847
|
+
if (!blocker) return null;
|
|
780
848
|
|
|
781
849
|
const subagentSummary = await readSubagentSessionSummary(cwd, sessionId).catch(() => null);
|
|
782
850
|
if (subagentSummary && subagentSummary.activeSubagentThreadIds.length > 0) {
|
|
@@ -785,12 +853,116 @@ async function buildSkillStopOutput(
|
|
|
785
853
|
|
|
786
854
|
return {
|
|
787
855
|
decision: "block",
|
|
788
|
-
reason: `OMX skill ${skill} is still active (phase: ${phase}); continue until the current ${skill} workflow reaches a terminal state.`,
|
|
789
|
-
stopReason: `skill_${skill}_${phase}`,
|
|
790
|
-
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}).`,
|
|
791
859
|
};
|
|
792
860
|
}
|
|
793
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
|
+
|
|
794
966
|
async function buildStopHookOutput(
|
|
795
967
|
payload: CodexHookPayload,
|
|
796
968
|
cwd: string,
|
|
@@ -844,7 +1016,7 @@ async function buildStopHookOutput(
|
|
|
844
1016
|
if (!stopHookActive && skillOutput) return skillOutput;
|
|
845
1017
|
}
|
|
846
1018
|
|
|
847
|
-
const deepInterviewActive = await isDeepInterviewSuppressedForStop(cwd,
|
|
1019
|
+
const deepInterviewActive = await isDeepInterviewSuppressedForStop(cwd, sessionId, threadId);
|
|
848
1020
|
const lastAssistantMessage = safeString(
|
|
849
1021
|
payload.last_assistant_message ?? payload.lastAssistantMessage,
|
|
850
1022
|
);
|
|
@@ -922,6 +1094,7 @@ export async function dispatchCodexNativeHook(
|
|
|
922
1094
|
turnId,
|
|
923
1095
|
});
|
|
924
1096
|
}
|
|
1097
|
+
await reconcileHudForPromptSubmit(cwd).catch(() => {});
|
|
925
1098
|
}
|
|
926
1099
|
|
|
927
1100
|
if (omxEventName) {
|
|
@@ -954,6 +1127,9 @@ export async function dispatchCodexNativeHook(
|
|
|
954
1127
|
} else if (hookEventName === "PreToolUse") {
|
|
955
1128
|
outputJson = buildNativePreToolUseOutput(payload);
|
|
956
1129
|
} else if (hookEventName === "PostToolUse") {
|
|
1130
|
+
if (detectMcpTransportFailure(payload)) {
|
|
1131
|
+
await markTeamTransportFailure(cwd, payload);
|
|
1132
|
+
}
|
|
957
1133
|
outputJson = buildNativePostToolUseOutput(payload);
|
|
958
1134
|
} else if (hookEventName === "Stop") {
|
|
959
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
|
|
74
|
-
|
|
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 {
|