Skip to content

Patch Stage

PatchStage produces minimal, validated patches for verified bugs. It runs after VerifyStage in the verify_and_patch workflow. For each verified bug it generates a candidate diff, reviews its minimality, applies it in isolation, rechecks that the vulnerability is gone, and runs integration tests.

Location

src/stages/patch/stage.py

Stage declaration

class PatchStage(BaseStage[StageInput, StageOutput]):
    name = StageName.PATCH
    required_artifacts = ("source_snapshot", "verification_evidence")
    produced_artifacts = ("patch",)

Per-finding pipeline

Each bug is processed through up to 3 attempts by default (configurable via patch_config["max_attempts"]). Bugs with verification.status != "verified" are skipped immediately.

Step 1 — Gather context

  • Load the original finding from input.data["bugs"].
  • Load the matching verification result from the verification_evidence artifact.
  • Extract root_cause, evidence, bug_description, source file paths, and sink/source descriptions.
  • Obtain plugin patch policy: context.plugins.plugin.patch_policy(context, bug).

Step 2 — Generate minimal candidate

Runs an agent task with purpose="patch.generate" using stages/patch/prompts/generate.txt.

The agent must:

  • Address only the verified root cause.
  • Prefer existing helpers and local code style.
  • Avoid broad refactors, unrelated cleanups, speculative hardening.
  • Return a unified diff.
  • Return changed-line count and touched files/functions.
  • Explain why each touched location is necessary.

Changed-line count = added + removed code lines (excluding file headers, hunk headers, and context lines).

Step 3 — Minimality review gate

If the candidate changes more than 10 code lines, runs a separate agent task with purpose="patch.minimality_review" using stages/patch/prompts/minimality_review.txt.

The review must answer:

  • Are all changed lines necessary to eliminate the verified root cause?
  • Can any lines be removed without reintroducing the vulnerability?
  • Is the patch doing unrelated refactoring or behavior changes?

If the review finds the candidate unsafe, the stage either retries generation with feedback or rejects the candidate.

Step 4 — Apply in isolation

Applies the unified diff to an isolated copy of the source snapshot. The original checkout is never mutated.

Records:

  • status: "applied" or "failed"
  • error: patch apply error if any
  • applied_paths: files changed by the patch

If the patch does not apply cleanly, generation retries with the apply error as feedback.

Step 5 — Vulnerability recheck

Runs an agent task with purpose="patch.vulnerability_recheck" using stages/patch/prompts/vulnerability_recheck.txt.

The agent tries to find the same vulnerability again in the patched source, checking the previous vulnerable files/functions first and nearby call paths if needed.

Interpretation:

  • If the vulnerability is still reachable → candidate fails.
  • If the agent explains why the original path is blocked → candidate passes.
  • If uncertain → candidate rejected or flagged low confidence.

Step 6 — Integration test

Runs focused tests after the vulnerability recheck passes.

Test source priority:

  1. Backend-provided options["patch_test_commands"] in request metadata.
  2. Plugin test policy (patch_policy hook).
  3. Tests identified by the agent from changed files.
  4. Manual workflow description when no executable test is available.

Records status: "passed", "failed", or "not_run".

Step 7 — Accept or reject

A candidate is accepted when:

  • Patch applies cleanly.
  • Minimality gate passes (or was not required).
  • Vulnerability recheck cannot find the original vulnerability.
  • Integration test passes, or no executable test exists and the absence is recorded.

A candidate is rejected when:

  • Patch cannot be applied.
  • Minimality review cannot justify the changes.
  • Original vulnerability is still present after patch.
  • Integration test shows a regression.
  • Agent cannot produce a patch with enough confidence.

vega.remediation_results artifact

PatchStage writes format_version 2:

{
  "format_version": 2,
  "artifact_kind": "vega.remediation_results",
  "repo_id": "...",
  "run_id": "...",
  "bug_count": 1,
  "eligible_bug_count": 1,
  "accepted_patch_count": 1,
  "rejected_patch_count": 0,
  "results": [
    {
      "finding_id": "bug_1",
      "status": "accepted",
      "root_cause": "...",
      "fix_strategy": "add existing authorization guard",
      "proposed_fixes": [
        {
          "title": "...",
          "patch": "--- a/src/service.py\n+++ ...",
          "changed_line_count": 6,
          "affected_paths": ["src/service.py"],
          "affected_symbols": ["handle_admin_action"]
        }
      ],
      "minimality_review": {
        "required": false,
        "status": "not_required",
        "changed_line_count": 6
      },
      "apply_result": { "status": "applied", "applied_paths": ["src/service.py"] },
      "vulnerability_recheck": {
        "status": "passed",
        "vulnerability_still_present": false
      },
      "integration_test": { "status": "passed", "commands": ["pytest tests/"] },
      "attempts": 1,
      "confidence": "high"
    }
  ],
  "summary": {
    "patch_count": 1,
    "skipped_unverified_count": 0,
    "minimality_reviewed_count": 0,
    "vulnerability_recheck_passed_count": 1,
    "integration_test_passed_count": 1
  }
}

Result status values:

Status Meaning
"accepted" All gates passed
"rejected" Candidate failed; attempts exhausted
"skipped_unverified" Bug was not verified
"no_safe_patch" Root cause is valid but no safe minimal fix found

Domain events

Event kind Payload
patch_candidate_created finding_id, attempt, diff summary
patch_minimality_reviewed finding_id, review decision
patch_apply_completed finding_id, status, applied_paths
patch_vulnerability_rechecked finding_id, status, vulnerability_still_present
patch_integration_test_completed finding_id, status, test output
patch_finding_completed finding_id, final status
patch_created Final artifact ref + summary counts

Plugin hook

PatchStage calls context.plugins.plugin.patch_policy(context, bug) for each bug to get plugin-specific patch policy (e.g. test commands, patch constraints, domain-specific code style rules for the fix).

Cancellation

PatchStage calls context.checkpoint(StageName.PATCH, progress) between finding iterations. Cancellation terminates in-flight agent calls.

Prompts

File Purpose
stages/patch/prompts/generate.txt Candidate diff generation
stages/patch/prompts/minimality_review.txt Review for over-10-line candidates
stages/patch/prompts/vulnerability_recheck.txt Search for original vuln in patched source
stages/patch/prompts/default.txt Fallback / combined prompt