Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,20 @@ function buildEnvBase(cwd: string, env: ResolvedEnv): ProjectRuntimeConfig {
"CODEX_REASONING_EFFORT_REVIEW_TEST",
),
},
fastModes: {
plan: normalizeBooleanEnvValue(
env.CODEX_FAST_MODE_PLAN,
"CODEX_FAST_MODE_PLAN",
),
implement: normalizeBooleanEnvValue(
env.CODEX_FAST_MODE_IMPLEMENT,
"CODEX_FAST_MODE_IMPLEMENT",
),
reviewTest: normalizeBooleanEnvValue(
env.CODEX_FAST_MODE_REVIEW_TEST,
"CODEX_FAST_MODE_REVIEW_TEST",
),
},
sandbox,
codexHome,
},
Expand Down Expand Up @@ -788,6 +802,11 @@ function mergeRuntime(
...(rootDefaults.codex?.reasoningEfforts ?? {}),
...(project.codex?.reasoningEfforts ?? {}),
},
fastModes: {
...(base.codex.fastModes ?? {}),
...(rootDefaults.codex?.fastModes ?? {}),
...(project.codex?.fastModes ?? {}),
},
},
skills: {
root: skillRoot,
Expand Down Expand Up @@ -983,6 +1002,30 @@ function normalizeReasoningEffortValue(
);
}

function normalizeBooleanEnvValue(
input: string | undefined,
envName: string,
): boolean | undefined {
if (!input) {
return undefined;
}

const value = input.trim().toLowerCase();
if (!value || value === "off" || value === "none") {
return undefined;
}
if (value === "1" || value === "true" || value === "yes" || value === "on") {
return true;
}
if (value === "0" || value === "false" || value === "no") {
return false;
}

throw new Error(
`Invalid ${envName} value '${input}'. Use true/false, 1/0, yes/no, or leave empty.`,
);
}

