Auto-Fix Pipeline

UserDeveloper8 min read

12. Auto-Fix Pipeline

The Auto-Fix pipeline is a 4-phase automated remediation system that scans GitHub repositories, detects issues via LLM analysis, generates unified-diff patches, and optionally creates pull requests -- all streamed in real time over SSE.

Pipeline Architecture

flowchart LR subgraph Phase1["Phase 1: Scan"] A[scanRepository] --> B[Index files into RepositoryFile] end subgraph Phase2["Phase 2: Detect"] C[detectIssues] --> D[Create AutoFixIssue records] end subgraph Phase3["Phase 3: Fix"] E[generateFixes] --> F[Write fixPatch + fixExplanation] end subgraph Phase4["Phase 4: PR"] G[applyPatch] --> H[createBranch + createPullRequest] end Phase1 --> Phase2 --> Phase3 --> Phase4 Phase4 --> I[extractAutoFixInsights]

Entry Point

The pipeline is orchestrated by runAutoFix() in src/server/services/auto-fix/pipeline.ts, an AsyncGenerator<AutoFixEvent> consumed by the SSE route at /api/v1/events/auto-fix/[id].

export async function* runAutoFix(
  opts: AutoFixRunOptions
): AsyncGenerator<AutoFixEvent>

AutoFixRunOptions

Field Type Description
runId string UUID of the AutoFixRun record
repositoryId string Target repository
tenantId string Tenant scope
userId string Initiating user
provider string? LLM provider (default: "anthropic")
model string? Model override
autoCreatePR boolean? Whether to execute Phase 4
categories IssueCategory[]? Filter to specific issue types

Phase 1: Repository Scan

Delegates to scanRepository() from the code-analysis scanner. If RepositoryFile records already exist for the repository, the scan is skipped and existing file metadata is reused. Otherwise, the scanner fetches the GitHub tree via fetchRepoTree() and indexes files in batches of 50.

Status transition: pending -> scanning

Phase 2: Issue Detection

Delegates to detectIssues(), which sends batches of source file content to the LLM and parses structured JSON responses. Each detected issue is persisted as an AutoFixIssue record.

Status transition: scanning -> detecting

If zero issues are found, the pipeline completes early with a clean-repository message.

Issue Categories

The 5 supported categories are validated at the tRPC layer:

Category Description
security Vulnerabilities, injection risks, secrets exposure
bug Logic errors, null dereferences, race conditions
performance N+1 queries, unnecessary allocations, blocking I/O
error-handling Missing try/catch, swallowed errors, unhelpful messages
code-smell Dead code, excessive complexity, naming issues

Severity Levels

Each issue is assigned a severity: critical, high, medium, or low. The UI renders these with color-coded badges via the severityVariant helper:

  • critical -> danger (red)
  • high -> warning (orange)
  • medium -> accent (blue)
  • low -> default (gray)

Phase 3: Fix Generation

Delegates to generateFixes(), which iterates over AutoFixIssue records with fixStatus: "pending" and calls the LLM to produce a unified diff (fixPatch) and human-readable explanation (fixExplanation).

Status transition: detecting -> fixing

Each generated fix updates the issue record:

// On success:
{ fixStatus: "fix_generated", fixPatch: "...", fixExplanation: "..." }

// On failure:
{ fixStatus: "failed" }

Phase 4: PR Creation (Optional)

Only runs when autoCreatePR: true and at least one fix was generated. For each issue with fixStatus: "fix_generated":

  1. Create a branch: autofix/{runIdShort}/{slug} (slug is title, max 30 chars)
  2. Fetch current file content from GitHub
  3. Apply the unified diff via applyPatch(original, fixPatch)
  4. Upload patched file with commit message fix(autofix): {title}
  5. Create PR with structured body containing category, severity, issue detail, and fix explanation

Status transition: fixing -> creating_prs

// Branch naming pattern
const branchName = `autofix/${runId.slice(0, 8)}/${slug}`;

The PR body is auto-generated:

## AutoFix: {title}

**Category:** {category}
**Severity:** {severity}

### Issue
{detail}

### Fix
{fixExplanation}

---
*Generated by nyxCore AutoFix*

Standalone PR Creation

The createPRsForRun() function in src/server/services/auto-fix/create-prs.ts can be called independently from the retryPRs tRPC mutation, enabling users to create PRs after the main pipeline has completed.

Returns a PRCreationResult:

