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.
- 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 +62 -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 +20 -8
- 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 +481 -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 +171 -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 +227 -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 +203 -85
- 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 +636 -8
- package/src/scripts/codex-native-hook.ts +249 -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;
|
|
@@ -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
|
-
|
|
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
|
|
623
|
-
|
|
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 (
|
|
626
|
-
|
|
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
|
|
757
|
-
if (!
|
|
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,
|
|
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
|
|
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 {
|