function validateProject(project: ResolvedProjectConfig): void {
if (!project.linear.apiKey) {
throw new Error(`LINEAR_API_KEY is required for project '${project.id}'`);
Expand Down
5 changes: 5 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ export interface ProjectRuntimeConfig {
implement?: CodexReasoningEffort;
reviewTest?: CodexReasoningEffort;
};
fastModes?: {
plan?: boolean;
implement?: boolean;
reviewTest?: boolean;
};
plugins?: string[];
skillsets?: string[];
/**
Expand Down
25 changes: 22 additions & 3 deletions src/services/codex-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ export class CodexAdapter implements AgentAdapter {
const reasoningEffort =
this.config.codex.reasoningEfforts?.plan ??
this.config.codex.reasoningEffort;
const fastModeEnabled = this.config.codex.fastModes?.plan;
return this.runCodex(
this.buildExecArgs(
prompt,
await this.nextOutputFile(),
model,
reasoningEffort,
fastModeEnabled,
),
);
}
Expand All @@ -31,13 +33,15 @@ export class CodexAdapter implements AgentAdapter {
const reasoningEffort =
this.config.codex.reasoningEfforts?.implement ??
this.config.codex.reasoningEffort;
const fastModeEnabled = this.config.codex.fastModes?.implement;
return this.runCodex(
this.buildResumeArgs(
sessionId,
prompt,
await this.nextOutputFile(),
model,
reasoningEffort,
fastModeEnabled,
),
);
}
Expand All @@ -51,12 +55,16 @@ export class CodexAdapter implements AgentAdapter {
this.config.codex.reasoningEfforts?.reviewTest ??
this.config.codex.reasoningEfforts?.implement ??
this.config.codex.reasoningEffort;
const fastModeEnabled =
this.config.codex.fastModes?.reviewTest ??
this.config.codex.fastModes?.implement;
return this.runCodex(
this.buildExecArgs(
prompt,
await this.nextOutputFile(),
model,
reasoningEffort,
fastModeEnabled,
),
);
}
Expand All @@ -66,6 +74,7 @@ export class CodexAdapter implements AgentAdapter {
outputFile: string,
modelOverride?: string,
reasoningEffortOverride?: CodexReasoningEffort,
fastModeEnabled?: boolean,
): string[] {
const args = [
"exec",
Expand All @@ -83,7 +92,7 @@ export class CodexAdapter implements AgentAdapter {
if (this.config.codex.sandbox) {
args.push("--sandbox", this.config.codex.sandbox);
}
this.appendConfigArgs(args, reasoningEffortOverride);
this.appendConfigArgs(args, reasoningEffortOverride, fastModeEnabled);
args.push(prompt);
return args;
}
Expand All @@ -94,6 +103,7 @@ export class CodexAdapter implements AgentAdapter {
outputFile: string,
modelOverride?: string,
reasoningEffortOverride?: CodexReasoningEffort,
fastModeEnabled?: boolean,
): string[] {
const args = [
"exec",
Expand All @@ -107,7 +117,7 @@ export class CodexAdapter implements AgentAdapter {
if (model) {
args.push("--model", model);
}
this.appendConfigArgs(args, reasoningEffortOverride);
this.appendConfigArgs(args, reasoningEffortOverride, fastModeEnabled);
args.push(sessionId, prompt);
return args;
}
Expand Down Expand Up @@ -149,14 +159,19 @@ export class CodexAdapter implements AgentAdapter {
private appendConfigArgs(
args: string[],
reasoningEffortOverride?: CodexReasoningEffort,
fastModeEnabled?: boolean,
): void {
for (const override of this.buildConfigOverrides(reasoningEffortOverride)) {
for (const override of this.buildConfigOverrides(
reasoningEffortOverride,
fastModeEnabled,
)) {
args.push("--config", override);
}
}

private buildConfigOverrides(
reasoningEffortOverride?: CodexReasoningEffort,
fastModeEnabled?: boolean,
): string[] {
const overrides: string[] = [];
const plugins = normalizeList(this.config.codex.plugins);
Expand All @@ -174,6 +189,10 @@ export class CodexAdapter implements AgentAdapter {
`model_reasoning_effort=${JSON.stringify(reasoningEffortOverride)}`,
);
}
if (fastModeEnabled) {
overrides.push('service_tier="fast"');
overrides.push("features.fast_mode=true");
}
for (const [rawKey, rawValue] of Object.entries(
this.config.codex.configOverrides ?? {},
)) {
Expand Down
59 changes: 58 additions & 1 deletion tests/codex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ const config: ResolvedProjectConfig = {
plan: "high",
implement: "low",
},
fastModes: {
plan: true,
implement: false,
reviewTest: true,
},
plugins: ["github@openai-curated", "linear@openai-curated"],
skillsets: ["adhd-ai", "repo-defaults"],
configOverrides: {
Expand Down Expand Up @@ -128,6 +133,32 @@ describe("codex adapter", () => {
expect(calls[2]).toContain('model_reasoning_effort="low"');
});

it("uses stage-specific fast mode overrides", async () => {
const adapter = new CodexAdapter(config);
const calls: string[][] = [];
(
adapter as unknown as { runCodex: (args: string[]) => Promise<unknown> }
).runCodex = async (args: string[]) => {
calls.push(args);
return { finalMessage: "", stdout: "" };
};
(
adapter as unknown as { nextOutputFile: () => Promise<string> }
).nextOutputFile = async () => "/tmp/out.txt";

await adapter.runPlan("plan prompt");
await adapter.resume("session-1", "implement prompt");
await adapter.runReview("review prompt");

expect(calls).toHaveLength(3);
expect(calls[0]).toContain('service_tier="fast"');
expect(calls[0]).toContain("features.fast_mode=true");
expect(calls[1]).not.toContain('service_tier="fast"');
expect(calls[1]).not.toContain("features.fast_mode=true");
expect(calls[2]).toContain('service_tier="fast"');
expect(calls[2]).toContain("features.fast_mode=true");
});

it("keeps raw config override as final reasoning-effort escape hatch", () => {
const adapter = new CodexAdapter({
...config,
Expand All @@ -140,12 +171,38 @@ describe("codex adapter", () => {
});
const overrides = (
adapter as unknown as {
buildConfigOverrides: (effort?: string) => string[];
buildConfigOverrides: (effort?: string, fastMode?: boolean) => string[];
}
).buildConfigOverrides("low");
const first = overrides.indexOf('model_reasoning_effort="low"');
const second = overrides.indexOf('model_reasoning_effort="xhigh"');
expect(first).toBeGreaterThanOrEqual(0);
expect(second).toBeGreaterThan(first);
});

it("keeps raw config override as final fast-mode escape hatch", () => {
const adapter = new CodexAdapter({
...config,
codex: {
...config.codex,
configOverrides: {
service_tier: '"default"',
"features.fast_mode": "false",
},
},
});
const overrides = (
adapter as unknown as {
buildConfigOverrides: (effort?: string, fastMode?: boolean) => string[];
}
).buildConfigOverrides(undefined, true);
const fastTier = overrides.indexOf('service_tier="fast"');
const defaultTier = overrides.indexOf('service_tier="default"');
const fastModeTrue = overrides.indexOf("features.fast_mode=true");
const fastModeFalse = overrides.indexOf("features.fast_mode=false");
expect(fastTier).toBeGreaterThanOrEqual(0);
expect(defaultTier).toBeGreaterThan(fastTier);
expect(fastModeTrue).toBeGreaterThanOrEqual(0);
expect(fastModeFalse).toBeGreaterThan(fastModeTrue);
});
});
82 changes: 82 additions & 0 deletions tests/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
"CODEX_REASONING_EFFORT_PLAN",
"CODEX_REASONING_EFFORT_IMPLEMENT",
"CODEX_REASONING_EFFORT_REVIEW_TEST",
"CODEX_FAST_MODE_PLAN",
"CODEX_FAST_MODE_IMPLEMENT",
"CODEX_FAST_MODE_REVIEW_TEST",
"CODEX_MODEL_PLAN",
"CODEX_MODEL_IMPLEMENT",
"CODEX_MODEL_REVIEW_TEST",
Expand Down Expand Up @@ -58,6 +61,9 @@
key === "CODEX_REASONING_EFFORT_PLAN" ||
key === "CODEX_REASONING_EFFORT_IMPLEMENT" ||
key === "CODEX_REASONING_EFFORT_REVIEW_TEST" ||
key === "CODEX_FAST_MODE_PLAN" ||
key === "CODEX_FAST_MODE_IMPLEMENT" ||
key === "CODEX_FAST_MODE_REVIEW_TEST" ||
key === "CLAUDE_CODE_MODEL" ||
key === "CLAUDE_CODE_ALLOWED_TOOLS"
? ""
Expand Down Expand Up @@ -222,7 +228,7 @@

it("loads default hourly review and daily maintenance cron jobs", async () => {
const config = await loadConfig(process.cwd());
expect(config.cron.jobs).toEqual([

Check failure on line 231 in tests/config.test.ts

View workflow job for this annotation

GitHub Actions / Check, typecheck, and test

error: expect(received).toEqual(expected)

[ { "enabled": true, "id": "hourly-pr-review", "name": "Hourly PR Review", "run": { "allProjects": true, "exitWhenIdle": undefined, "issueArg": undefined, "maxPollCycles": undefined, "poll": undefined, "pollIntervalMs": undefined, "projectId": undefined, "reviewOnly": true, }, "schedule": { "every": 1, "frequency": "hourly", "minute": 0, }, }, { "enabled": true, "id": "daily-codebase-maintenance", "name": "Daily Codebase Maintenance", "run": { "allProjects": true, "exitWhenIdle": true, "issueArg": undefined, "maxPollCycles": 1, "poll": true, "pollIntervalMs": undefined, "projectId": undefined, "reviewOnly": undefined, }, "schedule": { "frequency": "daily", "time": "09:00", }, + "skills": { + "implement": "daily-codebase-maintenance/SKILL.md", + "plan": undefined, + "reviewTest": undefined, + }, }, ] - Expected - 0 + Received + 5 at <anonymous> (/home/runner/work/ADHD.ai/ADHD.ai/tests/config.test.ts:231:28)
{
id: "hourly-pr-review",
name: "Hourly PR Review",
Expand Down Expand Up @@ -591,6 +597,82 @@
}
});

it("loads stage-specific codex fast mode from env", async () => {
process.env.CODEX_FAST_MODE_PLAN = "true";
process.env.CODEX_FAST_MODE_IMPLEMENT = "1";
process.env.CODEX_FAST_MODE_REVIEW_TEST = "no";
const tempDir = await mkdtemp(
path.join(process.cwd(), ".tmp-config-test-"),
);
await writeFile(
path.join(tempDir, "adhd-ai.config.ts"),
["export default { projects: [{ id: 'default' }] };", ""].join("\n"),
);

try {
const config = await loadConfig(tempDir);
expect(config.projects[0]?.codex.fastModes).toEqual({
plan: true,
implement: true,
reviewTest: false,
});
} finally {
await rm(tempDir, { recursive: true, force: true });
}
});

it("preserves env fast mode values when config overrides only plan", async () => {
process.env.CODEX_FAST_MODE_IMPLEMENT = "true";
process.env.CODEX_FAST_MODE_REVIEW_TEST = "false";
const tempDir = await mkdtemp(
path.join(process.cwd(), ".tmp-config-test-"),
);
await writeFile(
path.join(tempDir, "adhd-ai.config.ts"),
[
"export default {",
" codex: {",
" fastModes: {",
" plan: true",
" }",
" },",
" projects: [{ id: 'default' }]",
"};",
"",
].join("\n"),
);

try {
const config = await loadConfig(tempDir);
expect(config.projects[0]?.codex.fastModes).toEqual({
plan: true,
implement: true,
reviewTest: false,
});
} finally {
await rm(tempDir, { recursive: true, force: true });
}
});

it("throws on invalid codex fast mode env value", async () => {
process.env.CODEX_FAST_MODE_PLAN = "maybe";
const tempDir = await mkdtemp(
path.join(process.cwd(), ".tmp-config-test-"),
);
await writeFile(
path.join(tempDir, "adhd-ai.config.ts"),
["export default { projects: [{ id: 'default' }] };", ""].join("\n"),
);

try {
await expect(loadConfig(tempDir)).rejects.toThrow(
"Invalid CODEX_FAST_MODE_PLAN value",
);
} finally {
await rm(tempDir, { recursive: true, force: true });
}
});

it("does not stream codex logs by default", async () => {
process.env.PIV_DEV_MODE = "0";
process.env.PIV_PRINT_CODEX_LOGS = "0";
Expand Down
Loading