oh-my-codex 0.12.0 → 0.12.2
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 +7 -0
- package/dist/cli/__tests__/cleanup.test.js +15 -0
- package/dist/cli/__tests__/cleanup.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +147 -2
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +95 -1
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/cleanup.d.ts.map +1 -1
- package/dist/cli/cleanup.js +6 -3
- package/dist/cli/cleanup.js.map +1 -1
- package/dist/cli/index.d.ts +18 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +178 -23
- package/dist/cli/index.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +18 -0
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +156 -1
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +56 -2
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/agents-overlay.d.ts.map +1 -1
- package/dist/hooks/agents-overlay.js +25 -3
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +5 -1
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +104 -6
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hud/__tests__/render.test.js +6 -0
- package/dist/hud/__tests__/render.test.js.map +1 -1
- package/dist/hud/__tests__/state.test.js +55 -1
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/render.d.ts.map +1 -1
- package/dist/hud/render.js +8 -3
- package/dist/hud/render.js.map +1 -1
- package/dist/hud/state.d.ts.map +1 -1
- package/dist/hud/state.js +71 -9
- package/dist/hud/state.js.map +1 -1
- package/dist/hud/types.d.ts +2 -2
- package/dist/hud/types.d.ts.map +1 -1
- package/dist/mcp/__tests__/state-server.test.js +82 -0
- package/dist/mcp/__tests__/state-server.test.js.map +1 -1
- package/dist/mcp/state-server.d.ts +4 -4
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/state-server.js +35 -0
- package/dist/mcp/state-server.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +336 -6
- 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 +221 -23
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/notify-fallback-watcher.js +60 -3
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/state/__tests__/skill-active.test.d.ts +2 -0
- package/dist/state/__tests__/skill-active.test.d.ts.map +1 -0
- package/dist/state/__tests__/skill-active.test.js +84 -0
- package/dist/state/__tests__/skill-active.test.js.map +1 -0
- package/dist/state/skill-active.d.ts +55 -0
- package/dist/state/skill-active.d.ts.map +1 -0
- package/dist/state/skill-active.js +179 -0
- package/dist/state/skill-active.js.map +1 -0
- package/dist/team/__tests__/runtime-cli.test.js +85 -0
- package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +241 -7
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/state.test.js +7 -0
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +143 -0
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/runtime-cli.d.ts +15 -0
- package/dist/team/runtime-cli.d.ts.map +1 -1
- package/dist/team/runtime-cli.js +37 -20
- package/dist/team/runtime-cli.js.map +1 -1
- package/dist/team/runtime.d.ts +1 -0
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +16 -8
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/state/mailbox.d.ts.map +1 -1
- package/dist/team/state/mailbox.js +6 -0
- package/dist/team/state/mailbox.js.map +1 -1
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +32 -4
- package/dist/team/tmux-session.js.map +1 -1
- package/package.json +1 -1
- package/prompts/information-architect.md +15 -102
- package/src/scripts/__tests__/codex-native-hook.test.ts +421 -6
- package/src/scripts/codex-native-hook.ts +287 -25
- package/src/scripts/notify-fallback-watcher.ts +62 -5
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "child_process";
|
|
2
2
|
import { existsSync, readFileSync } from "fs";
|
|
3
3
|
import { mkdir, readFile, readdir, writeFile } from "fs/promises";
|
|
4
|
-
import { join } from "path";
|
|
4
|
+
import { join, resolve } from "path";
|
|
5
5
|
import { readModeState } from "../modes/base.js";
|
|
6
6
|
import { getReadScopedStateDirs } from "../mcp/state-paths.js";
|
|
7
7
|
import { readSubagentSessionSummary } from "../subagents/tracker.js";
|
|
@@ -15,8 +15,10 @@ import {
|
|
|
15
15
|
} from "../hooks/keyword-detector.js";
|
|
16
16
|
import {
|
|
17
17
|
detectStallPattern,
|
|
18
|
+
isDeepInterviewInputLockActive,
|
|
18
19
|
isDeepInterviewStateActive,
|
|
19
20
|
loadAutoNudgeConfig,
|
|
21
|
+
normalizeAutoNudgeSignatureText,
|
|
20
22
|
} from "./notify-hook/auto-nudge.js";
|
|
21
23
|
import {
|
|
22
24
|
buildNativePostToolUseOutput,
|
|
@@ -53,6 +55,8 @@ export interface NativeHookDispatchResult {
|
|
|
53
55
|
const TERMINAL_RALPH_PHASES = new Set(["complete", "failed", "cancelled"]);
|
|
54
56
|
const TERMINAL_MODE_PHASES = new Set(["complete", "failed", "cancelled"]);
|
|
55
57
|
const SKILL_STOP_BLOCKERS = new Set(["ralplan", "deep-interview"]);
|
|
58
|
+
const TEAM_TERMINAL_TASK_STATUSES = new Set(["completed", "failed"]);
|
|
59
|
+
const NATIVE_STOP_STATE_FILE = "native-stop-state.json";
|
|
56
60
|
|
|
57
61
|
function safeString(value: unknown): string {
|
|
58
62
|
return typeof value === "string" ? value : "";
|
|
@@ -420,14 +424,118 @@ async function buildSessionStartContext(
|
|
|
420
424
|
return sections.join("\n\n");
|
|
421
425
|
}
|
|
422
426
|
|
|
423
|
-
function buildAdditionalContextMessage(prompt: string): string | null {
|
|
427
|
+
function buildAdditionalContextMessage(prompt: string, skillState?: SkillActiveState | null): string | null {
|
|
424
428
|
if (!prompt) return null;
|
|
425
429
|
const match = detectPrimaryKeyword(prompt);
|
|
426
430
|
if (!match) return null;
|
|
427
431
|
|
|
432
|
+
if (skillState?.initialized_mode && skillState.initialized_state_path) {
|
|
433
|
+
return [
|
|
434
|
+
`OMX native UserPromptSubmit detected workflow keyword "${match.keyword}" -> ${match.skill}.`,
|
|
435
|
+
`skill: ${skillState.initialized_mode} activated and initial state initialized at ${skillState.initialized_state_path}; write subsequent updates via omx_state MCP.`,
|
|
436
|
+
"Follow AGENTS.md routing and preserve ralplan/ralph execution gates.",
|
|
437
|
+
].join(" ");
|
|
438
|
+
}
|
|
439
|
+
|
|
428
440
|
return `OMX native UserPromptSubmit detected workflow keyword "${match.keyword}" -> ${match.skill}. Follow AGENTS.md routing and preserve ralplan/ralph execution gates.`;
|
|
429
441
|
}
|
|
430
442
|
|
|
443
|
+
function parseTeamWorkerEnv(rawValue: string): { teamName: string; workerName: string } | null {
|
|
444
|
+
const match = /^([a-z0-9][a-z0-9-]{0,29})\/(worker-\d+)$/.exec(rawValue.trim());
|
|
445
|
+
if (!match) return null;
|
|
446
|
+
return {
|
|
447
|
+
teamName: match[1] || "",
|
|
448
|
+
workerName: match[2] || "",
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function readTeamStateRootFromJson(path: string): Promise<string | null> {
|
|
453
|
+
const parsed = await readJsonIfExists(path);
|
|
454
|
+
const value = safeString(parsed?.team_state_root).trim();
|
|
455
|
+
return value || null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function resolveTeamStateDirForWorkerContext(
|
|
459
|
+
cwd: string,
|
|
460
|
+
workerContext: { teamName: string; workerName: string },
|
|
461
|
+
): Promise<string> {
|
|
462
|
+
const explicitStateRoot = safeString(process.env.OMX_TEAM_STATE_ROOT).trim();
|
|
463
|
+
if (explicitStateRoot) {
|
|
464
|
+
return resolve(cwd, explicitStateRoot);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const leaderCwd = safeString(process.env.OMX_TEAM_LEADER_CWD).trim();
|
|
468
|
+
const candidateStateDirs = [
|
|
469
|
+
...(leaderCwd ? [join(resolve(leaderCwd), ".omx", "state")] : []),
|
|
470
|
+
join(cwd, ".omx", "state"),
|
|
471
|
+
];
|
|
472
|
+
|
|
473
|
+
for (const candidateStateDir of candidateStateDirs) {
|
|
474
|
+
const teamRoot = join(candidateStateDir, "team", workerContext.teamName);
|
|
475
|
+
if (!existsSync(teamRoot)) continue;
|
|
476
|
+
|
|
477
|
+
const identityRoot = await readTeamStateRootFromJson(
|
|
478
|
+
join(teamRoot, "workers", workerContext.workerName, "identity.json"),
|
|
479
|
+
);
|
|
480
|
+
if (identityRoot) return resolve(cwd, identityRoot);
|
|
481
|
+
|
|
482
|
+
const manifestRoot = await readTeamStateRootFromJson(join(teamRoot, "manifest.v2.json"));
|
|
483
|
+
if (manifestRoot) return resolve(cwd, manifestRoot);
|
|
484
|
+
|
|
485
|
+
const configRoot = await readTeamStateRootFromJson(join(teamRoot, "config.json"));
|
|
486
|
+
if (configRoot) return resolve(cwd, configRoot);
|
|
487
|
+
|
|
488
|
+
return candidateStateDir;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return join(cwd, ".omx", "state");
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function buildTeamWorkerStopOutput(
|
|
495
|
+
cwd: string,
|
|
496
|
+
): Promise<Record<string, unknown> | null> {
|
|
497
|
+
const workerContext = parseTeamWorkerEnv(safeString(process.env.OMX_TEAM_WORKER));
|
|
498
|
+
if (!workerContext) return null;
|
|
499
|
+
|
|
500
|
+
const stateDir = await resolveTeamStateDirForWorkerContext(cwd, workerContext);
|
|
501
|
+
const workerRoot = join(stateDir, "team", workerContext.teamName, "workers", workerContext.workerName);
|
|
502
|
+
const [identity, status] = await Promise.all([
|
|
503
|
+
readJsonIfExists(join(workerRoot, "identity.json")),
|
|
504
|
+
readJsonIfExists(join(workerRoot, "status.json")),
|
|
505
|
+
]);
|
|
506
|
+
|
|
507
|
+
const candidateTaskIds = new Set<string>();
|
|
508
|
+
const currentTaskId = safeString(status?.current_task_id).trim();
|
|
509
|
+
if (currentTaskId) candidateTaskIds.add(currentTaskId);
|
|
510
|
+
const assignedTasks = Array.isArray(identity?.assigned_tasks) ? identity?.assigned_tasks : [];
|
|
511
|
+
for (const taskId of assignedTasks) {
|
|
512
|
+
const normalized = safeString(taskId).trim();
|
|
513
|
+
if (normalized) candidateTaskIds.add(normalized);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
for (const taskId of candidateTaskIds) {
|
|
517
|
+
const task = await readJsonIfExists(
|
|
518
|
+
join(stateDir, "team", workerContext.teamName, "tasks", `task-${taskId}.json`),
|
|
519
|
+
);
|
|
520
|
+
const statusValue = safeString(task?.status).trim().toLowerCase();
|
|
521
|
+
if (!statusValue || TEAM_TERMINAL_TASK_STATUSES.has(statusValue)) continue;
|
|
522
|
+
return {
|
|
523
|
+
decision: "block",
|
|
524
|
+
reason:
|
|
525
|
+
`OMX team worker ${workerContext.workerName} is still assigned non-terminal task ${taskId} (${statusValue}); continue the current assigned task or report a concrete blocker before stopping.`,
|
|
526
|
+
stopReason: `team_worker_${workerContext.workerName}_${taskId}_${statusValue}`,
|
|
527
|
+
systemMessage:
|
|
528
|
+
`OMX team worker ${workerContext.workerName} is still assigned task ${taskId} (${statusValue}).`,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function hasTeamWorkerContext(): boolean {
|
|
536
|
+
return parseTeamWorkerEnv(safeString(process.env.OMX_TEAM_WORKER)) !== null;
|
|
537
|
+
}
|
|
538
|
+
|
|
431
539
|
function isStopExempt(payload: CodexHookPayload): boolean {
|
|
432
540
|
const candidates = [
|
|
433
541
|
payload.stop_reason,
|
|
@@ -469,15 +577,145 @@ async function buildTeamStopOutput(cwd: string): Promise<Record<string, unknown>
|
|
|
469
577
|
const coarsePhase = teamState.current_phase;
|
|
470
578
|
const canonicalPhase = teamName ? (await readTeamPhase(teamName, cwd))?.current_phase ?? coarsePhase : coarsePhase;
|
|
471
579
|
if (!isNonTerminalPhase(canonicalPhase)) return null;
|
|
472
|
-
|
|
580
|
+
return buildTeamStopOutputForPhase(teamName, formatPhase(canonicalPhase));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function buildTeamStopReason(teamName: string, phase: string): string {
|
|
584
|
+
const teamContext = teamName ? ` (${teamName})` : "";
|
|
585
|
+
return `OMX team pipeline is still active${teamContext} at phase ${phase}; continue coordinating until the team reaches a terminal phase. If system-generated worker auto-checkpoint commits exist, rewrite them into Lore-format final commits before merge/finalization.`;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function buildTeamStopOutputForPhase(teamName: string, phase: string): Record<string, unknown> {
|
|
473
589
|
return {
|
|
474
590
|
decision: "block",
|
|
475
|
-
reason:
|
|
591
|
+
reason: buildTeamStopReason(teamName, phase),
|
|
476
592
|
stopReason: `team_${phase}`,
|
|
477
593
|
systemMessage: `OMX team pipeline is still active at phase ${phase}.`,
|
|
478
594
|
};
|
|
479
595
|
}
|
|
480
596
|
|
|
597
|
+
function readPayloadSessionId(payload: CodexHookPayload): string {
|
|
598
|
+
return safeString(payload.session_id ?? payload.sessionId).trim();
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function readPayloadThreadId(payload: CodexHookPayload): string {
|
|
602
|
+
return safeString(payload.thread_id ?? payload.threadId).trim();
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function readPayloadTurnId(payload: CodexHookPayload): string {
|
|
606
|
+
return safeString(payload.turn_id ?? payload.turnId).trim();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async function isDeepInterviewSuppressedForStop(
|
|
610
|
+
cwd: string,
|
|
611
|
+
stateDir: string,
|
|
612
|
+
sessionId: string,
|
|
613
|
+
): 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;
|
|
620
|
+
if (scopedModeState?.active === true) return true;
|
|
621
|
+
|
|
622
|
+
const scopedSkillState = sessionId
|
|
623
|
+
? await readScopedJsonState("skill-active-state.json", cwd, sessionId)
|
|
624
|
+
: null;
|
|
625
|
+
if (!scopedSkillState || scopedSkillState.active !== true) return false;
|
|
626
|
+
return safeString(scopedSkillState.skill).trim() === "deep-interview";
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function buildRepeatableStopSignature(
|
|
630
|
+
payload: CodexHookPayload,
|
|
631
|
+
kind: string,
|
|
632
|
+
detail = "",
|
|
633
|
+
): string {
|
|
634
|
+
const sessionId = readPayloadSessionId(payload) || "no-session";
|
|
635
|
+
const threadId = readPayloadThreadId(payload) || "no-thread";
|
|
636
|
+
const turnId = readPayloadTurnId(payload);
|
|
637
|
+
const normalizedDetail = normalizeAutoNudgeSignatureText(detail) || safeString(detail).trim().toLowerCase();
|
|
638
|
+
const transcriptPath = safeString(payload.transcript_path ?? payload.transcriptPath).trim() || "no-transcript";
|
|
639
|
+
const lastAssistantMessage = normalizeAutoNudgeSignatureText(
|
|
640
|
+
payload.last_assistant_message ?? payload.lastAssistantMessage,
|
|
641
|
+
) || "no-message";
|
|
642
|
+
if (turnId) {
|
|
643
|
+
return [
|
|
644
|
+
kind,
|
|
645
|
+
sessionId,
|
|
646
|
+
threadId,
|
|
647
|
+
turnId,
|
|
648
|
+
transcriptPath,
|
|
649
|
+
lastAssistantMessage,
|
|
650
|
+
normalizedDetail || "no-detail",
|
|
651
|
+
].join("|");
|
|
652
|
+
}
|
|
653
|
+
return [
|
|
654
|
+
kind,
|
|
655
|
+
sessionId,
|
|
656
|
+
threadId,
|
|
657
|
+
transcriptPath,
|
|
658
|
+
lastAssistantMessage,
|
|
659
|
+
normalizedDetail || "no-detail",
|
|
660
|
+
].join("|");
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async function readNativeStopState(stateDir: string): Promise<Record<string, unknown>> {
|
|
664
|
+
return await readJsonIfExists(join(stateDir, NATIVE_STOP_STATE_FILE)) ?? {};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function readNativeStopSessionKey(payload: CodexHookPayload): string {
|
|
668
|
+
return readPayloadSessionId(payload) || readPayloadThreadId(payload) || "global";
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function readPreviousNativeStopSignature(
|
|
672
|
+
state: Record<string, unknown>,
|
|
673
|
+
sessionKey: string,
|
|
674
|
+
): string {
|
|
675
|
+
const sessions = safeObject(state.sessions);
|
|
676
|
+
const sessionState = safeObject(sessions[sessionKey]);
|
|
677
|
+
return safeString(sessionState.last_signature).trim();
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async function persistNativeStopSignature(
|
|
681
|
+
stateDir: string,
|
|
682
|
+
payload: CodexHookPayload,
|
|
683
|
+
signature: string,
|
|
684
|
+
): Promise<void> {
|
|
685
|
+
if (!signature) return;
|
|
686
|
+
const state = await readNativeStopState(stateDir);
|
|
687
|
+
const sessions = safeObject(state.sessions);
|
|
688
|
+
const sessionKey = readNativeStopSessionKey(payload);
|
|
689
|
+
sessions[sessionKey] = {
|
|
690
|
+
...safeObject(sessions[sessionKey]),
|
|
691
|
+
last_signature: signature,
|
|
692
|
+
updated_at: new Date().toISOString(),
|
|
693
|
+
};
|
|
694
|
+
await writeFile(join(stateDir, NATIVE_STOP_STATE_FILE), JSON.stringify({
|
|
695
|
+
...state,
|
|
696
|
+
sessions,
|
|
697
|
+
}, null, 2));
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
async function maybeReturnRepeatableStopOutput(
|
|
701
|
+
payload: CodexHookPayload,
|
|
702
|
+
stateDir: string,
|
|
703
|
+
signature: string,
|
|
704
|
+
output: Record<string, unknown> | null,
|
|
705
|
+
): Promise<Record<string, unknown> | null> {
|
|
706
|
+
if (!output) return null;
|
|
707
|
+
const stopHookActive = payload.stop_hook_active === true || payload.stopHookActive === true;
|
|
708
|
+
if (stopHookActive) {
|
|
709
|
+
const state = await readNativeStopState(stateDir);
|
|
710
|
+
const previousSignature = readPreviousNativeStopSignature(state, readNativeStopSessionKey(payload));
|
|
711
|
+
if (!signature || previousSignature === signature) {
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
await persistNativeStopSignature(stateDir, payload, signature);
|
|
716
|
+
return output;
|
|
717
|
+
}
|
|
718
|
+
|
|
481
719
|
async function findCanonicalActiveTeamForSession(
|
|
482
720
|
cwd: string,
|
|
483
721
|
sessionId: string,
|
|
@@ -513,9 +751,16 @@ async function findCanonicalActiveTeamForSession(
|
|
|
513
751
|
async function buildSkillStopOutput(
|
|
514
752
|
cwd: string,
|
|
515
753
|
sessionId: string,
|
|
754
|
+
threadId: string,
|
|
516
755
|
): Promise<Record<string, unknown> | null> {
|
|
517
756
|
const state = await readScopedJsonState("skill-active-state.json", cwd, sessionId);
|
|
518
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
|
+
}
|
|
519
764
|
const skill = safeString(state.skill).trim();
|
|
520
765
|
const phase = formatPhase(state.phase, "planning");
|
|
521
766
|
if (!SKILL_STOP_BLOCKERS.has(skill) || phase === "completing") return null;
|
|
@@ -542,10 +787,14 @@ async function buildStopHookOutput(
|
|
|
542
787
|
return null;
|
|
543
788
|
}
|
|
544
789
|
|
|
545
|
-
const sessionId =
|
|
790
|
+
const sessionId = readPayloadSessionId(payload);
|
|
791
|
+
const threadId = readPayloadThreadId(payload);
|
|
546
792
|
const ralphState = await readActiveRalphState(stateDir);
|
|
547
793
|
const stopHookActive = payload.stop_hook_active === true || payload.stopHookActive === true;
|
|
548
794
|
if (!ralphState) {
|
|
795
|
+
const teamWorkerOutput = await buildTeamWorkerStopOutput(cwd);
|
|
796
|
+
if (!stopHookActive && hasTeamWorkerContext()) return teamWorkerOutput;
|
|
797
|
+
|
|
549
798
|
const autopilotOutput = await buildModeBasedStopOutput("autopilot", cwd);
|
|
550
799
|
if (!stopHookActive && autopilotOutput) return autopilotOutput;
|
|
551
800
|
|
|
@@ -556,42 +805,55 @@ async function buildStopHookOutput(
|
|
|
556
805
|
if (!stopHookActive && ultraqaOutput) return ultraqaOutput;
|
|
557
806
|
|
|
558
807
|
const teamOutput = await buildTeamStopOutput(cwd);
|
|
559
|
-
if (
|
|
808
|
+
if (teamOutput) {
|
|
809
|
+
const teamSignature = buildRepeatableStopSignature(payload, "team-stop", safeString(teamOutput.stopReason));
|
|
810
|
+
return await maybeReturnRepeatableStopOutput(payload, stateDir, teamSignature, teamOutput);
|
|
811
|
+
}
|
|
560
812
|
|
|
561
813
|
if (sessionId) {
|
|
562
814
|
const canonicalTeam = await findCanonicalActiveTeamForSession(cwd, sessionId);
|
|
563
|
-
if (
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
815
|
+
if (canonicalTeam) {
|
|
816
|
+
const canonicalTeamOutput = buildTeamStopOutputForPhase(
|
|
817
|
+
canonicalTeam.teamName,
|
|
818
|
+
canonicalTeam.phase,
|
|
819
|
+
);
|
|
820
|
+
const canonicalTeamSignature = buildRepeatableStopSignature(payload, "team-stop", `${canonicalTeam.teamName}|${canonicalTeam.phase}`);
|
|
821
|
+
const repeatedCanonicalTeamOutput = await maybeReturnRepeatableStopOutput(
|
|
822
|
+
payload,
|
|
823
|
+
stateDir,
|
|
824
|
+
canonicalTeamSignature,
|
|
825
|
+
canonicalTeamOutput,
|
|
826
|
+
);
|
|
827
|
+
if (repeatedCanonicalTeamOutput) return repeatedCanonicalTeamOutput;
|
|
570
828
|
}
|
|
571
829
|
|
|
572
|
-
const skillOutput = await buildSkillStopOutput(cwd, sessionId);
|
|
830
|
+
const skillOutput = await buildSkillStopOutput(cwd, sessionId, threadId);
|
|
573
831
|
if (!stopHookActive && skillOutput) return skillOutput;
|
|
574
832
|
}
|
|
575
833
|
|
|
576
|
-
const deepInterviewActive = await
|
|
834
|
+
const deepInterviewActive = await isDeepInterviewSuppressedForStop(cwd, stateDir, sessionId);
|
|
577
835
|
const lastAssistantMessage = safeString(
|
|
578
836
|
payload.last_assistant_message ?? payload.lastAssistantMessage,
|
|
579
837
|
);
|
|
580
838
|
const autoNudgeConfig = await loadAutoNudgeConfig();
|
|
581
839
|
|
|
582
840
|
if (
|
|
583
|
-
!
|
|
584
|
-
&& !deepInterviewActive
|
|
841
|
+
!deepInterviewActive
|
|
585
842
|
&& autoNudgeConfig.enabled
|
|
586
843
|
&& detectStallPattern(lastAssistantMessage, autoNudgeConfig.patterns)
|
|
587
844
|
) {
|
|
588
|
-
return
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
845
|
+
return await maybeReturnRepeatableStopOutput(
|
|
846
|
+
payload,
|
|
847
|
+
stateDir,
|
|
848
|
+
buildRepeatableStopSignature(payload, "auto-nudge", lastAssistantMessage),
|
|
849
|
+
{
|
|
850
|
+
decision: "block",
|
|
851
|
+
reason: autoNudgeConfig.response,
|
|
852
|
+
stopReason: "auto_nudge",
|
|
853
|
+
systemMessage:
|
|
854
|
+
"OMX native Stop detected a stall/permission-style handoff and continued the turn automatically.",
|
|
855
|
+
},
|
|
856
|
+
);
|
|
595
857
|
}
|
|
596
858
|
|
|
597
859
|
return null;
|
|
@@ -667,7 +929,7 @@ export async function dispatchCodexNativeHook(
|
|
|
667
929
|
if (hookEventName === "SessionStart" || hookEventName === "UserPromptSubmit") {
|
|
668
930
|
const additionalContext = hookEventName === "SessionStart"
|
|
669
931
|
? await buildSessionStartContext(cwd, sessionId)
|
|
670
|
-
: buildAdditionalContextMessage(readPromptText(payload));
|
|
932
|
+
: buildAdditionalContextMessage(readPromptText(payload), skillState);
|
|
671
933
|
if (additionalContext) {
|
|
672
934
|
outputJson = {
|
|
673
935
|
hookSpecificOutput: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
|
-
import { appendFile, mkdir, open, readFile, readdir, rename, stat, unlink, writeFile } from 'fs/promises';
|
|
4
|
+
import { appendFile, mkdir, open, readFile, readdir, rename, rm, stat, unlink, writeFile } from 'fs/promises';
|
|
5
5
|
import { spawnSync } from 'child_process';
|
|
6
6
|
import { dirname, join, resolve } from 'path';
|
|
7
7
|
import { homedir } from 'os';
|
|
@@ -110,6 +110,13 @@ const stateDir = join(omxDir, 'state');
|
|
|
110
110
|
const statePath = join(stateDir, 'notify-fallback-state.json');
|
|
111
111
|
const pidFilePath = resolve(argValue('--pid-file', join(stateDir, 'notify-fallback.pid')));
|
|
112
112
|
const logPath = join(logsDir, `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
|
|
113
|
+
const logRotatePath = `${logPath}.1`;
|
|
114
|
+
const logLockPath = `${logPath}.lock`;
|
|
115
|
+
const defaultMaxLogBytes = 10 * 1024 * 1024;
|
|
116
|
+
const maxLogBytes = Math.max(
|
|
117
|
+
0,
|
|
118
|
+
asNumber(argValue('--log-max-bytes', process.env.OMX_NOTIFY_FALLBACK_LOG_MAX_BYTES || String(defaultMaxLogBytes)), defaultMaxLogBytes),
|
|
119
|
+
);
|
|
113
120
|
const ralphSteerTimestampPath = join(stateDir, 'ralph-last-steer-at');
|
|
114
121
|
const ralphSteerLockPath = join(stateDir, 'ralph-continue-steer.lock');
|
|
115
122
|
const watcherOwnerToken = `${process.pid}-${startedAt}-${Math.random().toString(36).slice(2, 10)}`;
|
|
@@ -117,6 +124,7 @@ const RALPH_CONTINUE_TEXT = 'Ralph loop active continue';
|
|
|
117
124
|
const RALPH_CONTINUE_CADENCE_MS = 60_000;
|
|
118
125
|
const RALPH_STEER_LOCK_STALE_MS = 30_000;
|
|
119
126
|
const RALPH_TERMINAL_PHASES = new Set(['complete', 'failed', 'cancelled']);
|
|
127
|
+
const QUIET_ONCE_EVENT_TYPES = new Set(['watcher_start', 'watcher_once_complete']);
|
|
120
128
|
|
|
121
129
|
interface WatcherFileMeta {
|
|
122
130
|
threadId: string;
|
|
@@ -311,8 +319,57 @@ let adaptivePollState: AdaptivePollState = {
|
|
|
311
319
|
last_activity_at: null,
|
|
312
320
|
last_activity_reason: 'init',
|
|
313
321
|
};
|
|
314
|
-
|
|
315
|
-
|
|
322
|
+
|
|
323
|
+
function shouldSuppressEventLog(event: Record<string, unknown>): boolean {
|
|
324
|
+
const eventType = safeString(event.type).trim();
|
|
325
|
+
return runOnce && QUIET_ONCE_EVENT_TYPES.has(eventType);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function acquireLogLock(timeoutMs = 1000): Promise<boolean> {
|
|
329
|
+
const deadline = Date.now() + timeoutMs;
|
|
330
|
+
while (Date.now() < deadline) {
|
|
331
|
+
try {
|
|
332
|
+
await mkdir(logLockPath, { recursive: false });
|
|
333
|
+
return true;
|
|
334
|
+
} catch (error) {
|
|
335
|
+
if ((error as NodeJS.ErrnoException | null)?.code !== 'EEXIST') return false;
|
|
336
|
+
const lockStat = await stat(logLockPath).catch(() => null);
|
|
337
|
+
if (lockStat && Date.now() - lockStat.mtimeMs > 5000) {
|
|
338
|
+
await rm(logLockPath, { recursive: true, force: true }).catch(() => {});
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
await sleep(10);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function releaseLogLock(): Promise<void> {
|
|
348
|
+
await rm(logLockPath, { recursive: true, force: true }).catch(() => {});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function rotateLogIfNeeded(nextEntryBytes: number): Promise<void> {
|
|
352
|
+
if (maxLogBytes <= 0) return;
|
|
353
|
+
const currentStat = await stat(logPath).catch(() => null);
|
|
354
|
+
if (!currentStat || currentStat.size + nextEntryBytes <= maxLogBytes) return;
|
|
355
|
+
await unlink(logRotatePath).catch(() => {});
|
|
356
|
+
await rename(logPath, logRotatePath).catch(() => {});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function eventLog(event: Record<string, unknown>): Promise<void> {
|
|
360
|
+
if (shouldSuppressEventLog(event)) return;
|
|
361
|
+
const line = `${JSON.stringify({ timestamp: new Date().toISOString(), ...event })}\n`;
|
|
362
|
+
await mkdir(dirname(logPath), { recursive: true }).catch(() => {});
|
|
363
|
+
const locked = await acquireLogLock();
|
|
364
|
+
if (!locked) return;
|
|
365
|
+
try {
|
|
366
|
+
await rotateLogIfNeeded(Buffer.byteLength(line));
|
|
367
|
+
await appendFile(logPath, line);
|
|
368
|
+
} catch {
|
|
369
|
+
// best effort only
|
|
370
|
+
} finally {
|
|
371
|
+
await releaseLogLock();
|
|
372
|
+
}
|
|
316
373
|
}
|
|
317
374
|
|
|
318
375
|
function shouldLogLeaderNudgeTick(reason: string): boolean {
|
|
@@ -1669,8 +1726,8 @@ async function main(): Promise<void> {
|
|
|
1669
1726
|
notify_script: notifyScript,
|
|
1670
1727
|
authority_only: authorityOnly,
|
|
1671
1728
|
poll_ms: pollMs,
|
|
1672
|
-
|
|
1673
|
-
|
|
1729
|
+
effective_poll_ms: adaptivePollState.current_ms,
|
|
1730
|
+
idle_max_poll_ms: idleMaxPollMs,
|
|
1674
1731
|
once: runOnce,
|
|
1675
1732
|
parent_pid: parentPid,
|
|
1676
1733
|
pid_file: runOnce ? null : pidFilePath,
|