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
2 changes: 2 additions & 0 deletions src/action/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ async function runScheduledAnalysis(
const report = await runSkill(skill, context, {
apiKey: inputs.anthropicApiKey,
model: resolved.model,
maxTurns: trigger.maxTurns ?? config.defaults?.maxTurns,
pathToClaudeCodeExecutable: claudePath,
});
console.log(`Found ${report.findings.length} findings`);
Expand Down Expand Up @@ -518,6 +519,7 @@ async function run(): Promise<void> {
const report = await runSkill(skill, context, {
apiKey: inputs.anthropicApiKey,
model: trigger.model,
maxTurns: trigger.maxTurns ?? config.defaults?.maxTurns,
pathToClaudeCodeExecutable: claudePath,
});
console.log(`Found ${report.findings.length} findings`);
Expand Down
11 changes: 9 additions & 2 deletions src/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ async function runSkills(
let repoPath: string | undefined;
let skillsConfig: SkillDefinition[] | undefined;
let defaultsModel: string | undefined;
let defaultsMaxTurns: number | undefined;

try {
repoPath = getRepoRoot(cwd);
Expand All @@ -114,6 +115,7 @@ async function runSkills(
const config = loadWardenConfig(dirname(configPath));
skillsConfig = config.skills;
defaultsModel = config.defaults?.model;
defaultsMaxTurns = config.defaults?.maxTurns;
}
} catch {
// Not in a git repo or no config - that's fine
Expand All @@ -122,7 +124,7 @@ async function runSkills(
// Build skill tasks
// Model precedence: defaults.model > CLI flag > WARDEN_MODEL env var > SDK default
const model = defaultsModel ?? options.model ?? process.env['WARDEN_MODEL'];
const runnerOptions: SkillRunnerOptions = { apiKey, model, abortController };
const runnerOptions: SkillRunnerOptions = { apiKey, model, abortController, maxTurns: defaultsMaxTurns };
const tasks: SkillTaskOptions[] = skillNames.map((skillName) => ({
name: skillName,
failOn: options.failOn,
Expand Down Expand Up @@ -432,7 +434,12 @@ async function runConfigMode(options: CLIOptions, reporter: Reporter): Promise<n
failOn: trigger.output.failOn ?? options.failOn,
resolveSkill: () => resolveSkillAsync(trigger.skill, repoPath, config.skills),
context,
runnerOptions: { apiKey, model: trigger.model, abortController },
runnerOptions: {
apiKey,
model: trigger.model,
abortController,
maxTurns: trigger.maxTurns ?? config.defaults?.maxTurns,
},
}));

// Run triggers with listr2
Expand Down
63 changes: 62 additions & 1 deletion src/config/loader.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { resolveTrigger } from './loader.js';
import type { Trigger, WardenConfig } from './schema.js';
import { WardenConfigSchema, type Trigger, type WardenConfig } from './schema.js';

describe('resolveTrigger', () => {
const baseTrigger: Trigger = {
Expand Down Expand Up @@ -194,3 +194,64 @@ describe('resolveTrigger', () => {
});
});
});

describe('maxTurns config', () => {
it('accepts maxTurns in defaults', () => {
const config = {
version: 1,
defaults: {
maxTurns: 25,
},
triggers: [],
};

const result = WardenConfigSchema.safeParse(config);
expect(result.success).toBe(true);
expect(result.data?.defaults?.maxTurns).toBe(25);
});

it('accepts maxTurns in trigger', () => {
const config = {
version: 1,
triggers: [
{
name: 'test',
event: 'pull_request',
actions: ['opened'],
skill: 'security-review',
maxTurns: 30,
},
],
};

const result = WardenConfigSchema.safeParse(config);
expect(result.success).toBe(true);
expect(result.data?.triggers[0]?.maxTurns).toBe(30);
});

it('rejects non-positive maxTurns', () => {
const config = {
version: 1,
defaults: {
maxTurns: 0,
},
triggers: [],
};

const result = WardenConfigSchema.safeParse(config);
expect(result.success).toBe(false);
});

it('rejects non-integer maxTurns', () => {
const config = {
version: 1,
defaults: {
maxTurns: 10.5,
},
triggers: [],
};

const result = WardenConfigSchema.safeParse(config);
expect(result.success).toBe(false);
});
});
4 changes: 4 additions & 0 deletions src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ export const TriggerSchema = z.object({
output: OutputConfigSchema.optional(),
/** Model to use for this trigger (e.g., 'claude-sonnet-4-20250514'). Uses SDK default if not specified. */
model: z.string().optional(),
/** Maximum agentic turns (API round-trips) per hunk analysis. Overrides defaults.maxTurns. */
maxTurns: z.number().int().positive().optional(),
/** Schedule-specific configuration. Only used when event is 'schedule'. */
schedule: ScheduleConfigSchema.optional(),
}).refine(
Expand Down Expand Up @@ -114,6 +116,8 @@ export const DefaultsSchema = z.object({
output: OutputConfigSchema.optional(),
/** Default model for all triggers (e.g., 'claude-sonnet-4-20250514') */
model: z.string().optional(),
/** Maximum agentic turns (API round-trips) per hunk analysis. Default: 50 */
maxTurns: z.number().int().positive().optional(),
});
export type Defaults = z.infer<typeof DefaultsSchema>;

Expand Down
2 changes: 1 addition & 1 deletion src/sdk/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@
repoPath: string,
options: SkillRunnerOptions
): Promise<HunkAnalysisResult> {
const { maxTurns = 5, model, abortController, pathToClaudeCodeExecutable } = options;
const { maxTurns = 50, model, abortController, pathToClaudeCodeExecutable } = options;

Check warning on line 239 in src/sdk/runner.ts

View check run for this annotation

@sentry/warden / warden: find-bugs

Excessive default maxTurns increase from 5 to 50

The default value for maxTurns has been increased 10x from 5 to 50. This change could lead to significantly increased resource consumption (API calls, compute time, costs) and potential denial of service if the analysis enters an unexpected loop or takes an unusually long path. The original value of 5 was likely a deliberate safeguard.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 10x increase in maxTurns default may cause resource exhaustion

The default maxTurns value was increased from 5 to 50. This 10x increase could lead to significantly longer execution times and higher API costs if the analysis gets stuck in a loop or takes many iterations. While 5 turns may have been too restrictive for complex analysis, 50 turns without any timeout or cost guard could be excessive.

Suggested fix: Consider a more moderate increase (e.g., 15-20) or add a cost/time guard to prevent runaway analysis. Also consider making this configurable per-skill if different skills have different complexity needs.

Suggested change
const { maxTurns = 50, model, abortController, pathToClaudeCodeExecutable } = options;
const { maxTurns = 15, model, abortController, pathToClaudeCodeExecutable } = options;

warden: find-bugs


const systemPrompt = buildHunkSystemPrompt(skill);
const userPrompt = buildHunkUserPrompt(hunkCtx);
Expand Down