diff --git a/packages/ai/ai.test.ts b/packages/ai/ai.test.ts index fa002a26d..8c1a24c9d 100644 --- a/packages/ai/ai.test.ts +++ b/packages/ai/ai.test.ts @@ -878,7 +878,40 @@ describe("resolveSDKModel", () => { // Codex SDK event mapping // --------------------------------------------------------------------------- -import { mapCodexEvent, mapCodexItem } from "./providers/codex-sdk.ts"; +import { + mapCodexEvent, + mapCodexItem, + shouldSkipGitRepoCheck, +} from "./providers/codex-sdk.ts"; + +describe("shouldSkipGitRepoCheck", () => { + test("keeps the Codex git repo check inside a worktree", async () => { + const probe = async (command: string, args: string[], options: { encoding: "utf8" }) => { + expect(command).toBe("git"); + expect(args).toEqual(["-C", "/repo", "rev-parse", "--is-inside-work-tree"]); + expect(options).toEqual({ encoding: "utf8" }); + return { stdout: "true\n" }; + }; + + expect(await shouldSkipGitRepoCheck("/repo", probe)).toBe(false); + }); + + test("skips the Codex git repo check for standalone document sessions", async () => { + const probe = async () => { + throw new Error("not a git repo"); + }; + + expect(await shouldSkipGitRepoCheck("/tmp/plain-session", probe)).toBe(true); + }); + + test("skips the Codex git repo check when the probe cannot run", async () => { + const probe = async () => { + throw new Error("git unavailable"); + }; + + expect(await shouldSkipGitRepoCheck("/tmp/plain-session", probe)).toBe(true); + }); +}); describe("mapCodexEvent", () => { function offsets() { diff --git a/packages/ai/providers/codex-sdk.ts b/packages/ai/providers/codex-sdk.ts index 1088be575..293535a00 100644 --- a/packages/ai/providers/codex-sdk.ts +++ b/packages/ai/providers/codex-sdk.ts @@ -12,6 +12,8 @@ import { buildSystemPrompt, buildEffectivePrompt } from "../context.ts"; import { BaseSession } from "../base-session.ts"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; import type { AIProvider, AIProviderCapabilities, @@ -28,6 +30,30 @@ import type { const PROVIDER_NAME = "codex-sdk"; const DEFAULT_MODEL = "gpt-5.4"; +type GitWorkTreeProbe = ( + command: string, + args: string[], + options: { encoding: "utf8" }, +) => Promise<{ stdout?: string | Buffer }>; + +const execFileAsync = promisify(execFile); + +export async function shouldSkipGitRepoCheck( + cwd: string, + probe: GitWorkTreeProbe = execFileAsync, +): Promise { + try { + const result = probe( + "git", + ["-C", cwd, "rev-parse", "--is-inside-work-tree"], + { encoding: "utf8" }, + ); + return String((await result).stdout ?? "").trim() !== "true"; + } catch { + return true; + } +} + // --------------------------------------------------------------------------- // Provider // --------------------------------------------------------------------------- @@ -57,10 +83,13 @@ export class CodexSDKProvider implements AIProvider { } async createSession(options: CreateSessionOptions): Promise { + const cwd = options.cwd ?? this.config.cwd ?? process.cwd(); + const skipGitRepoCheck = await shouldSkipGitRepoCheck(cwd); return new CodexSDKSession({ ...this.baseConfig(options), systemPrompt: buildSystemPrompt(options.context), - cwd: options.cwd ?? this.config.cwd ?? process.cwd(), + cwd, + skipGitRepoCheck, parentSessionId: null, }); } @@ -73,10 +102,13 @@ export class CodexSDKProvider implements AIProvider { } async resumeSession(sessionId: string): Promise { + const cwd = this.config.cwd ?? process.cwd(); + const skipGitRepoCheck = await shouldSkipGitRepoCheck(cwd); return new CodexSDKSession({ ...this.baseConfig(), systemPrompt: null, - cwd: this.config.cwd ?? process.cwd(), + cwd, + skipGitRepoCheck, parentSessionId: null, resumeThreadId: sessionId, }); @@ -123,6 +155,7 @@ interface SessionConfig { maxTurns: number; sandboxMode: "read-only" | "workspace-write" | "danger-full-access"; cwd: string; + skipGitRepoCheck: boolean; parentSessionId: string | null; resumeThreadId?: string; codexExecutablePath?: string; @@ -173,6 +206,7 @@ class CodexSDKSession extends BaseSession { this._thread = this._codexInstance.resumeThread(this.config.resumeThreadId, { model: this.config.model, workingDirectory: this.config.cwd, + skipGitRepoCheck: this.config.skipGitRepoCheck, sandboxMode: this.config.sandboxMode, ...(this.config.reasoningEffort && { modelReasoningEffort: this.config.reasoningEffort }), }); @@ -180,6 +214,7 @@ class CodexSDKSession extends BaseSession { this._thread = this._codexInstance.startThread({ model: this.config.model, workingDirectory: this.config.cwd, + skipGitRepoCheck: this.config.skipGitRepoCheck, sandboxMode: this.config.sandboxMode, ...(this.config.reasoningEffort && { modelReasoningEffort: this.config.reasoningEffort }), });