diff --git a/packages/agent/src/adapters/codex/spawn.test.ts b/packages/agent/src/adapters/codex/spawn.test.ts index 4f2653c7c1..48fb947283 100644 --- a/packages/agent/src/adapters/codex/spawn.test.ts +++ b/packages/agent/src/adapters/codex/spawn.test.ts @@ -38,3 +38,39 @@ describe("spawnCodexProcess MCP disable args", () => { expect(args.some((arg) => arg.includes("weird name"))).toBe(false); }); }); + +describe("spawnCodexProcess developer instructions", () => { + it("passes guidance via developer_instructions to preserve the base prompt", () => { + spawnMock.mockClear(); + spawnMock.mockReturnValue(makeFakeChild()); + + spawnCodexProcess({ + logger: new Logger({ debug: false }), + developerInstructions: "Follow PostHog signed-commit rules.", + }); + + const args: string[] = spawnMock.mock.calls[0][1]; + expect(args).toContain( + 'developer_instructions="Follow PostHog signed-commit rules."', + ); + // The bare `instructions` and `model_instructions_file` keys replace Codex's + // model-optimized base prompt, so guidance must never go through them. + expect(args.some((arg) => arg.startsWith("instructions="))).toBe(false); + expect(args.some((arg) => arg.startsWith("model_instructions_file="))).toBe( + false, + ); + }); + + it("escapes backslashes, newlines and quotes in developer_instructions", () => { + spawnMock.mockClear(); + spawnMock.mockReturnValue(makeFakeChild()); + + spawnCodexProcess({ + logger: new Logger({ debug: false }), + developerInstructions: 'a\\b\n"c', + }); + + const args: string[] = spawnMock.mock.calls[0][1]; + expect(args).toContain('developer_instructions="a\\\\b\\n\\"c"'); + }); +}); diff --git a/packages/agent/src/adapters/codex/spawn.ts b/packages/agent/src/adapters/codex/spawn.ts index 72ff246057..3cbcf915af 100644 --- a/packages/agent/src/adapters/codex/spawn.ts +++ b/packages/agent/src/adapters/codex/spawn.ts @@ -12,7 +12,12 @@ export interface CodexProcessOptions { apiKey?: string; model?: string; reasoningEffort?: string; - instructions?: string; + /** + * Guidance appended on top of Codex's model-optimized base prompt via the + * `developer_instructions` config key. Unlike `instructions` / + * `model_instructions_file`, this does not replace the native base prompt. + */ + developerInstructions?: string; binaryPath?: string; logger?: Logger; processCallbacks?: ProcessSpawnedCallback; @@ -73,13 +78,13 @@ function buildConfigArgs(options: CodexProcessOptions): string[] { args.push("-c", `sandbox_workspace_write.writable_roots=[${escaped}]`); } - if (options.instructions) { - const escaped = options.instructions + if (options.developerInstructions) { + const escaped = options.developerInstructions .replace(/\\/g, "\\\\") .replace(/\n/g, "\\n") .replace(/\r/g, "\\r") .replace(/"/g, '\\"'); - args.push("-c", `instructions="${escaped}"`); + args.push("-c", `developer_instructions="${escaped}"`); } return args; diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index e1264f7c47..7cb2d2b82b 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -136,7 +136,7 @@ export class Agent { binaryPath: options.codexBinaryPath, model: sanitizedModel, reasoningEffort: options.reasoningEffort, - instructions: options.instructions, + developerInstructions: options.developerInstructions, additionalDirectories: options.additionalDirectories, } : undefined, diff --git a/packages/agent/src/server/agent-server.ts b/packages/agent/src/server/agent-server.ts index 0492b9fd06..b3410b570d 100644 --- a/packages/agent/src/server/agent-server.ts +++ b/packages/agent/src/server/agent-server.ts @@ -992,7 +992,7 @@ export class AgentServer { apiKey: this.config.apiKey, model: this.config.model ?? DEFAULT_CODEX_MODEL, reasoningEffort: this.config.reasoningEffort, - instructions: codexInstructions, + developerInstructions: codexInstructions, } : undefined, onStructuredOutput: async (output) => { diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts index 4e5a3c0190..23b14bc696 100644 --- a/packages/agent/src/types.ts +++ b/packages/agent/src/types.ts @@ -54,7 +54,11 @@ export interface TaskExecutionOptions { gatewayUrl?: string; codexBinaryPath?: string; reasoningEffort?: EffortLevel; - instructions?: string; + /** + * Codex-only. Appended on top of the model's base prompt via the Codex + * `developer_instructions` config key, preserving Codex's native base prompt. + */ + developerInstructions?: string; processCallbacks?: ProcessSpawnedCallback; /** Callback invoked when the agent calls the create_output tool for structured output */ onStructuredOutput?: (output: Record) => Promise; diff --git a/packages/workspace-server/src/services/agent/agent.ts b/packages/workspace-server/src/services/agent/agent.ts index 9a79db7ff3..f1c9419588 100644 --- a/packages/workspace-server/src/services/agent/agent.ts +++ b/packages/workspace-server/src/services/agent/agent.ts @@ -690,7 +690,8 @@ When creating pull requests, add the following footer at the end of the PR descr adapter === "codex" ? this.getCodexBinaryPath() : undefined, model, reasoningEffort: adapter === "codex" ? effort : undefined, - instructions: adapter === "codex" ? systemPrompt.append : undefined, + developerInstructions: + adapter === "codex" ? systemPrompt.append : undefined, additionalDirectories: adapter === "codex" ? additionalDirectories : undefined, onStructuredOutput: jsonSchema