interface PRCreationResult {
  created: number;
  failed: number;
  skipped: number;   // Patch could not be applied
  results: {
    issueId: string;
    title: string;
    status: "created" | "failed" | "skipped";
    prUrl?: string;
    prNumber?: number;
    error?: string;
  }[];
}

Retry Logic

Retry Fixes

retryFixesForRun() in src/server/services/auto-fix/retry-fixes.ts resets pending or failed issues back to pending (clearing fixPatch and fixExplanation), sets the run status to "fixing", and re-runs generateFixes(). On completion, restores status to "completed".

Supports targeting specific issues via issueIds and switching provider/model:

interface RetryFixOptions {
  runId: string;
  tenantId: string;
  issueIds?: string[];  // Optional subset
  provider?: string;
  model?: string;
}

Retry PRs

The retryPRs mutation filters for issues with fixStatus: "fix_generated" and delegates to createPRsForRun(). Requires at least one eligible issue.

Orphaned Run Cleanup

When a new run is started via the start mutation, any existing runs for the same repository that have been stuck in an active status (scanning, detecting, fixing, creating_prs) for over 10 minutes are automatically marked as failed:

await ctx.prisma.autoFixRun.updateMany({
  where: {
    repositoryId: input.repositoryId,
    status: { in: ["scanning", "detecting", "fixing", "creating_prs"] },
    updatedAt: { lt: new Date(Date.now() - 10 * 60 * 1000) },
  },
  data: {
    status: "failed",
    errorMessage: "Orphaned: process terminated without completing",
  },
});

NerdStats

Every event emitted by the pipeline includes a cumulative nerdStats snapshot:

interface NerdStatsData {
  tokenUsage: { prompt: number; completion: number; total: number };
  costEstimate: number;
  llmCalls: number;
  model: string | null;
  provider: string | null;
  phases: Record<string, {
    tokenUsage: { prompt: number; completion: number; total: number };
    costEstimate: number;
    llmCalls: number;
    startedAt: number | null;
    completedAt: number | null;
  }>;
}

Phase-level breakdown tracks detect, fix, and pr independently. The withNerd() helper attaches a structuredClone of the current stats to every yielded event.

Insight Extraction

After pipeline completion, extractAutoFixInsights() runs as a best-effort operation to persist learnings from the run into the WorkflowInsight table for future pipeline reuse. Failures are silently caught.

Data Model

AutoFixRun

Column Type Description
id UUID Primary key
tenantId UUID Tenant scope
userId UUID Initiating user
repositoryId UUID Target repository
status String pending / scanning / detecting / fixing / creating_prs / completed / failed / cancelled
config Json? { provider, model, autoCreatePR, categories }
stats Json? { filesScanned, issuesFound, fixesGenerated, prsCreated, nerdStats }
errorMessage Text? Error description on failure
startedAt DateTime? Pipeline start time
completedAt DateTime? Pipeline completion time

AutoFixIssue

Column Type Description
id UUID Primary key
runId UUID Parent run
category String One of the 5 issue categories
severity String critical / high / medium / low
title String Short issue description
detail Text Full issue explanation
filePath String Affected file
lineStart / lineEnd Int? Line range
evidence String[] Code snippets proving the issue
fixStatus String pending / fix_generated / pr_created / resolved / skipped / failed
fixPatch Text? Unified diff
fixExplanation Text? Human-readable fix description
prUrl String? GitHub PR URL
prNumber Int? GitHub PR number
resolvedBy String? auto / webhook / manual / claude-code

tRPC Router

The autoFixRouter in src/server/trpc/routers/auto-fix.ts exposes:

Procedure Type Rate Limit Description
list query standard Paginated runs for tenant
get query standard Single run with all issues
start mutation LLM Create new run (cleans orphans first)
cancel mutation standard Cancel active run
resolveIssue mutation standard Mark issue as manually resolved
skipIssue mutation standard Skip an issue with optional reason
retryPRs mutation standard Re-attempt PR creation for generated fixes
retryFixes mutation LLM Re-generate fixes for pending/failed issues
resolutionStats query standard Aggregate resolved/total/skipped counts
generateReport mutation LLM Generate styled report from run data

UI: IssueCard Component

src/components/auto-fix/issue-card.tsx renders each issue as an expandable card with:

  • Header: severity badge, category badge, fix status badge, title, file path with line range
  • Expanded body: detail text, evidence code blocks, PatchViewer for the unified diff, PR link, action buttons
  • Actions: Generate Fix (retry), Resolve (manual), Skip -- each with loading states