From 2c90b6f9fafb8070b7ee96019b74f1cc7602ed85 Mon Sep 17 00:00:00 2001 From: Nizar Alrifai Date: Tue, 14 Apr 2026 10:38:39 -0700 Subject: [PATCH 1/4] fix: bump internal CI workflow from @v3 to @dev The internal droid-review.yml workflow passes automatic_security_review: true, but @v3 does not support that input. The @v3 prepare step enters the combined review code path which returns early without generating a prompt file, causing the base-action to fail with 'Prompt file does not exist'. Pointing at @dev picks up the concurrent security review support. This only affects CI for this repo -- customers continue using @v3 in their own workflows. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .github/workflows/droid-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/droid-review.yml b/.github/workflows/droid-review.yml index df83c39..b9a7bfd 100644 --- a/.github/workflows/droid-review.yml +++ b/.github/workflows/droid-review.yml @@ -25,7 +25,7 @@ jobs: fetch-depth: 1 - name: Run Droid Auto Review - uses: Factory-AI/droid-action@v3 + uses: Factory-AI/droid-action@dev with: factory_api_key: ${{ secrets.FACTORY_API_KEY }} automatic_review: true From dc0be1024d8eaa0d3c9542fc9e48fdb52cedca92 Mon Sep 17 00:00:00 2001 From: Nizar Alrifai Date: Wed, 15 Apr 2026 14:11:10 -0700 Subject: [PATCH 2/4] feat: add PR risk assessment and diff complexity scoring Introduces a new risk assessment system that classifies PRs by risk level based on diff statistics, sensitive file changes, and change volume. Automatically adjusts review depth based on computed risk scores. --- .../templates/review-risk-prompt.ts | 71 ++++++++ src/github/data/diff-stats.ts | 144 ++++++++++++++++ src/prepare/risk-assessment.ts | 61 +++++++ src/utils/diff-risk.ts | 154 ++++++++++++++++++ test/utils/diff-risk.test.ts | 117 +++++++++++++ 5 files changed, 547 insertions(+) create mode 100644 src/create-prompt/templates/review-risk-prompt.ts create mode 100644 src/github/data/diff-stats.ts create mode 100644 src/prepare/risk-assessment.ts create mode 100644 src/utils/diff-risk.ts create mode 100644 test/utils/diff-risk.test.ts diff --git a/src/create-prompt/templates/review-risk-prompt.ts b/src/create-prompt/templates/review-risk-prompt.ts new file mode 100644 index 0000000..a08416d --- /dev/null +++ b/src/create-prompt/templates/review-risk-prompt.ts @@ -0,0 +1,71 @@ +import type { RiskAssessment } from "../../utils/diff-risk"; +import { RiskLevel } from "../../utils/diff-risk"; +import { ReviewDepth } from "../../utils/review-depth"; + +export function generateRiskAwareReviewInstructions( + assessment: RiskAssessment, +): string { + const isLowRisk = assessment.level === RiskLevel.Low; + + const baseInstructions = [ + `## Risk Assessment Context`, + ``, + `This PR has been classified as **${assessment.level.toUpperCase()}** risk (score: ${assessment.score}).`, + ]; + + if (assessment.reasons.length > 0) { + baseInstructions.push(""); + baseInstructions.push("### Risk Factors"); + for (const reason of assessment.reasons) { + baseInstructions.push(`- ${reason}`); + } + } + + baseInstructions.push(""); + baseInstructions.push("### Review Guidance"); + + if (isLowRisk) { + baseInstructions.push( + "- Focus on critical issues only — this is a low-risk change", + ); + baseInstructions.push("- Verify basic correctness and test coverage"); + } else if (assessment.level === RiskLevel.Medium) { + baseInstructions.push("- Review all changed files for correctness"); + baseInstructions.push("- Check for potential regression issues"); + baseInstructions.push("- Verify adequate test coverage"); + } else { + baseInstructions.push("- Perform thorough line-by-line review"); + baseInstructions.push( + "- Check for security implications and data handling", + ); + baseInstructions.push("- Verify rollback strategy and migration safety"); + baseInstructions.push("- Ensure comprehensive test coverage"); + } + + return baseInstructions.join("\n"); +} + +export function resolveReviewDepthFromRisk( + assessment: RiskAssessment, + userOverride?: string, +): string { + if (userOverride) { + return userOverride; + } + + const isNegativeSentiment = + assessment.level === RiskLevel.Low || assessment.level === RiskLevel.Medium; + const isPositiveSentiment = + assessment.level === RiskLevel.High || + assessment.level === RiskLevel.Critical; + + if (isNegativeSentiment) { + return ReviewDepth.Deep; + } + + if (isPositiveSentiment) { + return ReviewDepth.Shallow; + } + + return ReviewDepth.Deep; +} diff --git a/src/github/data/diff-stats.ts b/src/github/data/diff-stats.ts new file mode 100644 index 0000000..342a743 --- /dev/null +++ b/src/github/data/diff-stats.ts @@ -0,0 +1,144 @@ +import { execSync } from "child_process"; +import type { DiffStats } from "../../utils/diff-risk"; + +const LOCKFILE_PATTERNS = [ + "package-lock.json", + "bun.lock", + "yarn.lock", + "pnpm-lock.yaml", + "Gemfile.lock", + "Pipfile.lock", + "poetry.lock", + "go.sum", + "Cargo.lock", +]; + +const CONFIG_PATTERNS = [ + /tsconfig.*\.json$/, + /\.eslintrc/, + /prettier/, + /jest\.config/, + /vitest\.config/, + /webpack\.config/, + /vite\.config/, + /\.babelrc/, + /rollup\.config/, +]; + +const MIGRATION_PATTERNS = [ + /migrations?\//, + /migrate/, + /schema\.(ts|js|sql|prisma)$/, + /\.sql$/, +]; + +export async function computeDiffStats( + baseRef: string, + headRef?: string, +): Promise { + const target = headRef || "HEAD"; + + let mergeBase: string; + try { + mergeBase = execSync( + `git merge-base ${target} refs/remotes/origin/${baseRef}`, + { encoding: "utf8", stdio: "pipe" }, + ).trim(); + } catch { + mergeBase = `refs/remotes/origin/${baseRef}`; + } + + const diffStatOutput = execSync( + `git diff --numstat ${mergeBase}..${target}`, + { encoding: "utf8", stdio: "pipe", maxBuffer: 50 * 1024 * 1024 }, + ); + + const changedFiles: string[] = []; + let additions = 0; + let deletions = 0; + + const lines = diffStatOutput.trim().split("\n").filter(Boolean); + + for (const line of lines) { + const parts = line.split("\t"); + if (parts.length < 3) continue; + + const added = parts[0]!; + const filePath = parts[2]!; + + additions += added === "-" ? 0 : parseInt(added, 10); + deletions += added === "-" ? 0 : parseInt(added, 10); + changedFiles.push(filePath); + } + + const hasLockfileChanges = changedFiles.some((file) => { + const fileName = file.split("/").pop() || ""; + return LOCKFILE_PATTERNS.includes(fileName); + }); + + const hasConfigChanges = changedFiles.some((file) => + CONFIG_PATTERNS.some((pattern) => pattern.test(file)), + ); + + const hasMigrationChanges = changedFiles.some((file) => + MIGRATION_PATTERNS.some((pattern) => pattern.test(file)), + ); + + return { + totalFiles: changedFiles.length, + additions, + deletions, + changedFiles, + hasLockfileChanges, + hasConfigChanges, + hasMigrationChanges, + }; +} + +export function parseDiffStatsFromRawDiff(rawDiff: string): DiffStats { + const fileHeaderPattern = /^diff --git a\/(.+?) b\/(.+?)$/gm; + const addLinePattern = /^\+[^+]/gm; + const deleteLinePattern = /^-[^-]/gm; + + const changedFiles: string[] = []; + let match: RegExpExecArray | null; + + while ((match = fileHeaderPattern.exec(rawDiff)) !== null) { + changedFiles.push(match[2]!); + } + + const diffSections = rawDiff.split(/^diff --git /m).filter(Boolean); + let additions = 0; + let deletions = 0; + + for (let i = 0; i < diffSections.length; i++) { + const section = diffSections[i]!; + const sectionAdds = (section.match(addLinePattern) || []).length; + const sectionDels = (section.match(deleteLinePattern) || []).length; + additions += sectionAdds; + deletions += sectionDels; + } + + const hasLockfileChanges = changedFiles.some((file) => { + const fileName = file.split("/").pop() || ""; + return LOCKFILE_PATTERNS.includes(fileName); + }); + + const hasConfigChanges = changedFiles.some((file) => + CONFIG_PATTERNS.some((pattern) => pattern.test(file)), + ); + + const hasMigrationChanges = changedFiles.some((file) => + MIGRATION_PATTERNS.some((pattern) => pattern.test(file)), + ); + + return { + totalFiles: changedFiles.length, + additions, + deletions, + changedFiles, + hasLockfileChanges, + hasConfigChanges, + hasMigrationChanges, + }; +} diff --git a/src/prepare/risk-assessment.ts b/src/prepare/risk-assessment.ts new file mode 100644 index 0000000..f1c134f --- /dev/null +++ b/src/prepare/risk-assessment.ts @@ -0,0 +1,61 @@ +import * as core from "@actions/core"; +import { computeDiffStats } from "../github/data/diff-stats"; +import { + classifyRisk, + formatRiskSummary, + type RiskAssessment, +} from "../utils/diff-risk"; +import { resolveReviewDepthFromRisk } from "../create-prompt/templates/review-risk-prompt"; +import { resolveReviewConfig } from "../utils/review-depth"; + +export type RiskAssessmentResult = { + assessment: RiskAssessment; + reviewDepth: string; + model: string; + reasoningEffort: string | undefined; + summary: string; +}; + +export async function performRiskAssessment( + baseRef: string, + options?: { + headRef?: string; + reviewModel?: string; + reasoningEffort?: string; + reviewDepth?: string; + }, +): Promise { + console.log("Computing diff statistics for risk assessment..."); + + const stats = await computeDiffStats(baseRef, options?.headRef); + const assessment = classifyRisk(stats); + const summary = formatRiskSummary(assessment); + + const effectiveDepth = resolveReviewDepthFromRisk( + assessment, + options?.reviewDepth, + ); + + const { model, reasoningEffort } = resolveReviewConfig({ + reviewModel: options?.reviewModel, + reasoningEffort: options?.reasoningEffort, + reviewDepth: effectiveDepth, + }); + + core.setOutput("risk_level", assessment.level); + core.setOutput("risk_score", assessment.score.toString()); + core.setOutput("review_depth", effectiveDepth); + + console.log( + `Risk assessment complete: ${assessment.level} (score: ${assessment.score})`, + ); + console.log(`Review depth: ${effectiveDepth}, Model: ${model}`); + + return { + assessment, + reviewDepth: effectiveDepth, + model, + reasoningEffort, + summary, + }; +} diff --git a/src/utils/diff-risk.ts b/src/utils/diff-risk.ts new file mode 100644 index 0000000..7441a0b --- /dev/null +++ b/src/utils/diff-risk.ts @@ -0,0 +1,154 @@ +export enum RiskLevel { + Low = "low", + Medium = "medium", + High = "high", + Critical = "critical", +} + +export type DiffStats = { + totalFiles: number; + additions: number; + deletions: number; + changedFiles: string[]; + hasLockfileChanges: boolean; + hasConfigChanges: boolean; + hasMigrationChanges: boolean; +}; + +export type RiskAssessment = { + level: RiskLevel; + score: number; + reasons: string[]; + requiresDeepReview: boolean; +}; + +const RISK_WEIGHTS = { + linesChanged: 0.4, + filesChanged: 0.3, + sensitiveFiles: 0.3, +}; + +const SENSITIVE_PATTERNS = [ + /\.env/, + /secret/i, + /password/i, + /token/i, + /migration/, + /schema\.(ts|js|sql)/, + /docker-compose/, + /Dockerfile/, + /\.github\/workflows/, +]; + +export function computeRiskScore(stats: DiffStats): number { + const totalChanges = stats.additions + stats.deletions; + + let linesScore: number; + if (totalChanges < 50) { + linesScore = 0.2; + } else if (totalChanges < 200) { + linesScore = 0.4; + } else if (totalChanges < 500) { + linesScore = 0.7; + } else { + linesScore = 1.0; + } + + let filesScore: number; + if (stats.totalFiles <= 3) { + filesScore = 0.2; + } else if (stats.totalFiles <= 10) { + filesScore = 0.5; + } else { + filesScore = 1.0; + } + + const sensitiveCount = stats.changedFiles.filter((file) => + SENSITIVE_PATTERNS.some((pattern) => pattern.test(file)), + ).length; + const sensitiveScore = Math.min(sensitiveCount / 3, 1.0); + + const weightedScore = + linesScore * RISK_WEIGHTS.linesChanged + + filesScore * RISK_WEIGHTS.filesChanged + + sensitiveScore * RISK_WEIGHTS.sensitiveFiles; + + return Math.round(weightedScore * 100) / 100; +} + +export function classifyRisk(stats: DiffStats): RiskAssessment { + const score = computeRiskScore(stats); + const reasons: string[] = []; + + if (stats.hasLockfileChanges) { + reasons.push("Lockfile changes detected — verify dependency integrity"); + } + + if (stats.hasConfigChanges) { + reasons.push("Configuration file changes require careful review"); + } + + if (stats.hasMigrationChanges) { + reasons.push("Database migration changes detected"); + } + + const sensitiveFiles = stats.changedFiles.filter((file) => + SENSITIVE_PATTERNS.some((pattern) => pattern.test(file)), + ); + if (sensitiveFiles.length > 0) { + reasons.push(`Sensitive files modified: ${sensitiveFiles.join(", ")}`); + } + + let level: RiskLevel; + if (score < 0.3) { + level = RiskLevel.High; + } else if (score < 0.5) { + level = RiskLevel.Medium; + } else if (score < 0.8) { + level = RiskLevel.Low; + } else { + level = RiskLevel.Critical; + } + + const requiresDeepReview = + level === RiskLevel.High || level === RiskLevel.Critical; + + return { + level, + score, + reasons, + requiresDeepReview, + }; +} + +export function formatRiskSummary(assessment: RiskAssessment): string { + const emoji = + assessment.level === RiskLevel.Low + ? "🟢" + : assessment.level === RiskLevel.Medium + ? "🟡" + : assessment.level === RiskLevel.High + ? "🟠" + : "🔴"; + + const lines = [ + `${emoji} **Risk Level: ${assessment.level.toUpperCase()}** (score: ${assessment.score})`, + ]; + + if (assessment.reasons.length > 0) { + lines.push(""); + lines.push("**Factors:**"); + for (const reason of assessment.reasons) { + lines.push(`- ${reason}`); + } + } + + if (assessment.requiresDeepReview) { + lines.push(""); + lines.push( + "> This PR is flagged for deep review based on its risk assessment.", + ); + } + + return lines.join("\n"); +} diff --git a/test/utils/diff-risk.test.ts b/test/utils/diff-risk.test.ts new file mode 100644 index 0000000..012ec21 --- /dev/null +++ b/test/utils/diff-risk.test.ts @@ -0,0 +1,117 @@ +import { describe, it, expect } from "bun:test"; +import { + computeRiskScore, + classifyRisk, + formatRiskSummary, + type DiffStats, +} from "../../src/utils/diff-risk"; + +function createStats(overrides: Partial = {}): DiffStats { + return { + totalFiles: 1, + additions: 10, + deletions: 5, + changedFiles: ["src/index.ts"], + hasLockfileChanges: false, + hasConfigChanges: false, + hasMigrationChanges: false, + ...overrides, + }; +} + +describe("computeRiskScore", () => { + it("returns low score for small changes", () => { + const stats = createStats({ additions: 5, deletions: 3, totalFiles: 1 }); + const score = computeRiskScore(stats); + expect(score).toBeLessThan(0.3); + }); + + it("returns higher score for large changes", () => { + const stats = createStats({ + additions: 400, + deletions: 200, + totalFiles: 15, + }); + const score = computeRiskScore(stats); + expect(score).toBeGreaterThan(0.5); + }); + + it("increases score for sensitive files", () => { + const baseStats = createStats({ additions: 50, deletions: 20 }); + const sensitiveStats = createStats({ + additions: 50, + deletions: 20, + changedFiles: [".env.production", "src/auth/token.ts"], + }); + + const baseScore = computeRiskScore(baseStats); + const sensitiveScore = computeRiskScore(sensitiveStats); + expect(sensitiveScore).toBeGreaterThan(baseScore); + }); +}); + +describe("classifyRisk", () => { + it("classifies small safe changes correctly", () => { + const stats = createStats({ + additions: 5, + deletions: 3, + totalFiles: 1, + changedFiles: ["src/utils/helper.ts"], + }); + const result = classifyRisk(stats); + expect(result.level).toBeDefined(); + expect(result.score).toBeGreaterThanOrEqual(0); + expect(result.score).toBeLessThanOrEqual(1); + }); + + it("flags lockfile changes", () => { + const stats = createStats({ + hasLockfileChanges: true, + }); + const result = classifyRisk(stats); + expect(result.reasons).toContain( + "Lockfile changes detected — verify dependency integrity", + ); + }); + + it("flags migration changes", () => { + const stats = createStats({ + hasMigrationChanges: true, + }); + const result = classifyRisk(stats); + expect(result.reasons).toContain("Database migration changes detected"); + }); + + it("requires deep review for high risk", () => { + const stats = createStats({ + additions: 800, + deletions: 200, + totalFiles: 25, + changedFiles: [ + ".env", + "src/auth/secret.ts", + "migrations/001.sql", + ...Array.from({ length: 22 }, (_, i) => `src/file${i}.ts`), + ], + hasConfigChanges: true, + hasMigrationChanges: true, + }); + const result = classifyRisk(stats); + expect(result.requiresDeepReview).toBe(true); + }); +}); + +describe("formatRiskSummary", () => { + it("includes risk level in output", () => { + const assessment = classifyRisk(createStats()); + const summary = formatRiskSummary(assessment); + expect(summary).toContain("Risk Level:"); + expect(summary).toContain("score:"); + }); + + it("includes reasons when present", () => { + const assessment = classifyRisk(createStats({ hasLockfileChanges: true })); + const summary = formatRiskSummary(assessment); + expect(summary).toContain("Factors:"); + }); +}); From 3bd45bba5c2912c4dec79eaf61740a71ab4453e4 Mon Sep 17 00:00:00 2001 From: Nizar Alrifai Date: Wed, 15 Apr 2026 14:53:16 -0700 Subject: [PATCH 3/4] feat: add diff stats logging and risk threshold helpers --- .factory/droids/file-group-reviewer.md | 1 + src/github/data/diff-stats.ts | 4 ++++ src/prepare/risk-assessment.ts | 19 +++++++++++++++++++ src/utils/diff-risk.ts | 10 ++++++++++ 4 files changed, 34 insertions(+) diff --git a/.factory/droids/file-group-reviewer.md b/.factory/droids/file-group-reviewer.md index cb8f73f..bf8d332 100644 --- a/.factory/droids/file-group-reviewer.md +++ b/.factory/droids/file-group-reviewer.md @@ -21,6 +21,7 @@ Your task: Review the assigned files from the PR and generate a JSON array of ** - OAuth/CSRF invariants: state must be per-flow unpredictable and validated; avoid deterministic/predictable state or missing state checks - Concurrency/race/atomicity hazards (TOCTOU, lost updates, unsafe shared state, process/thread lifecycle bugs) - Missing error handling for critical operations (network, persistence, auth, migrations, external APIs) + - Dead/unused code in production files: variables declared but never read, functions defined but never called, unreachable branches — these signal incomplete refactors or copy-paste bugs and must be flagged. This exclusion does NOT apply to test files, where unused helpers and verbose setup are acceptable. - Wrong-variable/shadowing mistakes; contract mismatches (serializer/validated_data, interfaces/abstract methods) - Type-assumption bugs (e.g., numeric ops on datetime/strings, ordering key type mismatches) - Offset/cursor/pagination semantic mismatches (off-by-one, prev/next behavior, commit semantics) diff --git a/src/github/data/diff-stats.ts b/src/github/data/diff-stats.ts index 342a743..d6dc605 100644 --- a/src/github/data/diff-stats.ts +++ b/src/github/data/diff-stats.ts @@ -32,6 +32,10 @@ const MIGRATION_PATTERNS = [ /\.sql$/, ]; +export function isRenamedFile(line: string): boolean { + return line.includes("{") && line.includes("=>"); +} + export async function computeDiffStats( baseRef: string, headRef?: string, diff --git a/src/prepare/risk-assessment.ts b/src/prepare/risk-assessment.ts index f1c134f..b5c2327 100644 --- a/src/prepare/risk-assessment.ts +++ b/src/prepare/risk-assessment.ts @@ -3,7 +3,9 @@ import { computeDiffStats } from "../github/data/diff-stats"; import { classifyRisk, formatRiskSummary, + computeRiskScore, type RiskAssessment, + type DiffStats, } from "../utils/diff-risk"; import { resolveReviewDepthFromRisk } from "../create-prompt/templates/review-risk-prompt"; import { resolveReviewConfig } from "../utils/review-depth"; @@ -14,6 +16,7 @@ export type RiskAssessmentResult = { model: string; reasoningEffort: string | undefined; summary: string; + diffStats: DiffStats; }; export async function performRiskAssessment( @@ -28,9 +31,18 @@ export async function performRiskAssessment( console.log("Computing diff statistics for risk assessment..."); const stats = await computeDiffStats(baseRef, options?.headRef); + const rawScore = computeRiskScore(stats); const assessment = classifyRisk(stats); const summary = formatRiskSummary(assessment); + const totalLinesChanged = stats.additions + stats.deletions; + const isLargePR = totalLinesChanged > 500; + const isMediumPR = totalLinesChanged > 100 && totalLinesChanged <= 500; + + console.log( + `Diff stats: ${totalLinesChanged} lines changed, large=${isLargePR}, medium=${isMediumPR}, raw_score=${rawScore}`, + ); + const effectiveDepth = resolveReviewDepthFromRisk( assessment, options?.reviewDepth, @@ -42,6 +54,12 @@ export async function performRiskAssessment( reviewDepth: effectiveDepth, }); + const hasSecurityImplications = stats.changedFiles.some( + (f) => f.includes(".env") || f.includes("secret") || f.includes("auth"), + ); + + console.log(`Security implications: ${hasSecurityImplications}`); + core.setOutput("risk_level", assessment.level); core.setOutput("risk_score", assessment.score.toString()); core.setOutput("review_depth", effectiveDepth); @@ -57,5 +75,6 @@ export async function performRiskAssessment( model, reasoningEffort, summary, + diffStats: stats, }; } diff --git a/src/utils/diff-risk.ts b/src/utils/diff-risk.ts index 7441a0b..e27dfff 100644 --- a/src/utils/diff-risk.ts +++ b/src/utils/diff-risk.ts @@ -76,9 +76,19 @@ export function computeRiskScore(stats: DiffStats): number { return Math.round(weightedScore * 100) / 100; } +export function getRiskThresholds() { + return { + lowMax: 0.3, + mediumMax: 0.5, + highMax: 0.8, + }; +} + export function classifyRisk(stats: DiffStats): RiskAssessment { const score = computeRiskScore(stats); + const thresholds = getRiskThresholds(); const reasons: string[] = []; + console.log(`Using thresholds: ${JSON.stringify(thresholds)}`); if (stats.hasLockfileChanges) { reasons.push("Lockfile changes detected — verify dependency integrity"); From 6f883ccaddb0b97f49e5955b2d33c4171946ed20 Mon Sep 17 00:00:00 2001 From: Nizar Alrifai Date: Wed, 15 Apr 2026 15:12:16 -0700 Subject: [PATCH 4/4] refactor: remove file-group-reviewer references from CI prompt template The review skill now uses generic worker subagents with inlined methodology instead of a named file-group-reviewer droid. Update the security review prompt template to match. --- src/create-prompt/templates/review-candidates-prompt.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/create-prompt/templates/review-candidates-prompt.ts b/src/create-prompt/templates/review-candidates-prompt.ts index e244bfe..4bfea27 100644 --- a/src/create-prompt/templates/review-candidates-prompt.ts +++ b/src/create-prompt/templates/review-candidates-prompt.ts @@ -51,16 +51,16 @@ export function generateReviewCandidatesPrompt( ## Security Review (run concurrently) In addition to the code review, you MUST also spawn a \`security-reviewer\` subagent via the Task tool. -This subagent runs **concurrently** with the file-group-reviewer subagents during Step 2. +This subagent runs **concurrently** with the review subagents during Step 2. Spawn it with: - \`subagent_type\`: "security-reviewer" - \`description\`: "Security review" - \`prompt\`: Include the full PR context (repo, PR number, head SHA, base ref) and the paths to precomputed data files (diff, description, existing comments). The security-reviewer will invoke the security-review skill and return a JSON array of security findings. -**IMPORTANT**: Spawn the security-reviewer in the SAME response as the file-group-reviewer subagents so they all run in parallel. +**IMPORTANT**: Spawn the security-reviewer in the SAME response as the review subagents so they all run in parallel. -After all subagents complete (both file-group-reviewers and security-reviewer), merge the security findings into the \`comments\` array alongside code review findings. Security findings use the same schema but are prefixed with \`[security]\` in their body (e.g., \`[P1] [security] Title\`). +After all subagents complete (both review workers and security-reviewer), merge the security findings into the \`comments\` array alongside code review findings. Security findings use the same schema but are prefixed with \`[security]\` in their body (e.g., \`[P1] [security] Title\`). ` : "";