Skip to content
Open
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
4 changes: 4 additions & 0 deletions src/action/triggers/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export interface TriggerExecutorDeps {
globalRequestChanges?: boolean;
/** Global fail-check from action inputs (trigger-specific takes precedence) */
globalFailCheck?: boolean;
/** Prior-phase skill reports, injected into prompts for second-pass skills */
priorReports?: SkillReport[];
}

/**
Expand Down Expand Up @@ -134,6 +136,8 @@ export async function executeTrigger(
maxTurns: trigger.maxTurns,
batchDelayMs: config.defaults?.batchDelayMs,
pathToClaudeCodeExecutable: claudePath,
priorReports: deps.priorReports,
scope: trigger.scope,
},
};

Expand Down
52 changes: 34 additions & 18 deletions src/action/workflow/pr-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { ExistingComment } from '../../output/dedup.js';
import { buildAnalyzedScope, findStaleComments, resolveStaleComments } from '../../output/stale.js';
import type { EventContext, SkillReport, Finding } from '../../types/index.js';
import { processInBatches } from '../../utils/index.js';
import { groupByPhase } from '../../pipeline/index.js';
import { evaluateFixAttempts, postThreadReply } from '../fix-evaluation/index.js';
import type { FixEvaluation } from '../fix-evaluation/index.js';
import { logAction, warnAction } from '../../cli/output/tty.js';
Expand Down Expand Up @@ -229,7 +230,9 @@ async function setupGitHubState(
}

/**
* Run all matched triggers in parallel batches.
* Run all matched triggers, grouped by phase.
* Within each phase triggers run in parallel batches.
* Prior-phase reports are threaded forward to later phases.
*/
async function executeAllTriggers(
matchedTriggers: ResolvedTrigger[],
Expand All @@ -241,23 +244,36 @@ async function executeAllTriggers(
const concurrency = config.runner?.concurrency ?? inputs.parallel;
const claudePath = await findClaudeCodeExecutable();

return processInBatches(
matchedTriggers,
(trigger) =>
executeTrigger(trigger, {
octokit,
context,
config,
anthropicApiKey: inputs.anthropicApiKey,
claudePath,
globalFailOn: inputs.failOn,
globalReportOn: inputs.reportOn,
globalMaxFindings: inputs.maxFindings,
globalRequestChanges: inputs.requestChanges,
globalFailCheck: inputs.failCheck,
}),
concurrency
);
const byPhase = groupByPhase(matchedTriggers);
const allResults: TriggerResult[] = [];
const priorReports: SkillReport[] = [];

for (const [, phaseTriggers] of byPhase) {
const results = await processInBatches(
phaseTriggers,
(trigger) =>
executeTrigger(trigger, {
octokit,
context,
config,
anthropicApiKey: inputs.anthropicApiKey,
claudePath,
globalFailOn: inputs.failOn,
globalReportOn: inputs.reportOn,
globalMaxFindings: inputs.maxFindings,
globalRequestChanges: inputs.requestChanges,
globalFailCheck: inputs.failCheck,
priorReports: priorReports.length > 0 ? priorReports : undefined,
}),
concurrency
);

const reports = results.flatMap((r) => r.report ? [r.report] : []);
priorReports.push(...reports);
allResults.push(...results);
}

return allResults;
}

/**
Expand Down
126 changes: 77 additions & 49 deletions src/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { dirname, join, resolve } from 'node:path';
import { config as dotenvConfig } from 'dotenv';
import { Sentry, flushSentry } from '../sentry.js';
import { loadWardenConfig, resolveSkillConfigs } from '../config/loader.js';
import type { SkillRunnerOptions } from '../sdk/runner.js';
import { groupByPhase } from '../pipeline/index.js';
import { resolveSkillAsync } from '../skills/loader.js';
import { matchTrigger, filterContextByPaths, shouldFail, countFindingsAtOrAbove } from '../triggers/matcher.js';
import type { SkillReport } from '../types/index.js';
Expand Down Expand Up @@ -90,6 +90,8 @@ interface SkillToRun {
skill: string;
remote?: string;
filters: { paths?: string[]; ignorePaths?: string[] };
phase?: number;
scope?: 'diff' | 'report';
}

interface ProcessedResults {
Expand Down Expand Up @@ -253,7 +255,7 @@ async function runSkills(
seen.add(t.skill);
return true;
})
.map((t) => ({ skill: t.skill, remote: t.remote, filters: t.filters }));
.map((t) => ({ skill: t.skill, remote: t.remote, filters: t.filters, phase: t.phase, scope: t.scope }));
} else {
skillsToRun = [];
}
Expand All @@ -269,41 +271,54 @@ async function runSkills(
return 0;
}

// Build skill tasks
// Build skill tasks grouped by phase
// Model precedence: defaults.model > CLI flag > WARDEN_MODEL env var > SDK default
const model = config?.defaults?.model ?? options.model ?? process.env['WARDEN_MODEL'];
const runnerOptions: SkillRunnerOptions = {
apiKey,
model,
abortController,
maxTurns: config?.defaults?.maxTurns,
batchDelayMs: config?.defaults?.batchDelayMs,
};
const tasks: SkillTaskOptions[] = skillsToRun.map(({ skill, remote, filters }) => ({
name: skill,
failOn: options.failOn,
resolveSkill: () => resolveSkillAsync(skill, repoPath, {
remote,
offline: options.offline,
}),
context: filterContextByPaths(context, filters),
runnerOptions,
}));

// Run skills with Ink UI (TTY) or simple console output (non-TTY)
const concurrency = options.parallel ?? DEFAULT_CONCURRENCY;
const taskOptions = {
mode: reporter.mode,
verbosity: reporter.verbosity,
concurrency,
};
const results = reporter.mode.isTTY
? await runSkillTasksWithInk(tasks, taskOptions)
: await runSkillTasks(tasks, taskOptions);

// Group skills by phase
const byPhase = groupByPhase(skillsToRun);

const allResults: Awaited<ReturnType<typeof runSkillTasks>> = [];
const priorReports: SkillReport[] = [];

for (const [, phaseSkills] of byPhase) {
const tasks: SkillTaskOptions[] = phaseSkills.map(({ skill, remote, filters, scope }) => ({
name: skill,
failOn: options.failOn,
resolveSkill: () => resolveSkillAsync(skill, repoPath, {
remote,
offline: options.offline,
}),
context: filterContextByPaths(context, filters),
runnerOptions: {
apiKey,
model,
abortController,
maxTurns: config?.defaults?.maxTurns,
batchDelayMs: config?.defaults?.batchDelayMs,
priorReports: priorReports.length > 0 ? priorReports : undefined,
scope,
},
}));

const results = reporter.mode.isTTY
? await runSkillTasksWithInk(tasks, taskOptions)
: await runSkillTasks(tasks, taskOptions);

const reports = results.flatMap((r) => r.report ? [r.report] : []);
priorReports.push(...reports);
allResults.push(...results);
}

// Process results and output
const totalDuration = Date.now() - startTime;
const processed = processTaskResults(results, options.reportOn);
const processed = processTaskResults(allResults, options.reportOn);
return outputResultsAndHandleFixes(processed, options, reporter, repoPath ?? cwd, totalDuration);
}

Expand Down Expand Up @@ -515,38 +530,51 @@ async function runConfigMode(options: CLIOptions, reporter: Reporter): Promise<n
reporter.debug('No API key found. Using Claude Code subscription auth.');
}

// Build trigger tasks
const tasks: SkillTaskOptions[] = triggersToRun.map((trigger) => ({
name: trigger.name,
displayName: trigger.skill,
failOn: trigger.failOn ?? options.failOn,
resolveSkill: () => resolveSkillAsync(trigger.skill, repoPath, {
remote: trigger.remote,
offline: options.offline,
}),
context: filterContextByPaths(context, trigger.filters),
runnerOptions: {
apiKey,
model: trigger.model,
abortController,
maxTurns: trigger.maxTurns,
},
}));

// Run triggers with Ink UI (TTY) or simple console output (non-TTY)
// Group triggers by phase and run each phase sequentially
const byPhase = groupByPhase(triggersToRun);
const concurrency = options.parallel ?? config.runner?.concurrency ?? DEFAULT_CONCURRENCY;
const taskOptions = {
mode: reporter.mode,
verbosity: reporter.verbosity,
concurrency,
};
const results = reporter.mode.isTTY
? await runSkillTasksWithInk(tasks, taskOptions)
: await runSkillTasks(tasks, taskOptions);

const allResults: Awaited<ReturnType<typeof runSkillTasks>> = [];
const priorReports: SkillReport[] = [];

for (const [, phaseTriggers] of byPhase) {
const tasks: SkillTaskOptions[] = phaseTriggers.map((trigger) => ({
name: trigger.name,
displayName: trigger.skill,
failOn: trigger.failOn ?? options.failOn,
resolveSkill: () => resolveSkillAsync(trigger.skill, repoPath, {
remote: trigger.remote,
offline: options.offline,
}),
context: filterContextByPaths(context, trigger.filters),
runnerOptions: {
apiKey,
model: trigger.model,
abortController,
maxTurns: trigger.maxTurns,
priorReports: priorReports.length > 0 ? priorReports : undefined,
scope: trigger.scope,
},
}));

const results = reporter.mode.isTTY
? await runSkillTasksWithInk(tasks, taskOptions)
: await runSkillTasks(tasks, taskOptions);

// Collect reports for next phase
const reports = results.flatMap((r) => r.report ? [r.report] : []);
priorReports.push(...reports);
allResults.push(...results);
}

// Process results and output
const totalDuration = Date.now() - startTime;
const processed = processTaskResults(results, options.reportOn);
const processed = processTaskResults(allResults, options.reportOn);
return outputResultsAndHandleFixes(processed, options, reporter, repoPath, totalDuration);
}

Expand Down
59 changes: 49 additions & 10 deletions src/cli/output/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Sentry, emitSkillMetrics, emitDedupMetrics, logger } from '../../sentry
import {
prepareFiles,
analyzeFile,
analyzeReport,
aggregateUsage,
aggregateAuxiliaryUsage,
deduplicateFindings,
Expand Down Expand Up @@ -176,6 +177,53 @@ export async function runSkillTask(
// Resolve the skill
const skill = await resolveSkill();

// Build PR context for inclusion in prompts (if available)
const prContext: PRPromptContext | undefined = context.pullRequest
? {
changedFiles: context.pullRequest.files.map((f) => f.filename),
title: context.pullRequest.title,
body: context.pullRequest.body,
}
: undefined;

// Report-scoped: single SDK call on prior findings, skip per-file loop
if (runnerOptions.scope === 'report') {
callbacks.onSkillStart({
name,
displayName,
status: 'running',
startTime,
files: [],
findings: [],
});

const result = await analyzeReport(skill, context.repoPath, runnerOptions, prContext);
const uniqueFindings = deduplicateFindings(result.findings);
const duration = Date.now() - startTime;

const report: SkillReport = {
skill: skill.name,
summary: generateSummary(skill.name, uniqueFindings, skill.description),
findings: uniqueFindings,
usage: result.usage,
durationMs: duration,
};
if (result.auxiliaryUsage) {
report.auxiliaryUsage = aggregateAuxiliaryUsage(result.auxiliaryUsage);
}

emitSkillMetrics(report);
callbacks.onSkillUpdate(name, {
status: 'done',
durationMs: duration,
findings: uniqueFindings,
usage: report.usage,
});
callbacks.onSkillComplete(name, report);

return { name, report, failOn };
}

// Prepare files (parse patches into hunks)
const { files: preparedFiles, skippedFiles } = prepareFiles(context, {
contextLines: runnerOptions.contextLines,
Expand Down Expand Up @@ -216,15 +264,6 @@ export async function runSkillTask(
findings: [],
});

// Build PR context for inclusion in prompts (if available)
const prContext: PRPromptContext | undefined = context.pullRequest
? {
changedFiles: context.pullRequest.files.map((f) => f.filename),
title: context.pullRequest.title,
body: context.pullRequest.body,
}
: undefined;

// Process files with concurrency
const processFile = async (prepared: PreparedFile, index: number): Promise<FileProcessResult> => {
const filename = prepared.filename;
Expand Down Expand Up @@ -334,7 +373,7 @@ export async function runSkillTask(

const report: SkillReport = {
skill: skill.name,
summary: generateSummary(skill.name, uniqueFindings),
summary: generateSummary(skill.name, uniqueFindings, skill.description),
findings: uniqueFindings,
usage: aggregateUsage(allUsage),
durationMs: duration,
Expand Down
Loading