diff --git a/packages/docs/src/pages/cli.astro b/packages/docs/src/pages/cli.astro
index d2fdd118..f7f60ae6 100644
--- a/packages/docs/src/pages/cli.astro
+++ b/packages/docs/src/pages/cli.astro
@@ -136,7 +136,7 @@ warden setup-app --org my-org # For an organization`}
-m, --model <model> - Model to use (fallback when not set in config)
-o, --output <path> - Write full run output to a JSONL file
--report-on <severity> - Only show findings at or above this severity in output
- --parallel <n> - Max concurrent trigger/skill executions (default: 4)
+ --parallel <n> - Max concurrent file analyses across running skills (default: 4)
--quiet - Errors and final summary only
-vv - Show debug info (token counts, latencies)
--color / --no-color - Override color detection
diff --git a/packages/docs/src/pages/config.astro b/packages/docs/src/pages/config.astro
index 7caf0297..c8b45079 100644
--- a/packages/docs/src/pages/config.astro
+++ b/packages/docs/src/pages/config.astro
@@ -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' },
@@ -233,6 +234,26 @@ model = "anthropic/claude-opus-4-5"`}
Model precedence: trigger > skill > defaults > CLI flag (-m) > env var (WARDEN_MODEL). Most specific wins.
Synthesis fallback: defaults.synthesis.model falls back to defaults.auxiliary.model when not set.
+ Runner
+
+ Runner settings control run-level concurrency.
+
+
+ - concurrency
+ - Maximum concurrent file analyses across running CLI skills. In GitHub Actions, also limits matched trigger dispatch. If unset, the CLI uses
4 and the Action uses the workflow parallel input.
+
+
+
+
+
+
+ For CLI runs, --parallel overrides runner.concurrency. In GitHub Actions, runner.concurrency overrides the workflow parallel input.
+
Chunking
Control how files are split for analysis. By default, Warden analyzes each hunk separately.
@@ -562,7 +583,7 @@ jobs:
fail-check
Whether to fail the check run. Default: false
parallel
- Maximum concurrent trigger executions. Default: 5
+ Maximum concurrent matched trigger executions and file analyses, unless runner.concurrency is set. Default: 5
diff --git a/src/action/workflow/pr-workflow.test.ts b/src/action/workflow/pr-workflow.test.ts
index 5c7bf79f..d06972f7 100644
--- a/src/action/workflow/pr-workflow.test.ts
+++ b/src/action/workflow/pr-workflow.test.ts
@@ -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((resolve) => {
+ resolveFirstRun = resolve;
+ });
+ const firstRunStarted = new Promise((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.
diff --git a/src/action/workflow/pr-workflow.ts b/src/action/workflow/pr-workflow.ts
index a4aba836..04d38764 100644
--- a/src/action/workflow/pr-workflow.ts
+++ b/src/action/workflow/pr-workflow.ts
@@ -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,
diff --git a/src/cli/args.ts b/src/cli/args.ts
index 5cc8855c..d5a50a78 100644
--- a/src/cli/args.ts
+++ b/src/cli/args.ts
@@ -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(),
diff --git a/src/cli/help.ts b/src/cli/help.ts
index 0910b633..04ccde25 100644
--- a/src/cli/help.ts
+++ b/src/cli/help.ts
@@ -121,7 +121,7 @@ const HELP_OPTIONS: Record = {
},
parallel: {
label: '--parallel ',
- description: 'Max concurrent task or skill executions',
+ description: 'Max concurrent file analyses across running skills',
},
failFast: {
label: '-x, --fail-fast',