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: 1 addition & 1 deletion packages/docs/src/pages/cli.astro
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ warden setup-app --org my-org # For an organization`}
<li><code>-m, --model &lt;model&gt;</code> - Model to use (fallback when not set in config)</li>
<li><code>-o, --output &lt;path&gt;</code> - Write full run output to a JSONL file</li>
<li><code>--report-on &lt;severity&gt;</code> - Only show findings at or above this severity in output</li>
<li><code>--parallel &lt;n&gt;</code> - Max concurrent trigger/skill executions (default: 4)</li>
<li><code>--parallel &lt;n&gt;</code> - Max concurrent file analyses across running skills (default: 4)</li>
<li><code>--quiet</code> - Errors and final summary only</li>
<li><code>-vv</code> - Show debug info (token counts, latencies)</li>
<li><code>--color / --no-color</code> - Override color detection</li>
Expand Down
23 changes: 22 additions & 1 deletion packages/docs/src/pages/config.astro
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const tocItems = [
{ href: '#filters', title: 'Filters' },
{ href: '#output', title: 'Output' },
{ href: '#defaults', title: 'Defaults' },
{ href: '#runner', title: 'Runner' },
{ href: '#chunking', title: 'Chunking' },
{ href: '#schedule-triggers', title: 'Schedule Triggers' },
{ href: '#environment-variables', title: 'Environment Variables' },
Expand Down Expand Up @@ -233,6 +234,26 @@ model = "anthropic/claude-opus-4-5"`}
<p><strong>Model precedence:</strong> trigger &gt; skill &gt; defaults &gt; CLI flag (<code>-m</code>) &gt; env var (<code>WARDEN_MODEL</code>). Most specific wins.</p>
<p><strong>Synthesis fallback:</strong> <code>defaults.synthesis.model</code> falls back to <code>defaults.auxiliary.model</code> when not set.</p>

<h2 id="runner">Runner</h2>

<p>Runner settings control run-level concurrency.</p>

<dl>
<dt>concurrency</dt>
<dd>Maximum concurrent file analyses across running CLI skills. In GitHub Actions, also limits matched trigger dispatch. If unset, the CLI uses <code>4</code> and the Action uses the workflow <code>parallel</code> input.</dd>
</dl>

<Terminal showCopy={true}>
<Code
code={`[runner]
concurrency = 1`}
lang="toml"
theme="vitesse-black"
/>
</Terminal>

<p>For CLI runs, <code>--parallel</code> overrides <code>runner.concurrency</code>. In GitHub Actions, <code>runner.concurrency</code> overrides the workflow <code>parallel</code> input.</p>

<h2 id="chunking">Chunking</h2>

<p>Control how files are split for analysis. By default, Warden analyzes each hunk separately.</p>
Expand Down Expand Up @@ -562,7 +583,7 @@ jobs:
<dt>fail-check</dt>
<dd>Whether to fail the check run. Default: <code>false</code></dd>
<dt>parallel</dt>
<dd>Maximum concurrent trigger executions. Default: <code>5</code></dd>
<dd>Maximum concurrent matched trigger executions and file analyses, unless <code>runner.concurrency</code> is set. Default: <code>5</code></dd>
</dl>
</DocsPageShell>
</Base>
55 changes: 55 additions & 0 deletions src/action/workflow/pr-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,61 @@ describe('runPRWorkflow', () => {
expect(semaphore).toBeInstanceOf(Semaphore);
});

it('honors the parallel input when dispatching matched triggers', async () => {
let activeRuns = 0;
let maxActiveRuns = 0;
let invocationCount = 0;
let resolveFirstRun!: () => void;
let resolveFirstRunStarted!: () => void;
const firstRun = new Promise<void>((resolve) => {
resolveFirstRun = resolve;
});
const firstRunStarted = new Promise<void>((resolve) => {
resolveFirstRunStarted = resolve;
});

mockRunSkillTask.mockImplementation(async (taskOptions) => {
invocationCount++;
activeRuns++;
maxActiveRuns = Math.max(maxActiveRuns, activeRuns);
try {
if (invocationCount === 1) {
resolveFirstRunStarted();
await firstRun;
}
return {
name: taskOptions.name,
report: createSkillReport({ skill: taskOptions.displayName ?? taskOptions.name }),
};
} finally {
activeRuns--;
}
});

const workflow = runPRWorkflow(
mockOctokit,
createDefaultInputs({
baseConfigPath: '.warden-org/warden.toml',
baseSkillRoot: '.warden-org',
parallel: 1,
}),
'pull_request',
EVENT_PAYLOAD_PATH,
FIXTURES_DIR
);

await firstRunStarted;
// Let any incorrectly dispatched second trigger reach the mocked runner.
await new Promise((resolve) => setTimeout(resolve, 0));
const callsBeforeFirstRunFinished = mockRunSkillTask.mock.calls.length;
resolveFirstRun();
await workflow;

expect(mockRunSkillTask).toHaveBeenCalledTimes(2);
expect(callsBeforeFirstRunFinished).toBe(1);
expect(maxActiveRuns).toBe(1);
});

it('records trigger failure and updates check before failing', async () => {
// When all triggers fail, the workflow should still update the check
// before calling setFailed.
Expand Down
5 changes: 2 additions & 3 deletions src/action/workflow/pr-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,15 +319,14 @@ async function executeAllTriggers(
}
const claudePath = usesClaudeRuntime ? await findClaudeCodeExecutable() : undefined;

// Global semaphore gates file-level work across all triggers.
// All triggers launch immediately; the semaphore limits concurrent file analyses.
const semaphore = new Semaphore(concurrency);
const abortController = new AbortController();
const circuitBreaker = new ProviderFailureCircuitBreaker({ abortController });

// Limit trigger dispatch too; the semaphore only gates work after a trigger starts.
return runPool(
matchedTriggers,
matchedTriggers.length,
concurrency,
(trigger) =>
executeTrigger(trigger, {
octokit,
Expand Down
2 changes: 1 addition & 1 deletion src/cli/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const CLIOptionsSchema = z.object({
/** Only show findings at or above this confidence in output */
minConfidence: ConfidenceThresholdSchema.optional(),
help: z.boolean().default(false),
/** Max concurrent task or skill executions (default depends on command) */
/** Max concurrent file analyses across running skills (default depends on command) */
parallel: z.number().int().positive().optional(),
/** Model to use for analysis (fallback when not set in config) */
model: z.string().optional(),
Expand Down
2 changes: 1 addition & 1 deletion src/cli/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ const HELP_OPTIONS: Record<HelpOptionId, HelpOptionSpec> = {
},
parallel: {
label: '--parallel <n>',
description: 'Max concurrent task or skill executions',
description: 'Max concurrent file analyses across running skills',
},
failFast: {
label: '-x, --fail-fast',
Expand Down
Loading