oh-my-codex 0.18.11 → 0.18.13
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 +9 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.js +668 -0
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
- package/dist/autopilot/completion-gate.d.ts +10 -0
- package/dist/autopilot/completion-gate.d.ts.map +1 -0
- package/dist/autopilot/completion-gate.js +154 -0
- package/dist/autopilot/completion-gate.js.map +1 -0
- package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
- package/dist/autopilot/ralplan-gate.js +42 -21
- package/dist/autopilot/ralplan-gate.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +46 -3
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-invalid-config.test.js +35 -0
- package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +317 -0
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +120 -2
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/resume.test.js +217 -1
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +101 -0
- package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
- package/dist/cli/__tests__/session-search-help.test.js +3 -2
- package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
- package/dist/cli/__tests__/session-search.test.js +64 -2
- package/dist/cli/__tests__/session-search.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +289 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +290 -17
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/setup-prompts-overwrite.test.js +74 -0
- package/dist/cli/__tests__/setup-prompts-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-scope.test.js +45 -0
- package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
- package/dist/cli/__tests__/state.test.js +93 -0
- package/dist/cli/__tests__/state.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +157 -3
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/__tests__/version-sync-contract.test.js +2 -0
- package/dist/cli/__tests__/version-sync-contract.test.js.map +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +90 -12
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +13 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +439 -46
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/project-runtime-codex-homes.d.ts +6 -0
- package/dist/cli/project-runtime-codex-homes.d.ts.map +1 -0
- package/dist/cli/project-runtime-codex-homes.js +27 -0
- package/dist/cli/project-runtime-codex-homes.js.map +1 -0
- package/dist/cli/session-search.d.ts.map +1 -1
- package/dist/cli/session-search.js +8 -1
- package/dist/cli/session-search.js.map +1 -1
- package/dist/cli/setup.d.ts +2 -2
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +482 -126
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/state.d.ts.map +1 -1
- package/dist/cli/state.js +79 -8
- package/dist/cli/state.js.map +1 -1
- package/dist/cli/update.d.ts +1 -0
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +42 -10
- package/dist/cli/update.js.map +1 -1
- package/dist/config/__tests__/codex-hooks.test.js +73 -29
- package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
- package/dist/config/codex-hooks.d.ts +14 -0
- package/dist/config/codex-hooks.d.ts.map +1 -1
- package/dist/config/codex-hooks.js +54 -51
- package/dist/config/codex-hooks.js.map +1 -1
- package/dist/config/generator.d.ts +1 -1
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +1 -1
- package/dist/config/generator.js.map +1 -1
- package/dist/hooks/__tests__/best-practice-research-skill.test.js +12 -0
- package/dist/hooks/__tests__/best-practice-research-skill.test.js.map +1 -1
- package/dist/hud/__tests__/authority.test.js +45 -12
- package/dist/hud/__tests__/authority.test.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +95 -0
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/render.test.js +6 -6
- package/dist/hud/__tests__/render.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +2 -2
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/authority.d.ts.map +1 -1
- package/dist/hud/authority.js +17 -2
- package/dist/hud/authority.js.map +1 -1
- package/dist/hud/index.js +1 -4
- package/dist/hud/index.js.map +1 -1
- package/dist/hud/reconcile.d.ts.map +1 -1
- package/dist/hud/reconcile.js +42 -0
- package/dist/hud/reconcile.js.map +1 -1
- package/dist/hud/render.d.ts.map +1 -1
- package/dist/hud/render.js +6 -0
- package/dist/hud/render.js.map +1 -1
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +5 -4
- package/dist/hud/tmux.js.map +1 -1
- package/dist/mcp/__tests__/bootstrap.test.js +31 -1
- package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
- package/dist/mcp/bootstrap.d.ts +1 -0
- package/dist/mcp/bootstrap.d.ts.map +1 -1
- package/dist/mcp/bootstrap.js +32 -0
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/modes/__tests__/base-autopilot-gates.test.d.ts +2 -0
- package/dist/modes/__tests__/base-autopilot-gates.test.d.ts.map +1 -0
- package/dist/modes/__tests__/base-autopilot-gates.test.js +154 -0
- package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -0
- package/dist/modes/base.d.ts +4 -1
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +71 -1
- package/dist/modes/base.js.map +1 -1
- package/dist/pipeline/__tests__/orchestrator.test.js +144 -3
- package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +109 -0
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +11 -4
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/stages/code-review.d.ts +2 -0
- package/dist/pipeline/stages/code-review.d.ts.map +1 -1
- package/dist/pipeline/stages/code-review.js +2 -0
- package/dist/pipeline/stages/code-review.js.map +1 -1
- package/dist/pipeline/stages/ultraqa.d.ts +3 -0
- package/dist/pipeline/stages/ultraqa.d.ts.map +1 -1
- package/dist/pipeline/stages/ultraqa.js +3 -0
- package/dist/pipeline/stages/ultraqa.js.map +1 -1
- package/dist/ralplan/__tests__/consensus-gate.test.d.ts +2 -0
- package/dist/ralplan/__tests__/consensus-gate.test.d.ts.map +1 -0
- package/dist/ralplan/__tests__/consensus-gate.test.js +631 -0
- package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -0
- package/dist/ralplan/consensus-gate.d.ts +9 -1
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +287 -65
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +481 -0
- 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 +145 -25
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/codex-native-pre-post.d.ts +1 -0
- package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
- package/dist/scripts/codex-native-pre-post.js +130 -0
- package/dist/scripts/codex-native-pre-post.js.map +1 -1
- package/dist/session-history/__tests__/search.test.js +166 -0
- package/dist/session-history/__tests__/search.test.js.map +1 -1
- package/dist/session-history/search.d.ts +7 -0
- package/dist/session-history/search.d.ts.map +1 -1
- package/dist/session-history/search.js +83 -24
- package/dist/session-history/search.js.map +1 -1
- package/dist/sidecar/__tests__/collector.test.js +60 -0
- package/dist/sidecar/__tests__/collector.test.js.map +1 -1
- package/dist/sidecar/collector.d.ts.map +1 -1
- package/dist/sidecar/collector.js +3 -6
- package/dist/sidecar/collector.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +622 -0
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/skill-active.test.js +82 -0
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +31 -9
- package/dist/state/operations.js.map +1 -1
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +41 -1
- package/dist/state/skill-active.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +81 -57
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/runtime.js +4 -4
- package/dist/team/runtime.js.map +1 -1
- package/dist/utils/__tests__/paths.test.js +23 -0
- package/dist/utils/__tests__/paths.test.js.map +1 -1
- package/dist/utils/__tests__/version.test.js +27 -0
- package/dist/utils/__tests__/version.test.js.map +1 -1
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +4 -2
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/version.d.ts.map +1 -1
- package/dist/utils/version.js +7 -2
- package/dist/utils/version.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +4 -2
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +71 -3
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
- package/package.json +1 -1
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +53 -2
- package/plugins/oh-my-codex/skills/best-practice-research/SKILL.md +6 -1
- package/skills/best-practice-research/SKILL.md +6 -1
- package/src/scripts/__tests__/codex-native-hook.test.ts +615 -0
- package/src/scripts/codex-native-hook.ts +162 -32
- package/src/scripts/codex-native-pre-post.ts +137 -0
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
SLOPPY_FALLBACK_PHRASE_PATTERNS,
|
|
62
62
|
buildNativePostToolUseOutput,
|
|
63
63
|
buildNativePreToolUseOutput,
|
|
64
|
+
commandInvokesApplyPatch,
|
|
64
65
|
detectMcpTransportFailure,
|
|
65
66
|
hasAnyPattern,
|
|
66
67
|
} from "./codex-native-pre-post.js";
|
|
@@ -2708,11 +2709,74 @@ function readPreToolUsePathCandidates(payload: CodexHookPayload): string[] {
|
|
|
2708
2709
|
return candidates.map((candidate) => safeString(candidate).trim()).filter(Boolean);
|
|
2709
2710
|
}
|
|
2710
2711
|
|
|
2712
|
+
const APPLY_PATCH_TOOL_NAMES = new Set(["apply_patch", "ApplyPatch"]);
|
|
2713
|
+
|
|
2714
|
+
function isApplyPatchToolName(toolName: string): boolean {
|
|
2715
|
+
return APPLY_PATCH_TOOL_NAMES.has(toolName);
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
function readApplyPatchText(payload: CodexHookPayload): string {
|
|
2719
|
+
const input = safeObject(payload.tool_input);
|
|
2720
|
+
for (const key of ["input", "patch", "content", "text", "command"]) {
|
|
2721
|
+
const value = safeString(input[key]).trim();
|
|
2722
|
+
if (value) return value;
|
|
2723
|
+
}
|
|
2724
|
+
return "";
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
function extractApplyPatchTargetPaths(patchText: string): string[] {
|
|
2728
|
+
if (!patchText) return [];
|
|
2729
|
+
const paths: string[] = [];
|
|
2730
|
+
for (const match of patchText.matchAll(/^\s*\*\*\*\s+(?:Add|Update|Delete)\s+File:\s*(.+?)\s*$/gm)) {
|
|
2731
|
+
const candidate = safeString(match[1]).trim();
|
|
2732
|
+
if (candidate) paths.push(candidate);
|
|
2733
|
+
}
|
|
2734
|
+
for (const match of patchText.matchAll(/^\s*\*\*\*\s+Move\s+to:\s*(.+?)\s*$/gm)) {
|
|
2735
|
+
const candidate = safeString(match[1]).trim();
|
|
2736
|
+
if (candidate) paths.push(candidate);
|
|
2737
|
+
}
|
|
2738
|
+
return paths;
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
function collectImplementationToolPathCandidates(
|
|
2742
|
+
payload: CodexHookPayload,
|
|
2743
|
+
toolName: string,
|
|
2744
|
+
structuredCandidates: string[],
|
|
2745
|
+
): string[] {
|
|
2746
|
+
if (!isApplyPatchToolName(toolName)) return structuredCandidates;
|
|
2747
|
+
return [...structuredCandidates, ...extractApplyPatchTargetPaths(readApplyPatchText(payload))];
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2711
2750
|
function isNullDeviceRedirectTarget(target: string): boolean {
|
|
2712
2751
|
const normalized = target.trim().replace(/^['"]|['"]$/g, "").toLowerCase();
|
|
2713
2752
|
return normalized === "/dev/null" || normalized === "nul";
|
|
2714
2753
|
}
|
|
2715
2754
|
|
|
2755
|
+
// Collects same-command literal variable assignments (`NAME="value"`), skipping
|
|
2756
|
+
// any value that involves expansion (`$`, backticks) so unresolved/dynamic
|
|
2757
|
+
// targets stay conservatively blocked.
|
|
2758
|
+
function extractCommandLiteralAssignments(command: string): Map<string, string> {
|
|
2759
|
+
const assignments = new Map<string, string>();
|
|
2760
|
+
const pattern = /(?:^|[\n;&|(]|&&|\|\|)\s*([A-Za-z_][A-Za-z0-9_]*)=(?:"([^"$`]*)"|'([^']*)'|([^\s"'$`;&|<>]+))/g;
|
|
2761
|
+
for (const match of command.matchAll(pattern)) {
|
|
2762
|
+
const name = safeString(match[1]).trim();
|
|
2763
|
+
if (!name) continue;
|
|
2764
|
+
const value = match[2] ?? match[3] ?? match[4] ?? "";
|
|
2765
|
+
assignments.set(name, value);
|
|
2766
|
+
}
|
|
2767
|
+
return assignments;
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
// Resolves a redirect/tee target of the form `$NAME`/`${NAME}` against
|
|
2771
|
+
// same-command literal assignments; non-variable or unresolved targets are
|
|
2772
|
+
// returned unchanged so they remain subject to the allowed-path check.
|
|
2773
|
+
function resolveCommandRedirectTarget(target: string, assignments: Map<string, string>): string {
|
|
2774
|
+
const variableMatch = target.match(/^\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?$/);
|
|
2775
|
+
if (!variableMatch) return target;
|
|
2776
|
+
const resolved = assignments.get(safeString(variableMatch[1]));
|
|
2777
|
+
return resolved !== undefined ? resolved : target;
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2716
2780
|
function extractDeepInterviewCommandRedirectTargets(command: string): string[] {
|
|
2717
2781
|
const targets: string[] = [];
|
|
2718
2782
|
for (const match of command.matchAll(/(?:^|[^>])>{1,2}\s*(["']?)([^\s&|;<>]+)\1/g)) {
|
|
@@ -2723,7 +2787,7 @@ function extractDeepInterviewCommandRedirectTargets(command: string): string[] {
|
|
|
2723
2787
|
}
|
|
2724
2788
|
|
|
2725
2789
|
function commandHasDeepInterviewWriteIntent(command: string): boolean {
|
|
2726
|
-
return
|
|
2790
|
+
return commandInvokesApplyPatch(command)
|
|
2727
2791
|
|| extractDeepInterviewCommandRedirectTargets(command).length > 0
|
|
2728
2792
|
|| /\btee\s+(?:-a\s+)?[^\s&|;]+/.test(command)
|
|
2729
2793
|
|| /\bsed\s+(?:[^\n;&|]*\s)?-i(?:\b|['"])/.test(command)
|
|
@@ -2732,10 +2796,12 @@ function commandHasDeepInterviewWriteIntent(command: string): boolean {
|
|
|
2732
2796
|
}
|
|
2733
2797
|
|
|
2734
2798
|
function extractDeepInterviewCommandWriteTargets(command: string): string[] {
|
|
2735
|
-
const
|
|
2799
|
+
const assignments = extractCommandLiteralAssignments(command);
|
|
2800
|
+
const targets = extractDeepInterviewCommandRedirectTargets(command)
|
|
2801
|
+
.map((target) => resolveCommandRedirectTarget(target, assignments));
|
|
2736
2802
|
for (const match of command.matchAll(/\btee\s+(?:-a\s+)?(["']?)([^\s&|;<>]+)\1/g)) {
|
|
2737
2803
|
const candidate = safeString(match[2]).trim();
|
|
2738
|
-
if (candidate) targets.push(candidate);
|
|
2804
|
+
if (candidate) targets.push(resolveCommandRedirectTarget(candidate, assignments));
|
|
2739
2805
|
}
|
|
2740
2806
|
return targets;
|
|
2741
2807
|
}
|
|
@@ -2753,21 +2819,46 @@ async function readActiveDeepInterviewStateForPreToolUse(
|
|
|
2753
2819
|
sessionId: string,
|
|
2754
2820
|
threadId: string,
|
|
2755
2821
|
): Promise<Record<string, unknown> | null> {
|
|
2756
|
-
const modeState = sessionId
|
|
2757
|
-
? await readStopSessionPinnedState("deep-interview-state.json", cwd, sessionId, stateDir)
|
|
2758
|
-
: await readJsonIfExists(join(stateDir, "deep-interview-state.json"));
|
|
2759
|
-
if (!isActiveDeepInterviewPhase(modeState) || !modeState) return null;
|
|
2760
|
-
if (!modeStateMatchesSkillStopContext(modeState, cwd, sessionId)) return null;
|
|
2761
|
-
|
|
2762
2822
|
const canonicalState = sessionId
|
|
2763
2823
|
? await readVisibleSkillActiveStateForStateDir(stateDir, sessionId)
|
|
2764
2824
|
: await readSkillActiveState(join(stateDir, SKILL_ACTIVE_STATE_FILE));
|
|
2765
2825
|
if (!canonicalState) return null;
|
|
2766
|
-
|
|
2767
|
-
|
|
2826
|
+
|
|
2827
|
+
const modeState = sessionId
|
|
2828
|
+
? await readStopSessionPinnedState("deep-interview-state.json", cwd, sessionId, stateDir)
|
|
2829
|
+
: await readJsonIfExists(join(stateDir, "deep-interview-state.json"));
|
|
2830
|
+
if (isActiveDeepInterviewPhase(modeState) && modeState && modeStateMatchesSkillStopContext(modeState, cwd, sessionId)) {
|
|
2831
|
+
const hasActiveDeepInterviewSkill = listActiveSkills(canonicalState).some((entry) => (
|
|
2832
|
+
entry.skill === "deep-interview"
|
|
2833
|
+
&& matchesSkillStopContext(entry, canonicalState, sessionId, threadId)
|
|
2834
|
+
));
|
|
2835
|
+
if (hasActiveDeepInterviewSkill) return modeState;
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
const autopilotState = sessionId
|
|
2839
|
+
? await readStopSessionPinnedState("autopilot-state.json", cwd, sessionId, stateDir)
|
|
2840
|
+
: await readJsonIfExists(join(stateDir, "autopilot-state.json"));
|
|
2841
|
+
if (!autopilotState || autopilotState.active !== true) return null;
|
|
2842
|
+
const autopilotMode = safeString(autopilotState.mode).trim();
|
|
2843
|
+
if (autopilotMode && autopilotMode !== "autopilot") return null;
|
|
2844
|
+
if (!modeStateMatchesSkillStopContext(autopilotState, cwd, sessionId)) return null;
|
|
2845
|
+
const terminalAutopilotRunState = await readCanonicalTerminalRunStateForStop(cwd, sessionId, "autopilot");
|
|
2846
|
+
if (terminalAutopilotRunState) return null;
|
|
2847
|
+
|
|
2848
|
+
const autopilotStatePhase = safeString(autopilotState.current_phase ?? autopilotState.currentPhase).trim().toLowerCase();
|
|
2849
|
+
const autopilotIsDeepInterview = normalizeAutopilotPhase(autopilotStatePhase) === "deep-interview";
|
|
2850
|
+
const hasDeepInterviewScopedAutopilotSkill = listActiveSkills(canonicalState).some((entry) => (
|
|
2851
|
+
entry.skill === "autopilot"
|
|
2852
|
+
&& normalizeAutopilotPhase(safeString(entry.phase).trim().toLowerCase()) === "deep-interview"
|
|
2768
2853
|
&& matchesSkillStopContext(entry, canonicalState, sessionId, threadId)
|
|
2769
2854
|
));
|
|
2770
|
-
|
|
2855
|
+
const hasActiveAutopilotSkill = listActiveSkills(canonicalState).some((entry) => (
|
|
2856
|
+
entry.skill === "autopilot"
|
|
2857
|
+
&& matchesSkillStopContext(entry, canonicalState, sessionId, threadId)
|
|
2858
|
+
));
|
|
2859
|
+
if (!hasActiveAutopilotSkill) return null;
|
|
2860
|
+
if (!autopilotIsDeepInterview && !hasDeepInterviewScopedAutopilotSkill) return null;
|
|
2861
|
+
return autopilotState;
|
|
2771
2862
|
}
|
|
2772
2863
|
|
|
2773
2864
|
async function readActiveRalplanStateForPreToolUse(
|
|
@@ -2840,12 +2931,28 @@ async function buildRalplanPreToolUseBoundaryOutput(
|
|
|
2840
2931
|
const command = readPreToolUseCommand(payload);
|
|
2841
2932
|
const pathCandidates = readPreToolUsePathCandidates(payload);
|
|
2842
2933
|
let blocked = false;
|
|
2934
|
+
let blockedDetail = "implementation/write tools are blocked until an explicit execution handoff workflow is activated";
|
|
2843
2935
|
|
|
2844
2936
|
if (toolName === "Bash") {
|
|
2845
2937
|
blocked = !isAllowedRalplanBashWrite(cwd, command);
|
|
2938
|
+
if (blocked) {
|
|
2939
|
+
const targets = extractDeepInterviewCommandWriteTargets(command);
|
|
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";
|
|
2944
|
+
}
|
|
2846
2945
|
} else if (PLANNING_MODE_IMPLEMENTATION_TOOL_NAMES.has(toolName)) {
|
|
2847
|
-
|
|
2848
|
-
|
|
2946
|
+
if (pathCandidates.length === 0) {
|
|
2947
|
+
blocked = true;
|
|
2948
|
+
blockedDetail = `${toolName} did not include a file path; only planning artifact paths are allowed`;
|
|
2949
|
+
} else {
|
|
2950
|
+
const blockedPath = pathCandidates.find((candidate) => !isAllowedRalplanArtifactPath(cwd, candidate));
|
|
2951
|
+
blocked = blockedPath !== undefined;
|
|
2952
|
+
if (blockedPath !== undefined) {
|
|
2953
|
+
blockedDetail = `path ${blockedPath} is not under allowed planning artifact paths (${RALPLAN_ALLOWED_WRITE_PREFIXES.join(", ")})`;
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2849
2956
|
}
|
|
2850
2957
|
|
|
2851
2958
|
if (!blocked) return null;
|
|
@@ -2858,7 +2965,7 @@ async function buildRalplanPreToolUseBoundaryOutput(
|
|
|
2858
2965
|
: "Ralplan is consensus-planning mode";
|
|
2859
2966
|
return {
|
|
2860
2967
|
decision: "block",
|
|
2861
|
-
reason: `${planningModeLabel} is active (phase: ${phase}); implementation/write tools are blocked until an explicit execution handoff workflow is activated.`,
|
|
2968
|
+
reason: `${planningModeLabel} is active (phase: ${phase}); implementation/write tools are blocked until an explicit execution handoff workflow is activated; ${blockedDetail}.`,
|
|
2862
2969
|
hookSpecificOutput: {
|
|
2863
2970
|
hookEventName: "PreToolUse",
|
|
2864
2971
|
additionalContext:
|
|
@@ -2889,8 +2996,9 @@ async function buildDeepInterviewPreToolUseBoundaryOutput(
|
|
|
2889
2996
|
if (toolName === "Bash") {
|
|
2890
2997
|
blocked = !isAllowedDeepInterviewBashWrite(cwd, command);
|
|
2891
2998
|
} else if (DEEP_INTERVIEW_IMPLEMENTATION_TOOL_NAMES.has(toolName)) {
|
|
2892
|
-
|
|
2893
|
-
|
|
2999
|
+
const candidates = collectImplementationToolPathCandidates(payload, toolName, pathCandidates);
|
|
3000
|
+
blocked = candidates.length === 0
|
|
3001
|
+
|| !candidates.every((candidate) => isAllowedDeepInterviewArtifactPath(cwd, candidate));
|
|
2894
3002
|
}
|
|
2895
3003
|
|
|
2896
3004
|
if (!blocked) return null;
|
|
@@ -4141,28 +4249,50 @@ export async function dispatchCodexNativeHook(
|
|
|
4141
4249
|
if (hookEventName === "SessionStart" && nativeSessionId) {
|
|
4142
4250
|
const transcriptPath = safeString(payload.transcript_path ?? payload.transcriptPath).trim();
|
|
4143
4251
|
const subagentSessionStart = readNativeSubagentSessionStartMetadata(transcriptPath);
|
|
4144
|
-
if (subagentSessionStart
|
|
4252
|
+
if (subagentSessionStart) {
|
|
4253
|
+
// A native child/subagent SessionStart carries a parent_thread_id in its
|
|
4254
|
+
// transcript session_meta. Treat it as a child-agent lifecycle event for
|
|
4255
|
+
// notification suppression and subagent tracking even when the canonical
|
|
4256
|
+
// leader session has not been reconciled yet (#2831). A child start must
|
|
4257
|
+
// never promote itself into a root/leader session or emit an independent
|
|
4258
|
+
// session-start notification at session/minimal verbosity.
|
|
4145
4259
|
isSubagentSessionStart = true;
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
canonicalSessionId,
|
|
4149
|
-
currentSessionState,
|
|
4150
|
-
subagentSessionStart,
|
|
4151
|
-
);
|
|
4152
|
-
if (belongsToCanonicalSession) {
|
|
4153
|
-
resolvedNativeSessionId = nativeSessionId;
|
|
4154
|
-
await recordNativeSubagentSessionStart(
|
|
4260
|
+
if (canonicalSessionId) {
|
|
4261
|
+
const belongsToCanonicalSession = await nativeSubagentSessionStartBelongsToCanonicalSession(
|
|
4155
4262
|
cwd,
|
|
4156
4263
|
canonicalSessionId,
|
|
4157
|
-
|
|
4264
|
+
currentSessionState,
|
|
4158
4265
|
subagentSessionStart,
|
|
4159
|
-
transcriptPath,
|
|
4160
4266
|
);
|
|
4267
|
+
if (belongsToCanonicalSession) {
|
|
4268
|
+
resolvedNativeSessionId = nativeSessionId;
|
|
4269
|
+
await recordNativeSubagentSessionStart(
|
|
4270
|
+
cwd,
|
|
4271
|
+
canonicalSessionId,
|
|
4272
|
+
nativeSessionId,
|
|
4273
|
+
subagentSessionStart,
|
|
4274
|
+
transcriptPath,
|
|
4275
|
+
);
|
|
4276
|
+
} else {
|
|
4277
|
+
skipCanonicalSessionStartContext = true;
|
|
4278
|
+
resolvedNativeSessionId =
|
|
4279
|
+
safeString(currentSessionState?.native_session_id).trim() || nativeSessionId;
|
|
4280
|
+
await recordIgnoredNativeSubagentSessionStart(
|
|
4281
|
+
cwd,
|
|
4282
|
+
canonicalSessionId,
|
|
4283
|
+
nativeSessionId,
|
|
4284
|
+
subagentSessionStart,
|
|
4285
|
+
transcriptPath,
|
|
4286
|
+
);
|
|
4287
|
+
}
|
|
4161
4288
|
} else {
|
|
4289
|
+
// No canonical leader session is resolved in this worktree yet. Still
|
|
4290
|
+
// register the child thread under its parent so its later Stop is
|
|
4291
|
+
// recognized as subagent-scoped, skip leader SessionStart context, and
|
|
4292
|
+
// do not reconcile the child as a new root session.
|
|
4162
4293
|
skipCanonicalSessionStartContext = true;
|
|
4163
|
-
resolvedNativeSessionId =
|
|
4164
|
-
|
|
4165
|
-
await recordIgnoredNativeSubagentSessionStart(
|
|
4294
|
+
resolvedNativeSessionId = nativeSessionId;
|
|
4295
|
+
await recordNativeSubagentSessionStart(
|
|
4166
4296
|
cwd,
|
|
4167
4297
|
canonicalSessionId,
|
|
4168
4298
|
nativeSessionId,
|
|
@@ -325,6 +325,11 @@ function tokenizeShellCommandWithBoundaries(commandText: string): ShellToken[] |
|
|
|
325
325
|
let quote: "'" | "\"" | null = null;
|
|
326
326
|
let escaping = false;
|
|
327
327
|
let nextTokenStartsCommand = false;
|
|
328
|
+
// Command substitution executes even inside double quotes, so we track an
|
|
329
|
+
// active `"$(…)"` (paren depth) or `` "`…`" `` (backtick) substitution to
|
|
330
|
+
// restore double-quote mode once it closes.
|
|
331
|
+
let dquoteSubstParenDepth = 0;
|
|
332
|
+
let backtickFromDquote = false;
|
|
328
333
|
|
|
329
334
|
const pushCurrent = () => {
|
|
330
335
|
if (!current) return;
|
|
@@ -357,6 +362,25 @@ function tokenizeShellCommandWithBoundaries(commandText: string): ShellToken[] |
|
|
|
357
362
|
if (isDoubleQuotedShellEscapeTarget(trimmed[index + 1])) escaping = true;
|
|
358
363
|
else current += char;
|
|
359
364
|
}
|
|
365
|
+
// Command substitution runs inside double quotes, so `"$(cmd …)"` and
|
|
366
|
+
// `` "`cmd …`" `` are real invocations, not literal mentions. Treat the
|
|
367
|
+
// opener as a command-position boundary and parse the substitution body
|
|
368
|
+
// unquoted so the command-head walk resumes, then restore double-quote
|
|
369
|
+
// mode when it closes. Parameter expansion (`${VAR}`), `$HOME`, and an
|
|
370
|
+
// escaped `\$(` stay literal text (no false positive on `grep "(x"`).
|
|
371
|
+
else if (char === "$" && trimmed[index + 1] === "(") {
|
|
372
|
+
pushCurrent();
|
|
373
|
+
nextTokenStartsCommand = true;
|
|
374
|
+
index += 1;
|
|
375
|
+
dquoteSubstParenDepth = 1;
|
|
376
|
+
quote = null;
|
|
377
|
+
}
|
|
378
|
+
else if (char === "`") {
|
|
379
|
+
pushCurrent();
|
|
380
|
+
nextTokenStartsCommand = true;
|
|
381
|
+
backtickFromDquote = true;
|
|
382
|
+
quote = null;
|
|
383
|
+
}
|
|
360
384
|
else current += char;
|
|
361
385
|
continue;
|
|
362
386
|
}
|
|
@@ -368,6 +392,48 @@ function tokenizeShellCommandWithBoundaries(commandText: string): ShellToken[] |
|
|
|
368
392
|
continue;
|
|
369
393
|
}
|
|
370
394
|
|
|
395
|
+
// Grouping / command-substitution openers act as command-position
|
|
396
|
+
// boundaries so the command-head walk resumes after them, mirroring the
|
|
397
|
+
// legacy `[\n;&|(]` boundary set. This catches a real command immediately
|
|
398
|
+
// after `(`, `((`, `$(` (command substitution), or a backtick — e.g.
|
|
399
|
+
// `(apply_patch …)`, `true | (apply_patch …)`, `x=$(apply_patch …)`. We
|
|
400
|
+
// are outside quotes here, so a literal like `grep "(apply_patch"` is left
|
|
401
|
+
// intact and not split.
|
|
402
|
+
// Closing backtick of a command substitution opened inside double quotes
|
|
403
|
+
// ends the substitution and restores the surrounding double-quoted literal.
|
|
404
|
+
if (char === "`" && backtickFromDquote) {
|
|
405
|
+
pushCurrent();
|
|
406
|
+
backtickFromDquote = false;
|
|
407
|
+
quote = "\"";
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (char === "(" || char === ")" || char === "`") {
|
|
412
|
+
pushCurrent();
|
|
413
|
+
nextTokenStartsCommand = true;
|
|
414
|
+
// Track paren depth of a `"$(…)"` substitution opened inside double
|
|
415
|
+
// quotes so the matching `)` restores double-quote mode (nested `$(`
|
|
416
|
+
// and `(` inside it are balanced before we return to the literal).
|
|
417
|
+
if (dquoteSubstParenDepth > 0) {
|
|
418
|
+
if (char === "(") {
|
|
419
|
+
dquoteSubstParenDepth += 1;
|
|
420
|
+
} else if (char === ")") {
|
|
421
|
+
dquoteSubstParenDepth -= 1;
|
|
422
|
+
if (dquoteSubstParenDepth === 0) quote = "\"";
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// `{` is a group opener only as its own word (`{ apply_patch …; }`):
|
|
429
|
+
// require an empty pending token (preceded by start/whitespace/boundary)
|
|
430
|
+
// and a following whitespace/newline so brace expansion (`{a,b}`) and
|
|
431
|
+
// parameter expansion (`${VAR}`) are left intact.
|
|
432
|
+
if (char === "{" && current === "" && /\s/.test(trimmed[index + 1] ?? " ")) {
|
|
433
|
+
nextTokenStartsCommand = true;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
|
|
371
437
|
if (/\s/.test(char)) {
|
|
372
438
|
pushCurrent();
|
|
373
439
|
continue;
|
|
@@ -488,6 +554,77 @@ function findGitCommandTokenIndex(tokens: ShellToken[]): number {
|
|
|
488
554
|
return -1;
|
|
489
555
|
}
|
|
490
556
|
|
|
557
|
+
const APPLY_PATCH_COMMAND_WRAPPER_TOKENS = new Set(["sudo", "command", "exec"]);
|
|
558
|
+
|
|
559
|
+
// Detects a real `apply_patch` invocation at a shell command position using the
|
|
560
|
+
// same tokenized command-head walk the git commit guard uses
|
|
561
|
+
// (`findGitCommandTokenIndex`): after every statement boundary skip leading
|
|
562
|
+
// inline `NAME=VALUE` assignments, the `env` executable with its full option
|
|
563
|
+
// grammar (`-i`/`--ignore-environment`, `-u`/`--unset`/`--unset=`, `-C`/`-S`,
|
|
564
|
+
// `--`, interleaved assignments), and `sudo`/`command`/`exec` wrappers — in any
|
|
565
|
+
// order — then match when the resolved command token's basename is
|
|
566
|
+
// `apply_patch`. This blocks path-qualified (`/usr/bin/apply_patch`,
|
|
567
|
+
// `./apply_patch`), env-flag (`env -i apply_patch`, `env -u FOO apply_patch`),
|
|
568
|
+
// and reordered (`FOO=bar env apply_patch`, `exec env FOO=bar apply_patch`)
|
|
569
|
+
// forms, while read-only diagnostics that merely mention the literal token
|
|
570
|
+
// (e.g. `grep -n "apply_patch" file`) are not misread as write intent. Heredoc
|
|
571
|
+
// bodies are stripped first so patch payloads cannot break tokenization.
|
|
572
|
+
export function commandInvokesApplyPatch(command: string): boolean {
|
|
573
|
+
const tokens = tokenizeShellCommandWithBoundaries(removeHereDocBodies(command));
|
|
574
|
+
if (!tokens) return false;
|
|
575
|
+
|
|
576
|
+
for (let commandStart = 0; commandStart < tokens.length; commandStart = nextCommandStart(tokens, commandStart)) {
|
|
577
|
+
let index = commandStart;
|
|
578
|
+
const commandEnd = nextCommandStart(tokens, commandStart);
|
|
579
|
+
|
|
580
|
+
let advanced = true;
|
|
581
|
+
while (advanced && index < commandEnd) {
|
|
582
|
+
advanced = false;
|
|
583
|
+
|
|
584
|
+
while (index < commandEnd && isInlineShellEnvAssignment(tokens[index]?.value ?? "")) {
|
|
585
|
+
index += 1;
|
|
586
|
+
advanced = true;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
while (index < commandEnd && isEnvExecutableToken(tokens[index]?.value ?? "")) {
|
|
590
|
+
index += 1;
|
|
591
|
+
advanced = true;
|
|
592
|
+
while (index < commandEnd) {
|
|
593
|
+
const token = tokens[index]?.value ?? "";
|
|
594
|
+
if (token === "--") {
|
|
595
|
+
index += 1;
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
if (isInlineShellEnvAssignment(token)) {
|
|
599
|
+
index += 1;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (token === "-i" || token === "--ignore-environment" || token.startsWith("--unset=")) {
|
|
603
|
+
index += 1;
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
if (token.startsWith("-")) {
|
|
607
|
+
index += envOptionConsumesNextValue(token) ? 2 : 1;
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
while (index < commandEnd && APPLY_PATCH_COMMAND_WRAPPER_TOKENS.has((tokens[index]?.value ?? "").toLowerCase())) {
|
|
615
|
+
index += 1;
|
|
616
|
+
advanced = true;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (index < commandEnd && shellCommandBasename(tokens[index]?.value ?? "") === "apply_patch") return true;
|
|
621
|
+
if (commandEnd <= commandStart) break;
|
|
622
|
+
commandStart = commandEnd - 1;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
|
|
491
628
|
function tokenValues(tokens: ShellToken[]): string[] {
|
|
492
629
|
return tokens.map((token) => token.value);
|
|
493
630
|
}
|