oh-my-codex 0.18.13 → 0.18.14
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 +6 -6
- package/Cargo.toml +1 -1
- package/README.md +31 -11
- package/dist/agents/__tests__/native-config.test.js +25 -0
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/native-config.d.ts.map +1 -1
- package/dist/agents/native-config.js +5 -1
- package/dist/agents/native-config.js.map +1 -1
- package/dist/autopilot/__tests__/fsm.test.js +7 -0
- package/dist/autopilot/__tests__/fsm.test.js.map +1 -1
- package/dist/autopilot/__tests__/planner-routing.test.d.ts +2 -0
- package/dist/autopilot/__tests__/planner-routing.test.d.ts.map +1 -0
- package/dist/autopilot/__tests__/planner-routing.test.js +74 -0
- package/dist/autopilot/__tests__/planner-routing.test.js.map +1 -0
- package/dist/autopilot/__tests__/ralplan-gate.test.js +315 -0
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
- package/dist/autopilot/completion-gate.js +1 -1
- package/dist/autopilot/completion-gate.js.map +1 -1
- package/dist/autopilot/fsm.d.ts +2 -2
- package/dist/autopilot/fsm.d.ts.map +1 -1
- package/dist/autopilot/fsm.js +3 -0
- package/dist/autopilot/fsm.js.map +1 -1
- package/dist/autopilot/planner-routing.d.ts +21 -0
- package/dist/autopilot/planner-routing.d.ts.map +1 -0
- package/dist/autopilot/planner-routing.js +59 -0
- package/dist/autopilot/planner-routing.js.map +1 -0
- package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
- package/dist/autopilot/ralplan-gate.js +30 -0
- package/dist/autopilot/ralplan-gate.js.map +1 -1
- package/dist/autoresearch/goal.d.ts.map +1 -1
- package/dist/autoresearch/goal.js +10 -1
- package/dist/autoresearch/goal.js.map +1 -1
- package/dist/cli/__tests__/autoresearch-goal.test.js +33 -0
- package/dist/cli/__tests__/autoresearch-goal.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +30 -0
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-artifact-ownership.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-artifact-ownership.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-artifact-ownership.test.js +158 -0
- package/dist/cli/__tests__/doctor-artifact-ownership.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-spark-routing.test.js +13 -0
- package/dist/cli/__tests__/doctor-spark-routing.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +1 -0
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +5 -0
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/nested-help-routing.test.js +4 -0
- package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
- package/dist/cli/__tests__/package-bin-contract.test.js +7 -1
- package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
- package/dist/cli/__tests__/performance-goal.test.js +22 -0
- package/dist/cli/__tests__/performance-goal.test.js.map +1 -1
- package/dist/cli/__tests__/resume.test.js +96 -0
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +162 -0
- package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
- package/dist/cli/__tests__/session-search.test.js +36 -0
- package/dist/cli/__tests__/session-search.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +35 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +33 -0
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +17 -3
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +37 -0
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +1 -0
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/autoresearch-goal.d.ts.map +1 -1
- package/dist/cli/autoresearch-goal.js +2 -1
- package/dist/cli/autoresearch-goal.js.map +1 -1
- package/dist/cli/doctor.d.ts +22 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +263 -4
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +65 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/performance-goal.d.ts.map +1 -1
- package/dist/cli/performance-goal.js +4 -2
- package/dist/cli/performance-goal.js.map +1 -1
- package/dist/cli/plugin-marketplace.d.ts +1 -0
- package/dist/cli/plugin-marketplace.d.ts.map +1 -1
- package/dist/cli/plugin-marketplace.js +67 -5
- package/dist/cli/plugin-marketplace.js.map +1 -1
- package/dist/cli/project-runtime-codex-homes.d.ts +2 -0
- package/dist/cli/project-runtime-codex-homes.d.ts.map +1 -1
- package/dist/cli/project-runtime-codex-homes.js +112 -5
- package/dist/cli/project-runtime-codex-homes.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +15 -6
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +5 -1
- package/dist/cli/ultragoal.js.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +3 -0
- package/dist/cli/update.js.map +1 -1
- package/dist/config/__tests__/models.test.js +21 -1
- package/dist/config/__tests__/models.test.js.map +1 -1
- package/dist/config/models.d.ts +5 -0
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +23 -0
- package/dist/config/models.js.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts +2 -0
- package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.js +10 -0
- package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +7 -0
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +46 -0
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +100 -0
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +75 -0
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +225 -0
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +75 -0
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +200 -0
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +187 -12
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +225 -0
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +0 -0
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +75 -0
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +4 -0
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +137 -2
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/state.test.js +107 -0
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +17 -0
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/reconcile.d.ts.map +1 -1
- package/dist/hud/reconcile.js +21 -7
- package/dist/hud/reconcile.js.map +1 -1
- package/dist/hud/render.d.ts.map +1 -1
- package/dist/hud/render.js +9 -0
- package/dist/hud/render.js.map +1 -1
- package/dist/hud/state.d.ts.map +1 -1
- package/dist/hud/state.js +43 -5
- package/dist/hud/state.js.map +1 -1
- package/dist/hud/types.d.ts +6 -0
- package/dist/hud/types.d.ts.map +1 -1
- package/dist/hud/types.js.map +1 -1
- package/dist/modes/__tests__/base-autopilot-gates.test.js +48 -0
- package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +1 -0
- package/dist/modes/base.js.map +1 -1
- package/dist/performance-goal/artifacts.d.ts.map +1 -1
- package/dist/performance-goal/artifacts.js +10 -1
- package/dist/performance-goal/artifacts.js.map +1 -1
- package/dist/ralplan/__tests__/consensus-gate.test.js +112 -0
- package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts +23 -0
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +133 -21
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +831 -2
- 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 +486 -21
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js +87 -8
- package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/run-compiled-ci.d.ts +2 -0
- package/dist/scripts/run-compiled-ci.d.ts.map +1 -0
- package/dist/scripts/run-compiled-ci.js +77 -0
- package/dist/scripts/run-compiled-ci.js.map +1 -0
- package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js +1 -0
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/session-history/search.d.ts.map +1 -1
- package/dist/session-history/search.js +24 -9
- package/dist/session-history/search.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +107 -3
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +57 -2
- package/dist/state/operations.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js +22 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +57 -1
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +1468 -110
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +63 -0
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/state.test.js +24 -1
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +512 -0
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/worker-bootstrap.test.js +42 -9
- package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
- package/dist/team/model-contract.d.ts +16 -0
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/model-contract.js +41 -1
- package/dist/team/model-contract.js.map +1 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +116 -30
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +9 -1
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/types.d.ts +2 -0
- package/dist/team/state/types.d.ts.map +1 -1
- package/dist/team/state/types.js.map +1 -1
- package/dist/team/state.d.ts +3 -0
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +20 -0
- package/dist/team/state.js.map +1 -1
- package/dist/team/tmux-session.d.ts +38 -3
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +275 -36
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worker-bootstrap.d.ts.map +1 -1
- package/dist/team/worker-bootstrap.js +26 -1
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js +238 -0
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js +3 -0
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts +15 -0
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +167 -4
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/utils/__tests__/agents-md.test.js +39 -1
- package/dist/utils/__tests__/agents-md.test.js.map +1 -1
- package/dist/utils/__tests__/agents-model-table.test.js +28 -0
- package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
- package/dist/utils/agents-md.d.ts +4 -0
- package/dist/utils/agents-md.d.ts.map +1 -1
- package/dist/utils/agents-md.js +25 -0
- package/dist/utils/agents-md.js.map +1 -1
- package/dist/utils/agents-model-table.d.ts +7 -3
- package/dist/utils/agents-model-table.d.ts.map +1 -1
- package/dist/utils/agents-model-table.js +16 -8
- package/dist/utils/agents-model-table.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +4 -0
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/package.json +2 -2
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +59 -8
- package/plugins/oh-my-codex/skills/autopilot/SKILL.md +9 -5
- package/plugins/oh-my-codex/skills/autoresearch-goal/SKILL.md +3 -0
- package/plugins/oh-my-codex/skills/performance-goal/SKILL.md +3 -0
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +22 -4
- package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +2 -2
- package/plugins/oh-my-codex/skills/ultrawork/references/agent-tiers.md +61 -0
- package/skills/autopilot/SKILL.md +9 -5
- package/skills/autoresearch-goal/SKILL.md +3 -0
- package/skills/ecomode/SKILL.md +1 -1
- package/skills/ecomode/references/agent-tiers.md +61 -0
- package/skills/performance-goal/SKILL.md +3 -0
- package/skills/ralplan/SKILL.md +1 -1
- package/skills/ultragoal/SKILL.md +22 -4
- package/skills/ultrawork/SKILL.md +2 -2
- package/skills/ultrawork/references/agent-tiers.md +61 -0
- package/src/scripts/__tests__/codex-native-hook.test.ts +975 -40
- package/src/scripts/codex-native-hook.ts +529 -22
- package/src/scripts/notify-hook/team-tmux-guard.ts +92 -8
- package/src/scripts/notify-hook/tmux-injection.ts +1 -1
- package/src/scripts/run-compiled-ci.ts +85 -0
- package/src/scripts/sync-plugin-mirror.ts +1 -0
- package/templates/AGENTS.md +6 -0
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
readSkillActiveState,
|
|
14
14
|
readVisibleSkillActiveStateForStateDir,
|
|
15
15
|
type SkillActiveStateLike,
|
|
16
|
+
type SkillActiveEntry,
|
|
16
17
|
} from "../state/skill-active.js";
|
|
17
18
|
import {
|
|
18
19
|
isTrustedSubagentThread,
|
|
@@ -40,7 +41,7 @@ import {
|
|
|
40
41
|
} from "../team/state.js";
|
|
41
42
|
import { omxNotepadPath, resolveProjectMemoryPath } from "../utils/paths.js";
|
|
42
43
|
import { findGitLayout } from "../utils/git-layout.js";
|
|
43
|
-
import { getBaseStateDir, getStateFilePath, getStatePath } from "../mcp/state-paths.js";
|
|
44
|
+
import { getBaseStateDir, getStateFilePath, getStatePath, getAuthoritativeActiveStatePaths } from "../mcp/state-paths.js";
|
|
44
45
|
import {
|
|
45
46
|
detectKeywords,
|
|
46
47
|
detectPrimaryKeyword,
|
|
@@ -87,6 +88,9 @@ import { readAutoresearchCompletionStatus, readAutoresearchModeStateForActiveDec
|
|
|
87
88
|
import { normalizeAutopilotPhase } from "../autopilot/fsm.js";
|
|
88
89
|
import { readRunState } from "../runtime/run-state.js";
|
|
89
90
|
import { evaluateRalphCompletionAuditEvidence, isRalphCompletePhase } from "../ralph/completion-audit.js";
|
|
91
|
+
import {
|
|
92
|
+
buildCodexGoalTerminalCleanupNotice,
|
|
93
|
+
} from "../goal-workflows/codex-goal-snapshot.js";
|
|
90
94
|
import { getRunContinuationSnapshot, shouldContinueRun } from "../runtime/run-loop.js";
|
|
91
95
|
import {
|
|
92
96
|
parseUltragoalSteeringDirective,
|
|
@@ -147,6 +151,8 @@ const SKILL_STOP_BLOCKERS = new Set(["ralplan"]);
|
|
|
147
151
|
const TEAM_STOP_BLOCKING_TASK_STATUSES = new Set(["pending", "in_progress", "blocked"]);
|
|
148
152
|
const TEAM_WORKER_TERMINAL_RUN_STATES = new Set(["done", "complete", "completed", "failed", "stopped", "cancelled"]);
|
|
149
153
|
const NATIVE_STOP_STATE_FILE = "native-stop-state.json";
|
|
154
|
+
const NATIVE_SUBAGENT_CAPACITY_BLOCKER_FILE = "native-subagent-capacity-blocker.json";
|
|
155
|
+
const NATIVE_SUBAGENT_CAPACITY_BLOCKER_TTL_MS = 30 * 60_000;
|
|
150
156
|
const ORDINARY_STOP_NO_PROGRESS_DEFAULT_MAX_REPEATS = 8;
|
|
151
157
|
const RALPH_ORPHANED_STARTING_STALE_MS = 15 * 60_000;
|
|
152
158
|
const ORDINARY_STOP_NO_PROGRESS_DEFAULT_IDLE_MS = 10 * 60_000;
|
|
@@ -549,6 +555,23 @@ function readHookEventName(payload: CodexHookPayload): CodexHookEventName | null
|
|
|
549
555
|
return null;
|
|
550
556
|
}
|
|
551
557
|
|
|
558
|
+
function sanitizeCodexHookOutput(
|
|
559
|
+
hookEventName: CodexHookEventName | null,
|
|
560
|
+
output: Record<string, unknown> | null,
|
|
561
|
+
): Record<string, unknown> | null {
|
|
562
|
+
if (!output || hookEventName !== "PreToolUse") return output;
|
|
563
|
+
const systemMessage = safeString(output.systemMessage).trim();
|
|
564
|
+
if (systemMessage) return { systemMessage };
|
|
565
|
+
|
|
566
|
+
const reason = safeString(output.reason).trim();
|
|
567
|
+
const hookSpecificOutput = output.hookSpecificOutput;
|
|
568
|
+
const additionalContext = hookSpecificOutput && typeof hookSpecificOutput === "object"
|
|
569
|
+
? safeString((hookSpecificOutput as { additionalContext?: unknown }).additionalContext).trim()
|
|
570
|
+
: "";
|
|
571
|
+
const derivedSystemMessage = [reason, additionalContext].filter(Boolean).join("\n\n");
|
|
572
|
+
return derivedSystemMessage ? { systemMessage: derivedSystemMessage } : {};
|
|
573
|
+
}
|
|
574
|
+
|
|
552
575
|
export function mapCodexHookEventToOmxEvent(
|
|
553
576
|
hookEventName: CodexHookEventName | null,
|
|
554
577
|
): string | null {
|
|
@@ -2227,6 +2250,76 @@ function isStopExempt(payload: CodexHookPayload): boolean {
|
|
|
2227
2250
|
);
|
|
2228
2251
|
}
|
|
2229
2252
|
|
|
2253
|
+
async function readModeStateWithStopSource(
|
|
2254
|
+
mode: "autopilot" | "ultrawork" | "ultraqa",
|
|
2255
|
+
cwd: string,
|
|
2256
|
+
sessionId?: string,
|
|
2257
|
+
): Promise<{ state: Record<string, unknown>; path: string } | null> {
|
|
2258
|
+
const paths = await getAuthoritativeActiveStatePaths(mode, cwd, sessionId?.trim() || undefined).catch(() => [] as string[]);
|
|
2259
|
+
const path = paths[0];
|
|
2260
|
+
if (!path) return null;
|
|
2261
|
+
const state = await readJsonIfExists(path);
|
|
2262
|
+
return state ? { state, path } : null;
|
|
2263
|
+
}
|
|
2264
|
+
async function readRawSkillActiveState(path: string): Promise<SkillActiveStateLike | null> {
|
|
2265
|
+
try {
|
|
2266
|
+
const parsed = JSON.parse(await readFile(path, "utf-8"));
|
|
2267
|
+
return parsed && typeof parsed === "object" ? parsed as SkillActiveStateLike : null;
|
|
2268
|
+
} catch {
|
|
2269
|
+
return null;
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
|
|
2274
|
+
function canonicalStopDisagreement(modeState: Record<string, unknown>, canonicalState: SkillActiveStateLike | null, mode: string, sessionId?: string): string {
|
|
2275
|
+
if (!canonicalState) return "canonical_state_missing";
|
|
2276
|
+
const normalizedSessionId = safeString(sessionId).trim();
|
|
2277
|
+
const activeEntry = listRawActiveSkillEntries(canonicalState).find((entry) => {
|
|
2278
|
+
if (entry.skill !== mode) return false;
|
|
2279
|
+
const entrySessionId = safeString(entry.session_id ?? canonicalState.session_id).trim();
|
|
2280
|
+
return normalizedSessionId ? entrySessionId === normalizedSessionId || entrySessionId === "" : true;
|
|
2281
|
+
});
|
|
2282
|
+
if (!activeEntry) return "canonical_inactive";
|
|
2283
|
+
if (mode === "autopilot") {
|
|
2284
|
+
const phase = safeString(modeState.current_phase ?? modeState.currentPhase).trim();
|
|
2285
|
+
const canonicalPhase = safeString(activeEntry.phase ?? canonicalState.phase).trim();
|
|
2286
|
+
if (phase && canonicalPhase && normalizeAutopilotPhase(phase) !== normalizeAutopilotPhase(canonicalPhase)) {
|
|
2287
|
+
return `canonical_phase:${canonicalPhase}`;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
return "canonical_agrees";
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
function listRawActiveSkillEntries(state: SkillActiveStateLike | null): SkillActiveEntry[] {
|
|
2294
|
+
if (!state) return [];
|
|
2295
|
+
const entries: SkillActiveEntry[] = [];
|
|
2296
|
+
if (Array.isArray(state.active_skills)) {
|
|
2297
|
+
for (const candidate of state.active_skills) {
|
|
2298
|
+
if (!candidate || typeof candidate !== "object") continue;
|
|
2299
|
+
const raw = candidate as unknown as Record<string, unknown>;
|
|
2300
|
+
const skill = safeString(raw.skill).trim();
|
|
2301
|
+
if (!skill || raw.active === false) continue;
|
|
2302
|
+
entries.push({
|
|
2303
|
+
...raw,
|
|
2304
|
+
skill,
|
|
2305
|
+
phase: safeString(raw.phase).trim() || undefined,
|
|
2306
|
+
session_id: safeString(raw.session_id).trim() || undefined,
|
|
2307
|
+
thread_id: safeString(raw.thread_id).trim() || undefined,
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
const topLevelSkill = safeString(state.skill).trim();
|
|
2312
|
+
if (state.active === true && topLevelSkill) {
|
|
2313
|
+
entries.push({
|
|
2314
|
+
skill: topLevelSkill,
|
|
2315
|
+
phase: safeString(state.phase).trim() || undefined,
|
|
2316
|
+
session_id: safeString(state.session_id).trim() || undefined,
|
|
2317
|
+
thread_id: safeString(state.thread_id).trim() || undefined,
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
return entries;
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2230
2323
|
async function buildModeBasedStopOutput(
|
|
2231
2324
|
mode: "autopilot" | "ultrawork" | "ultraqa",
|
|
2232
2325
|
cwd: string,
|
|
@@ -2238,17 +2331,38 @@ async function buildModeBasedStopOutput(
|
|
|
2238
2331
|
if (mode === "autopilot" && await readAutopilotDeepInterviewQuestionWaitState(cwd, sessionId)) {
|
|
2239
2332
|
return null;
|
|
2240
2333
|
}
|
|
2241
|
-
const
|
|
2334
|
+
const sourcedState = await readModeStateWithStopSource(mode, cwd, sessionId);
|
|
2335
|
+
const state = sourcedState?.state ?? null;
|
|
2242
2336
|
if (!state || !shouldContinueRun(state)) return null;
|
|
2337
|
+
const rootCanonicalState = await readRawSkillActiveState(getSkillActiveStatePathsForStateDir(getBaseStateDir(cwd)).rootPath);
|
|
2338
|
+
const canonicalDisagreement = rootCanonicalState
|
|
2339
|
+
? canonicalStopDisagreement(state, rootCanonicalState, mode, sessionId)
|
|
2340
|
+
: "canonical_state_missing";
|
|
2341
|
+
if (canonicalDisagreement === "canonical_inactive") return null;
|
|
2243
2342
|
const phase = formatPhase(state.current_phase);
|
|
2343
|
+
if (!rootCanonicalState || mode !== "autopilot") {
|
|
2344
|
+
const systemMessage = mode === "autopilot" && phase.toLowerCase().replace(/_/g, "-") === "code-review"
|
|
2345
|
+
? "OMX autopilot is still active (phase: code-review). Run the required $code-review step before completing or clearing Autopilot state."
|
|
2346
|
+
: `OMX ${mode} is still active (phase: ${phase}).`;
|
|
2347
|
+
return {
|
|
2348
|
+
decision: "block",
|
|
2349
|
+
reason: `OMX ${mode} is still active (phase: ${phase}); continue the task and gather fresh verification evidence before stopping.`,
|
|
2350
|
+
stopReason: `${mode}_${phase}`,
|
|
2351
|
+
systemMessage,
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
const statePath = sourcedState ? formatStopStatePath(cwd, sourcedState.path) : "unknown";
|
|
2355
|
+
const diagnostic = `state: ${statePath}; canonical: ${canonicalDisagreement}`;
|
|
2244
2356
|
const systemMessage = mode === "autopilot" && phase.toLowerCase().replace(/_/g, "-") === "code-review"
|
|
2245
|
-
?
|
|
2246
|
-
: `OMX ${mode} is still active (phase: ${phase}).`;
|
|
2357
|
+
? `OMX autopilot is still active (phase: code-review; ${diagnostic}). Run the required $code-review step before completing or clearing Autopilot state.`
|
|
2358
|
+
: `OMX ${mode} is still active (phase: ${phase}; ${diagnostic}).`;
|
|
2247
2359
|
return {
|
|
2248
2360
|
decision: "block",
|
|
2249
|
-
reason: `OMX ${mode} is still active (phase: ${phase}); continue the task and gather fresh verification evidence before stopping.`,
|
|
2361
|
+
reason: `OMX ${mode} is still active (phase: ${phase}; ${diagnostic}); continue the task and gather fresh verification evidence before stopping.`,
|
|
2250
2362
|
stopReason: `${mode}_${phase}`,
|
|
2251
2363
|
systemMessage,
|
|
2364
|
+
statePath,
|
|
2365
|
+
canonicalDisagreement,
|
|
2252
2366
|
};
|
|
2253
2367
|
}
|
|
2254
2368
|
|
|
@@ -2294,6 +2408,63 @@ function reportsBlockedUltragoalCompletedAggregateMicrogoalLoop(goal: Record<str
|
|
|
2294
2408
|
&& /\b(?:unreconcilable|mismatch|loop|already complete|already completed|blocks?)\b/i.test(evidence);
|
|
2295
2409
|
}
|
|
2296
2410
|
|
|
2411
|
+
function looksLikeNewGoalPrompt(text: string): boolean {
|
|
2412
|
+
return /(?:\b(?:start|create|begin|new|another)\b.{0,80}\b(?:goal|ultragoal|performance[-\s]goal|autoresearch[-\s]goal)\b|\b(?:goal|ultragoal|performance[-\s]goal|autoresearch[-\s]goal)\b.{0,80}\b(?:start|create|begin|new|another)\b)/i.test(text);
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
async function findCompletedGoalWorkflowCleanupNotice(cwd: string): Promise<string | null> {
|
|
2416
|
+
const ultragoal = await readJsonIfExists(join(cwd, ".omx", "ultragoal", "goals.json"));
|
|
2417
|
+
const aggregateCompletion = safeObject(ultragoal?.aggregateCompletion);
|
|
2418
|
+
const ultragoals = Array.isArray(ultragoal?.goals) ? ultragoal.goals.map(safeObject) : [];
|
|
2419
|
+
if (safeString(aggregateCompletion.status) === "complete" || (ultragoals.length > 0 && ultragoals.every((goal) => safeString(goal.status) === "complete"))) {
|
|
2420
|
+
return buildCodexGoalTerminalCleanupNotice("Ultragoal completion");
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
const performanceRoot = join(cwd, ".omx", "goals", "performance");
|
|
2424
|
+
for (const entry of await readdir(performanceRoot, { withFileTypes: true }).catch(() => [])) {
|
|
2425
|
+
if (!entry.isDirectory()) continue;
|
|
2426
|
+
const state = await readJsonIfExists(join(performanceRoot, entry.name, "state.json"));
|
|
2427
|
+
if (state?.workflow === "performance-goal" && safeString(state.status) === "complete") {
|
|
2428
|
+
return buildCodexGoalTerminalCleanupNotice("Performance-goal completion");
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
const autoresearchRoot = join(cwd, ".omx", "goals", "autoresearch");
|
|
2433
|
+
for (const entry of await readdir(autoresearchRoot, { withFileTypes: true }).catch(() => [])) {
|
|
2434
|
+
if (!entry.isDirectory()) continue;
|
|
2435
|
+
const mission = await readJsonIfExists(join(autoresearchRoot, entry.name, "mission.json"));
|
|
2436
|
+
if (mission?.workflow === "autoresearch-goal" && safeString(mission.status) === "complete") {
|
|
2437
|
+
return buildCodexGoalTerminalCleanupNotice("Autoresearch-goal completion");
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
return null;
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
async function buildCompletedGoalCleanupPromptWarning(cwd: string, prompt: string): Promise<string | null> {
|
|
2445
|
+
if (!looksLikeNewGoalPrompt(prompt)) return null;
|
|
2446
|
+
const notice = await findCompletedGoalWorkflowCleanupNotice(cwd);
|
|
2447
|
+
if (!notice) return null;
|
|
2448
|
+
return `${notice} Do not continue into create_goal until cleanup is explicit; hooks only nudge and must not mutate Codex goal state.`;
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
async function buildCompletedGoalCleanupStopOutput(payload: CodexHookPayload, cwd: string): Promise<Record<string, unknown> | null> {
|
|
2452
|
+
const text = [
|
|
2453
|
+
safeString(payload.last_user_message ?? payload.lastUserMessage),
|
|
2454
|
+
safeString(payload.last_assistant_message ?? payload.lastAssistantMessage),
|
|
2455
|
+
].join("\n");
|
|
2456
|
+
if (!looksLikeNewGoalPrompt(text)) return null;
|
|
2457
|
+
const notice = await findCompletedGoalWorkflowCleanupNotice(cwd);
|
|
2458
|
+
if (!notice) return null;
|
|
2459
|
+
const systemMessage = `${notice} Do not continue into create_goal until cleanup is explicit; hooks only nudge and must not mutate Codex goal state.`;
|
|
2460
|
+
return {
|
|
2461
|
+
decision: "block",
|
|
2462
|
+
reason: systemMessage,
|
|
2463
|
+
stopReason: "completed_codex_goal_cleanup_required",
|
|
2464
|
+
systemMessage,
|
|
2465
|
+
};
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2297
2468
|
async function findActiveGoalWorkflowReconciliationRequirement(cwd: string): Promise<{ workflow: string; command: string; remediation?: string } | null> {
|
|
2298
2469
|
const ultragoal = await readJsonIfExists(join(cwd, ".omx", "ultragoal", "goals.json"));
|
|
2299
2470
|
const aggregateCompletion = safeObject(ultragoal?.aggregateCompletion);
|
|
@@ -2545,6 +2716,144 @@ function readPayloadTurnId(payload: CodexHookPayload): string {
|
|
|
2545
2716
|
return safeString(payload.turn_id ?? payload.turnId).trim();
|
|
2546
2717
|
}
|
|
2547
2718
|
|
|
2719
|
+
interface NativeSubagentCapacityBlocker {
|
|
2720
|
+
schema_version: 1;
|
|
2721
|
+
reason: "agent_thread_limit_reached";
|
|
2722
|
+
session_id?: string;
|
|
2723
|
+
thread_id?: string;
|
|
2724
|
+
turn_id?: string;
|
|
2725
|
+
tool_name?: string;
|
|
2726
|
+
error_summary: string;
|
|
2727
|
+
observed_at: string;
|
|
2728
|
+
expires_at: string;
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
function nativeSubagentCapacityBlockerPath(stateDir: string): string {
|
|
2732
|
+
return join(stateDir, NATIVE_SUBAGENT_CAPACITY_BLOCKER_FILE);
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
function stringifyUnknown(value: unknown): string {
|
|
2736
|
+
if (typeof value === "string") return value;
|
|
2737
|
+
if (value === undefined || value === null) return "";
|
|
2738
|
+
try {
|
|
2739
|
+
return JSON.stringify(value);
|
|
2740
|
+
} catch {
|
|
2741
|
+
return String(value);
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
function payloadEvidenceText(payload: CodexHookPayload): string {
|
|
2746
|
+
return [
|
|
2747
|
+
safeString(payload.tool_name),
|
|
2748
|
+
stringifyUnknown(payload.tool_response),
|
|
2749
|
+
stringifyUnknown(payload.response),
|
|
2750
|
+
stringifyUnknown(payload.error),
|
|
2751
|
+
stringifyUnknown(payload.message),
|
|
2752
|
+
].filter(Boolean).join("\n");
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
function isNativeSubagentCapacityFailure(payload: CodexHookPayload): boolean {
|
|
2756
|
+
const evidence = payloadEvidenceText(payload);
|
|
2757
|
+
if (!/\bagent thread limit reached\b/i.test(evidence)) return false;
|
|
2758
|
+
const toolName = safeString(payload.tool_name).trim();
|
|
2759
|
+
return !toolName || /(?:spawn_agent|multi_agent|subagent|collab|agent)/i.test(toolName);
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
function summarizeCapacityFailure(text: string): string {
|
|
2763
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
2764
|
+
if (!normalized) return "agent thread limit reached";
|
|
2765
|
+
const match = normalized.match(/[^.?!\n\r]*agent thread limit reached[^.?!\n\r]*/i);
|
|
2766
|
+
return (match?.[0] ?? normalized).slice(0, 500);
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
async function recordNativeSubagentCapacityBlocker(
|
|
2770
|
+
cwd: string,
|
|
2771
|
+
stateDir: string,
|
|
2772
|
+
payload: CodexHookPayload,
|
|
2773
|
+
): Promise<void> {
|
|
2774
|
+
if (!isNativeSubagentCapacityFailure(payload)) return;
|
|
2775
|
+
const nowMs = Date.now();
|
|
2776
|
+
const blocker: NativeSubagentCapacityBlocker = {
|
|
2777
|
+
schema_version: 1,
|
|
2778
|
+
reason: "agent_thread_limit_reached",
|
|
2779
|
+
...(readPayloadSessionId(payload) ? { session_id: readPayloadSessionId(payload) } : {}),
|
|
2780
|
+
...(readPayloadThreadId(payload) ? { thread_id: readPayloadThreadId(payload) } : {}),
|
|
2781
|
+
...(readPayloadTurnId(payload) ? { turn_id: readPayloadTurnId(payload) } : {}),
|
|
2782
|
+
...(safeString(payload.tool_name).trim() ? { tool_name: safeString(payload.tool_name).trim() } : {}),
|
|
2783
|
+
error_summary: summarizeCapacityFailure(payloadEvidenceText(payload)),
|
|
2784
|
+
observed_at: new Date(nowMs).toISOString(),
|
|
2785
|
+
expires_at: new Date(nowMs + NATIVE_SUBAGENT_CAPACITY_BLOCKER_TTL_MS).toISOString(),
|
|
2786
|
+
};
|
|
2787
|
+
await mkdir(stateDir, { recursive: true });
|
|
2788
|
+
await writeFile(nativeSubagentCapacityBlockerPath(stateDir), JSON.stringify({
|
|
2789
|
+
...blocker,
|
|
2790
|
+
cwd,
|
|
2791
|
+
}, null, 2));
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
function isFreshNativeSubagentCapacityBlocker(
|
|
2795
|
+
blocker: Record<string, unknown> | null,
|
|
2796
|
+
cwd: string,
|
|
2797
|
+
payload: CodexHookPayload,
|
|
2798
|
+
nowMs = Date.now(),
|
|
2799
|
+
): blocker is NativeSubagentCapacityBlocker & Record<string, unknown> {
|
|
2800
|
+
if (!blocker) return false;
|
|
2801
|
+
if (safeString(blocker.reason) !== "agent_thread_limit_reached") return false;
|
|
2802
|
+
const expiresAtMs = Date.parse(safeString(blocker.expires_at));
|
|
2803
|
+
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= nowMs) return false;
|
|
2804
|
+
const blockerCwd = safeString(blocker.cwd).trim();
|
|
2805
|
+
if (blockerCwd) {
|
|
2806
|
+
try {
|
|
2807
|
+
if (resolve(blockerCwd) !== resolve(cwd)) return false;
|
|
2808
|
+
} catch {
|
|
2809
|
+
return false;
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
const blockerSessionId = safeString(blocker.session_id).trim();
|
|
2813
|
+
const payloadSessionId = readPayloadSessionId(payload);
|
|
2814
|
+
return !blockerSessionId || !payloadSessionId || blockerSessionId === payloadSessionId;
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
function inputContainsCloseAgentRequest(value: unknown): boolean {
|
|
2818
|
+
if (typeof value === "string") return /\bclose_agent\b/i.test(value);
|
|
2819
|
+
if (!value || typeof value !== "object") return false;
|
|
2820
|
+
try {
|
|
2821
|
+
return /\bclose_agent\b/i.test(JSON.stringify(value));
|
|
2822
|
+
} catch {
|
|
2823
|
+
return false;
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
function isCloseAgentToolUse(payload: CodexHookPayload): boolean {
|
|
2828
|
+
const toolName = safeString(payload.tool_name).trim();
|
|
2829
|
+
if (/\bclose_agent\b/i.test(toolName)) return true;
|
|
2830
|
+
if (/multi_tool_use\.parallel/i.test(toolName) && inputContainsCloseAgentRequest(payload.tool_input)) return true;
|
|
2831
|
+
return inputContainsCloseAgentRequest(payload.tool_input) && /multi_agent|agent|tool_use/i.test(toolName);
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
async function buildNativeSubagentCapacityCloseGuardOutput(
|
|
2835
|
+
payload: CodexHookPayload,
|
|
2836
|
+
cwd: string,
|
|
2837
|
+
stateDir: string,
|
|
2838
|
+
): Promise<Record<string, unknown> | null> {
|
|
2839
|
+
if (!isCloseAgentToolUse(payload)) return null;
|
|
2840
|
+
const blocker = await readJsonIfExists(nativeSubagentCapacityBlockerPath(stateDir));
|
|
2841
|
+
if (!isFreshNativeSubagentCapacityBlocker(blocker, cwd, payload)) return null;
|
|
2842
|
+
|
|
2843
|
+
const evidence = safeString(blocker.error_summary).trim() || "agent thread limit reached";
|
|
2844
|
+
return {
|
|
2845
|
+
decision: "block",
|
|
2846
|
+
reason: "Native subagent capacity was exhausted recently; model-level close_agent cleanup is blocked because close_agent can hang indefinitely on stale handles.",
|
|
2847
|
+
hookSpecificOutput: {
|
|
2848
|
+
hookEventName: "PreToolUse",
|
|
2849
|
+
additionalContext:
|
|
2850
|
+
`OMX blocked ${safeString(payload.tool_name).trim() || "close_agent"} before it could start: a recent native subagent capacity failure was recorded (${evidence}). `
|
|
2851
|
+
+ "Do not call multi_agent_v1.close_agent, and do not batch close_agent through multi_tool_use.parallel, as stale native handles can hang the whole turn. "
|
|
2852
|
+
+ "Treat this as a bounded capacity blocker: persist/report the blocker evidence, avoid further native subagent cleanup from the model turn, and recover via runtime-level cleanup or a fresh Codex session.",
|
|
2853
|
+
},
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2548
2857
|
async function resolveInternalSessionIdForPayload(
|
|
2549
2858
|
cwd: string,
|
|
2550
2859
|
payloadSessionId: string,
|
|
@@ -2603,6 +2912,7 @@ const RALPLAN_ALLOWED_WRITE_PREFIXES = [
|
|
|
2603
2912
|
".omx/plans",
|
|
2604
2913
|
".omx/specs",
|
|
2605
2914
|
".omx/state",
|
|
2915
|
+
".beads",
|
|
2606
2916
|
] as const;
|
|
2607
2917
|
|
|
2608
2918
|
const PLANNING_MODE_IMPLEMENTATION_TOOL_NAMES = new Set([
|
|
@@ -2653,6 +2963,10 @@ function canAutopilotSkillMirrorSupplyRalplanPhase(phase: string): boolean {
|
|
|
2653
2963
|
return phase === "" || normalizeAutopilotPhase(phase) === "ralplan";
|
|
2654
2964
|
}
|
|
2655
2965
|
|
|
2966
|
+
function isAutopilotReviewReworkPhase(phase: string): boolean {
|
|
2967
|
+
return normalizeAutopilotPhase(phase) === "rework";
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2656
2970
|
function hasExplicitExecutionHandoffSkill(
|
|
2657
2971
|
state: SkillActiveStateLike | null,
|
|
2658
2972
|
sessionId: string,
|
|
@@ -2692,6 +3006,138 @@ function isAllowedRalplanArtifactPath(cwd: string, rawPath: string): boolean {
|
|
|
2692
3006
|
return isAllowedPlanningArtifactPath(cwd, rawPath, RALPLAN_ALLOWED_WRITE_PREFIXES);
|
|
2693
3007
|
}
|
|
2694
3008
|
|
|
3009
|
+
interface RalplanBeadsCommandClassification {
|
|
3010
|
+
present: boolean;
|
|
3011
|
+
allowed: boolean;
|
|
3012
|
+
reason?: string;
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
function shellTokenizeLiteralCommand(command: string): string[] | null {
|
|
3016
|
+
const tokens: string[] = [];
|
|
3017
|
+
let current = "";
|
|
3018
|
+
let quote: "'" | '"' | null = null;
|
|
3019
|
+
let escaping = false;
|
|
3020
|
+
|
|
3021
|
+
for (const char of command.trim()) {
|
|
3022
|
+
if (escaping) {
|
|
3023
|
+
current += char;
|
|
3024
|
+
escaping = false;
|
|
3025
|
+
continue;
|
|
3026
|
+
}
|
|
3027
|
+
if (quote === '"' && char === "\\") {
|
|
3028
|
+
escaping = true;
|
|
3029
|
+
continue;
|
|
3030
|
+
}
|
|
3031
|
+
if (quote) {
|
|
3032
|
+
if (char === quote) quote = null;
|
|
3033
|
+
else current += char;
|
|
3034
|
+
continue;
|
|
3035
|
+
}
|
|
3036
|
+
if (char === "'" || char === '"') {
|
|
3037
|
+
quote = char;
|
|
3038
|
+
continue;
|
|
3039
|
+
}
|
|
3040
|
+
if (/\s/.test(char)) {
|
|
3041
|
+
if (current) {
|
|
3042
|
+
tokens.push(current);
|
|
3043
|
+
current = "";
|
|
3044
|
+
}
|
|
3045
|
+
continue;
|
|
3046
|
+
}
|
|
3047
|
+
if (/[;&|<>`$(){}\n\r]/.test(char)) return null;
|
|
3048
|
+
current += char;
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
if (escaping || quote) return null;
|
|
3052
|
+
if (current) tokens.push(current);
|
|
3053
|
+
return tokens;
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
function findLiteralBdExecutableIndex(tokens: string[]): number {
|
|
3057
|
+
if (tokens[0] === "bd") return 0;
|
|
3058
|
+
if (tokens[0] === "command" || tokens[0] === "builtin" || tokens[0] === "exec" || tokens[0] === "nohup") {
|
|
3059
|
+
return tokens[1] === "bd" ? 1 : -1;
|
|
3060
|
+
}
|
|
3061
|
+
if (tokens[0] !== "env") return -1;
|
|
3062
|
+
|
|
3063
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
3064
|
+
const token = tokens[index] ?? "";
|
|
3065
|
+
if (token === "bd") return index;
|
|
3066
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token)) continue;
|
|
3067
|
+
if (token.startsWith("-")) continue;
|
|
3068
|
+
return -1;
|
|
3069
|
+
}
|
|
3070
|
+
return -1;
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
function isAllowedRalplanBeadsDbPath(cwd: string, rawPath: string): boolean {
|
|
3074
|
+
const trimmed = rawPath.trim().replace(/^['"]|['"]$/g, "");
|
|
3075
|
+
if (!trimmed || trimmed.includes("\0")) return false;
|
|
3076
|
+
let relativePath: string;
|
|
3077
|
+
try {
|
|
3078
|
+
const absolute = resolve(cwd, trimmed);
|
|
3079
|
+
relativePath = relative(cwd, absolute).replace(/\\/g, "/");
|
|
3080
|
+
} catch {
|
|
3081
|
+
return false;
|
|
3082
|
+
}
|
|
3083
|
+
return relativePath.startsWith(".beads/") && relativePath.length > ".beads/".length;
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
function classifyRalplanBeadsMetadataCommand(cwd: string, command: string): RalplanBeadsCommandClassification {
|
|
3087
|
+
const trimmedCommand = command.trim();
|
|
3088
|
+
const startsWithBd = /^(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"$`]*"|'[^']*'|[^\s"'$`;&|<>]+)\s+)*bd(?:\s|$)/.test(trimmedCommand);
|
|
3089
|
+
const hasCompoundBd = /[;&|()]\s*bd(?:\s|$)/.test(command);
|
|
3090
|
+
const tokens = shellTokenizeLiteralCommand(command);
|
|
3091
|
+
const bdExecutableIndex = tokens ? findLiteralBdExecutableIndex(tokens) : -1;
|
|
3092
|
+
if (!startsWithBd && !hasCompoundBd && bdExecutableIndex === -1) return { present: false, allowed: false };
|
|
3093
|
+
|
|
3094
|
+
if (!tokens || bdExecutableIndex !== 0) {
|
|
3095
|
+
return { present: true, allowed: false, reason: "Beads tracker command must be a single literal bd invocation" };
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
let dbPath = "";
|
|
3099
|
+
let dbValueIndex = -1;
|
|
3100
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
3101
|
+
const token = tokens[index] ?? "";
|
|
3102
|
+
if (token === "--db") {
|
|
3103
|
+
dbPath = tokens[index + 1] ?? "";
|
|
3104
|
+
dbValueIndex = index + 1;
|
|
3105
|
+
break;
|
|
3106
|
+
}
|
|
3107
|
+
if (token.startsWith("--db=")) {
|
|
3108
|
+
dbPath = token.slice("--db=".length);
|
|
3109
|
+
dbValueIndex = index;
|
|
3110
|
+
break;
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
if (!dbPath) {
|
|
3115
|
+
return { present: true, allowed: false, reason: "Beads tracker command is missing a literal --db .beads/<db> target" };
|
|
3116
|
+
}
|
|
3117
|
+
if (!isAllowedRalplanBeadsDbPath(cwd, dbPath)) {
|
|
3118
|
+
return { present: true, allowed: false, reason: `Beads tracker db target ${dbPath} is outside repo-local .beads metadata` };
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
const operationTokens = tokens
|
|
3122
|
+
.slice(dbValueIndex + 1)
|
|
3123
|
+
.filter((token) => token && !token.startsWith("-"));
|
|
3124
|
+
const operation = operationTokens[0] ?? "";
|
|
3125
|
+
const suboperation = operationTokens[1] ?? "";
|
|
3126
|
+
if (["create", "update", "edit", "close", "reopen", "status", "dep"].includes(operation)) {
|
|
3127
|
+
return { present: true, allowed: true };
|
|
3128
|
+
}
|
|
3129
|
+
if (operation === "comments" && suboperation === "add") {
|
|
3130
|
+
return { present: true, allowed: true };
|
|
3131
|
+
}
|
|
3132
|
+
return {
|
|
3133
|
+
present: true,
|
|
3134
|
+
allowed: false,
|
|
3135
|
+
reason: operation
|
|
3136
|
+
? `Beads tracker operation ${operation}${suboperation ? ` ${suboperation}` : ""} is not allowed during planning`
|
|
3137
|
+
: "Beads tracker command is missing an allowed metadata operation",
|
|
3138
|
+
};
|
|
3139
|
+
}
|
|
3140
|
+
|
|
2695
3141
|
function readPreToolUseCommand(payload: CodexHookPayload): string {
|
|
2696
3142
|
const toolInput = safeObject(payload.tool_input);
|
|
2697
3143
|
return safeString(toolInput.command).trim();
|
|
@@ -2805,6 +3251,33 @@ function extractDeepInterviewCommandWriteTargets(command: string): string[] {
|
|
|
2805
3251
|
}
|
|
2806
3252
|
return targets;
|
|
2807
3253
|
}
|
|
3254
|
+
function formatPlanningWriteBlockDetail(
|
|
3255
|
+
operationClass: string,
|
|
3256
|
+
target: string | undefined,
|
|
3257
|
+
allowedPrefixes: readonly string[],
|
|
3258
|
+
): string {
|
|
3259
|
+
const targetDetail = target ? `target ${target}` : "target <unresolved>";
|
|
3260
|
+
return `${operationClass} ${targetDetail} is not under allowed planning artifact paths (${allowedPrefixes.join(", ")})`;
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
function isUnresolvedVariableTarget(target: string): boolean {
|
|
3264
|
+
const normalized = target.trim().replace(/^['"]|['"]$/g, "");
|
|
3265
|
+
return /^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/.test(normalized);
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
|
|
3269
|
+
function describeImplementationToolBlock(
|
|
3270
|
+
toolName: string,
|
|
3271
|
+
blockedPath: string | undefined,
|
|
3272
|
+
pathCount: number,
|
|
3273
|
+
): string {
|
|
3274
|
+
if (pathCount === 0) {
|
|
3275
|
+
const operationClass = isApplyPatchToolName(toolName) ? "apply_patch target extraction failed" : `${toolName} path`;
|
|
3276
|
+
return `${operationClass} target <unresolved>; only planning artifact paths are allowed (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`;
|
|
3277
|
+
}
|
|
3278
|
+
const operationClass = isApplyPatchToolName(toolName) ? "apply_patch target" : `${toolName} path`;
|
|
3279
|
+
return formatPlanningWriteBlockDetail(operationClass, blockedPath, RALPLAN_ALLOWED_WRITE_PREFIXES);
|
|
3280
|
+
}
|
|
2808
3281
|
|
|
2809
3282
|
function isAllowedDeepInterviewBashWrite(cwd: string, command: string): boolean {
|
|
2810
3283
|
if (!commandHasDeepInterviewWriteIntent(command)) return true;
|
|
@@ -2899,6 +3372,7 @@ async function readActiveRalplanStateForPreToolUse(
|
|
|
2899
3372
|
));
|
|
2900
3373
|
if (!hasActiveAutopilotSkill) return null;
|
|
2901
3374
|
const autopilotStatePhase = safeString(autopilotState.current_phase ?? autopilotState.currentPhase).trim().toLowerCase();
|
|
3375
|
+
if (isAutopilotReviewReworkPhase(autopilotStatePhase)) return null;
|
|
2902
3376
|
if (!canAutopilotSkillMirrorSupplyRalplanPhase(autopilotStatePhase)) return null;
|
|
2903
3377
|
const hasRalplanScopedAutopilotSkill = listActiveSkills(canonicalState).some((entry) => (
|
|
2904
3378
|
entry.skill === "autopilot"
|
|
@@ -2910,10 +3384,37 @@ async function readActiveRalplanStateForPreToolUse(
|
|
|
2910
3384
|
}
|
|
2911
3385
|
|
|
2912
3386
|
function isAllowedRalplanBashWrite(cwd: string, command: string): boolean {
|
|
3387
|
+
const beadsCommand = classifyRalplanBeadsMetadataCommand(cwd, command);
|
|
3388
|
+
const targets = extractDeepInterviewCommandWriteTargets(command);
|
|
3389
|
+
const hasAllowedTargets = targets.length > 0
|
|
3390
|
+
&& targets.every((target) => isAllowedRalplanArtifactPath(cwd, target));
|
|
3391
|
+
|
|
3392
|
+
if (beadsCommand.present) {
|
|
3393
|
+
return beadsCommand.allowed && (targets.length === 0 || hasAllowedTargets);
|
|
3394
|
+
}
|
|
2913
3395
|
if (!commandHasDeepInterviewWriteIntent(command)) return true;
|
|
2914
3396
|
if (/\bomx\s+(?:state\s+(?:write|read|clear)|question)\b/.test(command)) return true;
|
|
3397
|
+
return hasAllowedTargets;
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3400
|
+
function buildRalplanBashBlockedDetail(cwd: string, command: string): string {
|
|
2915
3401
|
const targets = extractDeepInterviewCommandWriteTargets(command);
|
|
2916
|
-
|
|
3402
|
+
const blockedTarget = targets.find((target) => !isAllowedRalplanArtifactPath(cwd, target));
|
|
3403
|
+
if (blockedTarget && isUnresolvedVariableTarget(blockedTarget)) {
|
|
3404
|
+
return `unresolved Bash write target ${blockedTarget} is not under allowed planning artifact paths or metadata paths (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`;
|
|
3405
|
+
}
|
|
3406
|
+
if (blockedTarget) {
|
|
3407
|
+
const operationClass = /\btee\s+(?:-a\s+)?/.test(command) ? "Bash tee write" : "Bash redirect write";
|
|
3408
|
+
return `${operationClass} target ${blockedTarget} is not under allowed planning artifact paths or metadata paths (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`;
|
|
3409
|
+
}
|
|
3410
|
+
const beadsCommand = classifyRalplanBeadsMetadataCommand(cwd, command);
|
|
3411
|
+
if (beadsCommand.present && !beadsCommand.allowed) {
|
|
3412
|
+
return beadsCommand.reason ?? "Beads tracker command is not an allowed planning metadata mutation";
|
|
3413
|
+
}
|
|
3414
|
+
if (beadsCommand.present) {
|
|
3415
|
+
return "Beads tracker command also performs an implementation write outside allowed planning metadata";
|
|
3416
|
+
}
|
|
3417
|
+
return "Bash write intent did not identify an allowed planning artifact path or metadata path";
|
|
2917
3418
|
}
|
|
2918
3419
|
|
|
2919
3420
|
async function buildRalplanPreToolUseBoundaryOutput(
|
|
@@ -2936,21 +3437,18 @@ async function buildRalplanPreToolUseBoundaryOutput(
|
|
|
2936
3437
|
if (toolName === "Bash") {
|
|
2937
3438
|
blocked = !isAllowedRalplanBashWrite(cwd, command);
|
|
2938
3439
|
if (blocked) {
|
|
2939
|
-
|
|
2940
|
-
const blockedTarget = targets.find((target) => !isAllowedRalplanArtifactPath(cwd, target));
|
|
2941
|
-
blockedDetail = blockedTarget
|
|
2942
|
-
? `write target ${blockedTarget} is not under allowed planning artifact paths (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`
|
|
2943
|
-
: "Bash write intent did not identify an allowed planning artifact path";
|
|
3440
|
+
blockedDetail = buildRalplanBashBlockedDetail(cwd, command);
|
|
2944
3441
|
}
|
|
2945
3442
|
} else if (PLANNING_MODE_IMPLEMENTATION_TOOL_NAMES.has(toolName)) {
|
|
2946
|
-
|
|
3443
|
+
const toolPathCandidates = collectImplementationToolPathCandidates(payload, toolName, pathCandidates);
|
|
3444
|
+
if (toolPathCandidates.length === 0) {
|
|
2947
3445
|
blocked = true;
|
|
2948
|
-
blockedDetail =
|
|
3446
|
+
blockedDetail = describeImplementationToolBlock(toolName, undefined, toolPathCandidates.length);
|
|
2949
3447
|
} else {
|
|
2950
|
-
const blockedPath =
|
|
3448
|
+
const blockedPath = toolPathCandidates.find((candidate) => !isAllowedRalplanArtifactPath(cwd, candidate));
|
|
2951
3449
|
blocked = blockedPath !== undefined;
|
|
2952
3450
|
if (blockedPath !== undefined) {
|
|
2953
|
-
blockedDetail =
|
|
3451
|
+
blockedDetail = describeImplementationToolBlock(toolName, blockedPath, toolPathCandidates.length);
|
|
2954
3452
|
}
|
|
2955
3453
|
}
|
|
2956
3454
|
}
|
|
@@ -2970,7 +3468,7 @@ async function buildRalplanPreToolUseBoundaryOutput(
|
|
|
2970
3468
|
hookEventName: "PreToolUse",
|
|
2971
3469
|
additionalContext:
|
|
2972
3470
|
`${planningModeDescription}. `
|
|
2973
|
-
+ "Write only planning artifacts under `.omx/context/`, `.omx/plans/`, `.omx/specs/`,
|
|
3471
|
+
+ "Write only planning artifacts under `.omx/context/`, `.omx/plans/`, `.omx/specs/`, required `.omx/state/` files, or tracker metadata under `.beads/`. "
|
|
2974
3472
|
+ "Do not edit implementation files or run implementation-focused writes from planning phases. "
|
|
2975
3473
|
+ `To execute, first process an explicit handoff such as ${formatExecutionHandoffList(cwd)}, which must emit terminal planning state before implementation begins.`,
|
|
2976
3474
|
},
|
|
@@ -4361,7 +4859,8 @@ export async function dispatchCodexNativeHook(
|
|
|
4361
4859
|
|
|
4362
4860
|
if (hookEventName === "UserPromptSubmit") {
|
|
4363
4861
|
const prompt = readPromptText(payload);
|
|
4364
|
-
goalWorkflowAdditionalContext = await
|
|
4862
|
+
goalWorkflowAdditionalContext = await buildCompletedGoalCleanupPromptWarning(cwd, prompt).catch(() => null)
|
|
4863
|
+
?? await buildGoalWorkflowReconciliationPromptWarning(cwd, prompt).catch(() => null);
|
|
4365
4864
|
ultragoalSteeringAdditionalContext = prompt && !isSubagentPromptSubmit
|
|
4366
4865
|
? await applyUserPromptUltragoalSteering(cwd, prompt).catch((error) => `OMX native UserPromptSubmit rejected bounded .omx/ultragoal steering for G002-cli-and-prompt-submit-bridge: ${error instanceof Error ? error.message : String(error)}`)
|
|
4367
4866
|
: null;
|
|
@@ -4534,8 +5033,10 @@ export async function dispatchCodexNativeHook(
|
|
|
4534
5033
|
: "";
|
|
4535
5034
|
outputJson = await buildDeepInterviewPreToolUseBoundaryOutput(payload, cwd, stateDir, preToolUseSessionId)
|
|
4536
5035
|
?? await buildRalplanPreToolUseBoundaryOutput(payload, cwd, stateDir, preToolUseSessionId)
|
|
5036
|
+
?? await buildNativeSubagentCapacityCloseGuardOutput(payload, cwd, stateDir)
|
|
4537
5037
|
?? buildNativePreToolUseOutput(payload);
|
|
4538
5038
|
} else if (hookEventName === "PostToolUse") {
|
|
5039
|
+
await recordNativeSubagentCapacityBlocker(cwd, stateDir, payload).catch(() => {});
|
|
4539
5040
|
if (detectMcpTransportFailure(payload)) {
|
|
4540
5041
|
await markTeamTransportFailure(cwd, payload);
|
|
4541
5042
|
}
|
|
@@ -4544,7 +5045,7 @@ export async function dispatchCodexNativeHook(
|
|
|
4544
5045
|
} else if (hookEventName === "Stop") {
|
|
4545
5046
|
outputJson = await buildStopHookOutput(payload, cwd, stateDir, {
|
|
4546
5047
|
skipRalphStopBlock: isSubagentStop,
|
|
4547
|
-
});
|
|
5048
|
+
}) ?? await buildCompletedGoalCleanupStopOutput(payload, cwd);
|
|
4548
5049
|
}
|
|
4549
5050
|
|
|
4550
5051
|
return {
|
|
@@ -4657,6 +5158,9 @@ function buildMalformedStdinHookOutput(
|
|
|
4657
5158
|
const systemMessage =
|
|
4658
5159
|
`${reason} stdin JSON parsing failed inside codex-native-hook: ${parseError.message}.`;
|
|
4659
5160
|
const inferredHookEventName = inferHookEventNameFromMalformedInput(rawInput);
|
|
5161
|
+
if (inferredHookEventName === "PreToolUse") {
|
|
5162
|
+
return { systemMessage };
|
|
5163
|
+
}
|
|
4660
5164
|
if (inferredHookEventName === "Stop" || (!inferredHookEventName && hasNativeStopRuntimeSurface(cwd))) {
|
|
4661
5165
|
return {
|
|
4662
5166
|
decision: "block",
|
|
@@ -4699,11 +5203,14 @@ async function buildOversizedStdinHookOutput(
|
|
|
4699
5203
|
rawHookEventName: CodexHookEventName | null,
|
|
4700
5204
|
cwd: string,
|
|
4701
5205
|
): Promise<Record<string, unknown>> {
|
|
5206
|
+
const systemMessage =
|
|
5207
|
+
`OMX native hook rejected oversized stdin JSON before parsing; maxBytes=${MAX_NATIVE_STDIN_JSON_BYTES}.`;
|
|
5208
|
+
if (rawHookEventName === "PreToolUse") {
|
|
5209
|
+
return { systemMessage };
|
|
5210
|
+
}
|
|
4702
5211
|
if (rawHookEventName === "Stop") {
|
|
4703
5212
|
return await buildOversizedStopActiveWorkflowOutput(cwd) ?? {};
|
|
4704
5213
|
}
|
|
4705
|
-
const systemMessage =
|
|
4706
|
-
`OMX native hook rejected oversized stdin JSON before parsing; maxBytes=${MAX_NATIVE_STDIN_JSON_BYTES}.`;
|
|
4707
5214
|
return {
|
|
4708
5215
|
continue: false,
|
|
4709
5216
|
stopReason: "native_hook_stdin_oversized",
|
|
@@ -4817,8 +5324,8 @@ export async function runCodexNativeHookCli(): Promise<void> {
|
|
|
4817
5324
|
|
|
4818
5325
|
const result = await dispatchCodexNativeHook(payload);
|
|
4819
5326
|
if (result.outputJson) {
|
|
4820
|
-
writeNativeHookJsonStdout(result.outputJson);
|
|
4821
|
-
} else if (result.hookEventName
|
|
5327
|
+
writeNativeHookJsonStdout(sanitizeCodexHookOutput(result.hookEventName, result.outputJson) ?? {});
|
|
5328
|
+
} else if (result.hookEventName !== "PreCompact" && result.hookEventName !== "PostCompact") {
|
|
4822
5329
|
writeNativeHookJsonStdout({});
|
|
4823
5330
|
}
|
|
4824
5331
|
} catch (error) {
|