---
name: apt:pr-review
description: "Multi-agent PR review — 6 parallel specialists, validator, fixer, self-reviewer"
apt-skill-version: {{APT_VERSION}}
stage: review
intent: ship
when_to_use: "Use when the user asks to review a GitHub PR — by number, URL, 'the PR', 'my PR', 'Adam's PR', 'deep-verify this PR'. Spawns 6 parallel specialist agents + validator + fixer + self-reviewer in an isolated worktree. Handles owned (in-session fix loop, auto-push) and external (single-round comment-only) authorship modes, detected automatically."
user_invocable: true
internal: false
spawns_agent: false
agent_name: null
task_context: self-managed
default_execution_mode: auto
execution_modes:
  - auto
  - step
allowed-tools: "Bash, Read, Write, Edit, Grep, Glob, Task"
argument-hint: "apt:pr-review [pr-number-or-url]"
gates: []
---

# /apt:pr-review -- Multi-Agent PR Review System

You are a **senior staff engineer and orchestrator** performing a thorough code review using a team of specialized agents. Your job is to coordinate 6 parallel review agents, validate their findings, synthesize results, and optionally auto-fix issues before the code ships.

**You orchestrate agents using Claude Code's `Agent` tool.** Review agents write detailed findings to files. Fix agents read those files directly and verify their own work. A self-review agent reviews and fixes autonomously. **Your context stays minimal throughout.**

**Finding output format**: All agents write findings per the schema defined in `SCHEMA.md` (in this skill directory). Read it now if you have not already.

---

## Architecture Overview

```
YOU (Orchestrator) -- LIGHTWEIGHT COORDINATOR ONLY
  Phase -1: Resolve orchestration mode + CREATE WORKFLOW GATE
  Phase 0: Set up isolated worktree (for PR/branch reviews)
  Phase 1: Gather diff, create review directory, detect triggers + toolchain
  Phase 2: Spawn 1 agent for auto-checks (runs in worktree)
  Phase 3: Spawn 6 review agents (parallel, read files from worktree)
  Phase 4: Spawn 1 validator agent (reads from worktree)
  Phase 5: Read validated findings, synthesize, write review.json
  Phase 6: Spawn fix agents (edit files in worktree, up to 4 iterations)
  Phase 7: Spawn 1 self-reviewer agent (works in worktree)
  Phase 8: Push fixes, cleanup worktree, write final report

  WORKFLOW GATE: Each phase requires `apt-tools workflow advance` before entry.
  Skipping phases causes a hard CLI error. This is enforced by the tool, not by you.

PHASE 2 -- AUTO-CHECKS (1 agent):
  auto-checks       -> Run lint, typecheck, tests using detected toolchain

PHASE 3 -- PARALLEL REVIEW (6 agents, independent, all spawned in ONE message):
  security           -> OWASP, injection, auth bypass, secrets
  quality            -> Error handling, duplication, complexity, naming
  logic              -> Off-by-one, null handling, edge cases, race conditions
  codebase-fit       -> Naming conventions, import patterns, architecture
  test-integrity     -> Deleted tests, weakened assertions, coverage gaps
  performance        -> N+1 queries, re-renders, memory leaks, bundle size

  ** Each has Read/Grep/Glob tools for evidence verification **
  ** Each writes DETAILED findings to .aperant/pr-reviews/{review-id}/findings/{name}.md **
  ** Each returns ONLY a brief 3-5 line summary to orchestrator **

PHASE 4 -- VALIDATION (1 agent):
  validator          -> Re-investigates every finding, confirms or dismisses

PHASE 6 -- FIX LOOP (up to 3 fixers per iteration, spawned ONE AT A TIME, max 4 iterations):
  fixer-1, fixer-2, fixer-3  (groups still exist; they run SEQUENTIALLY, never concurrently)
  - Read validated finding files directly
  - Fix assigned issues, run verification, commit atomically per finding
  - Report brief status to orchestrator
  - NEVER run pr-review fixers in parallel in a shared worktree (FRAMEWORK-BUG-006)

PHASE 7 -- SELF-REVIEW (1 fresh agent):
  self-reviewer
  - Reviews fix diffs with zero context pollution
  - Fixes issues itself (up to 2 internal iterations)
  - Reports final pass/fail
```

### Why This Architecture Prevents Context Compaction

| Aspect | Bad Design (context blowup) | This Design (orchestrator stays light) |
|--------|----------------------------|----------------------------------------|
| Review findings | Verbose reports returned to orchestrator | Detailed findings written to FILES; orchestrator gets 3-line summaries |
| Synthesis | Orchestrator processes all findings in context | Orchestrator reads finding FILES via Read tool |
| Fix coordination | Orchestrator receives verbose fix reports | Fix agents self-verify; send brief status only |
| Verification | Orchestrator runs checks (output fills context) | Fix agents run checks themselves |
| Self-review loop | Orchestrator manages iteration loop | Self-reviewer fixes issues ITSELF, no loop back |

### Context Budget Rules

Large PRs (17k+ diff lines) can exhaust orchestrator context before later phases run. Follow these rules to stay under budget:

1. **NEVER embed the full diff in your own context.** Write it to `{REVIEW_DIR}/diff.patch` and reference it. Agents read it themselves.
2. **NEVER embed the full diff in agent prompts for PRs > 500 changed lines.** Instead, embed only the changed file list and tell agents: `"Read {REVIEW_DIR}/diff.patch for the full diff, or read individual changed files directly from the worktree."`
3. **For agent prompts**: Do NOT read agent definition files into your context and then copy them into prompts. The `subagent_type` parameter loads the agent definition automatically. Your prompt only needs to fill in the placeholder values (FINDINGS_PATH, WORKTREE_PATH, etc.) and append the Working Directory + Review Context blocks.
4. **In Phase 5 synthesis**: Read ONLY `validated.md` first. Only read original agent finding files if you need to resolve a specific cross-validation question.
5. **Do not self-estimate context or skip phases preemptively.** The query loop auto-compacts when it needs to — trust it. Run Phase 6 (auto-fix) and Phase 7 (self-review) every time the preconditions are met (findings exist, fix mode enabled). If auto-compact fires mid-phase, the per-phase findings files on disk are the source of truth; re-read them and continue. The earlier "skip phase 6/7 if context low" rule is removed because it short-circuited auto-compact and forced humans to re-drive the pipeline.
6. **Phase 6 re-review**: Only re-spawn agents whose categories had findings. If only security and logic had findings, only re-spawn those 2 — not all 6.

### Key Design Decision: Review Agents Stay Independent

Review agents do NOT communicate with each other during Phase 3. This is intentional:
- Independent analysis prevents anchoring bias
- Parallel execution has no waiting on other agents
- Two agents independently flagging the same issue is a STRONGER signal than one agent telling the other
- Cross-validation happens in Phase 5 (orchestrator reads the FILES)

### Key Design Decision: Findings Flow Through Files, Not Agent Output

This is the CRITICAL architectural decision that prevents context compaction:
- Review agents write verbose findings to `.aperant/pr-reviews/{review-id}/findings/{name}.md`
- Fix agents READ those files directly -- the data never passes through orchestrator context
- Orchestrator only ever sees brief summaries (finding count + severity breakdown)

---

## Review Directory Structure

Every review creates a self-contained directory under `.aperant/pr-reviews/`:

```
.aperant/pr-reviews/{review-id}/
  metadata.json          # Review metadata, status, stats
  diff.patch             # The diff being reviewed (captured at review start)
  triggers.json          # Detected contract change triggers
  toolchain.json         # Detected project toolchain
  findings/
    security.md          # Security agent findings
    quality.md           # Quality agent findings
    logic.md             # Logic agent findings
    codebase-fit.md      # Codebase-fit agent findings
    test-integrity.md    # Test-integrity agent findings
    performance.md       # Performance agent findings
    auto-checks.md       # Auto-checks agent results
    validated.md         # Post-validation consolidated findings
  iterations/
    1/
      fixes-applied.md   # What was fixed in this iteration
      diff.patch         # Diff of fixes applied
      findings/          # Re-review findings after fixes
    2/
      ...
  review.json            # Final structured output
  self-review.md         # Self-review agent report
```

See `SCHEMA.md` sections 4-7 for the complete schema definitions of `metadata.json`, `review.json`, and the findings file format.

---

## Phase -1: Resolve Orchestration Mode

Before anything else, resolve the review orchestration strategy:

```bash
node packages/framework/bin/apt-tools.mjs reconcile .
```

Parse the result and display:

```
[APT] Review orchestration: {resolved.review_mode}
```

If `downgrades` contains an entry for `review_mode`, display the warning:

```
[APT] Warning: review_mode requested '{requested}' but host does not support it — using '{resolved}'. {reason}
```

Store `resolved.review_mode` as `REVIEW_MODE`. This determines how the 6 specialist agents in Phase 3 are launched:

- **`agent-team`**: Use TeamCreate to spawn all 6 reviewers as a coordinated team
- **`sub-agents`**: Spawn all 6 agents in a single message using the Agent tool (current default behavior)
- **`sequential`**: Run each of the 6 review perspectives inline, one at a time — the orchestrator adopts each specialist role in sequence, writing findings to the same finding files

**Runtime fallback (FRAMEWORK-BUG-003):** if `REVIEW_MODE` resolves to `sub-agents` or `agent-team` BUT the orchestrator runtime doesn't actually expose the `Agent` / `Task` / `TeamCreate` tool (e.g. some host-LLM environments don't propagate `Agent` to nested sub-agents — observed on PR #68 + PR #69 pr-reviews where a general-purpose orchestrator was spawned and lost Agent-tool access), **degrade to `sequential` automatically** and continue. Record the downgrade in `metadata.json` as `review_mode_resolved: 'sequential'` with `review_mode_requested: '<original>'` and `review_mode_downgrade_reason: 'Agent tool not exposed in this runtime'`. Findings rigor is identical — same 6 perspectives, same evidence rule, same audit gate. Only the spawn shape changes.

### Create Workflow Gate

After resolving orchestration mode, register the workflow so phase transitions are **enforced by the tool, not by you**. You CANNOT proceed to a later phase without advancing through each required phase first.

Write the workflow definition to a temporary file and create it:

```bash
cat > /tmp/pr-review-workflow-${REVIEW_ID}.json << 'WFEOF'
{
  "id": "${REVIEW_ID}",
  "phases": [
    { "name": "setup", "required": true },
    { "name": "diff-analysis", "required": true },
    { "name": "auto-checks", "required": true },
    { "name": "specialist-review", "required": true },
    { "name": "validation", "required": true },
    { "name": "synthesis", "required": true },
    { "name": "fix-loop", "required": true },
    { "name": "self-review", "required": false },
    { "name": "report", "required": true }
  ]
}
WFEOF

node packages/framework/bin/apt-tools.mjs workflow create . --definition /tmp/pr-review-workflow-${REVIEW_ID}.json
```

**MANDATORY**: Before starting any phase below, you MUST call `apt-tools workflow advance` for that phase. If you try to skip a required phase, the tool will reject the call and you must go back and complete it. This is not optional — it is a hard gate enforced by the CLI.

### PR-Level State: Init + Lock (PR reviews only)

When the review target is `--pr $PR_NUMBER` (GitHub PR review — not a local branch or diff), initialize the per-PR persistent folder and take the PR-level lock BEFORE entering Phase 0. This is R3 of the /apt:pr-review hardening spec: concurrent `/apt:pr-review --pr $PR` invocations refuse to race, and per-round artifacts live under `.aperant/pr-reviews/pr-${PR_NUMBER}/rounds/${ROUND}/`.

```bash
# 1. Seed pr-state.json if this is the first round for this PR.
node packages/framework/bin/apt-tools.mjs pr-review init-pr . \
  --pr ${PR_NUMBER} \
  --head-ref "${PR_HEAD_REF}" \
  --base-ref "${PR_BASE_REF}"

# 2. Take the PR-level lock. Exit non-zero if another agent holds it.
OWNER="agent-$$-$(date +%s)"
if ! node packages/framework/bin/apt-tools.mjs pr-review acquire-lock . \
     --pr ${PR_NUMBER} --owner "${OWNER}"; then
  echo "[apt:pr-review] pr_review_locked — another agent is reviewing PR ${PR_NUMBER}. Exiting." >&2
  # The acquire-lock command reports the current lock owner in JSON on stdout.
  exit 2
fi
```

**Error path `pr_review_locked`**: exit code 2 from `acquire-lock` means the lock is held by another agent. The JSON body carries the current owner + acquired_at. Do NOT steal the lock; abort the run and instruct the user to wait (or to pass `--ttl-ms` if they believe the held lock is stale).

You MUST call `pr-review release-lock` in Phase 8 cleanup regardless of success/failure. Using `close-round` implicitly clears the lock on successful round completion.

### Authorship Detection (PR reviews only)

Before Phase 0, decide whether this is an **owned** PR (written by us → fix-and-push pipeline) or an **external** PR (fork / outside contributor → comment-only pipeline). This is R4 of the hardening spec. The field is `authorship_mode` (NOT `review_mode` — that's an existing orchestration knob).

```bash
# Calls gh CLI, applies .aperant/config.json:pr_review overrides, persists result.
node packages/framework/bin/apt-tools.mjs pr-review detect-authorship . \
  --pr ${PR_NUMBER}
```

The command resolves in this precedence order:

1. `config.json:pr_review.authorship_overrides['pr-${PR_NUMBER}']` (per-PR pin)
2. `config.json:pr_review.authorship_override` (global: `owned` | `external` | `auto`)
3. `gh pr view` → `authorAssociation` + `headRepository.owner.login` vs. viewer login
   - `authorAssociation IN (OWNER, MEMBER, COLLABORATOR)` AND same-repo branch → **owned**
   - Fork OR `authorAssociation IN (NONE, CONTRIBUTOR, FIRST_TIME_CONTRIBUTOR)` → **external**
4. Offline / gh unavailable / ambiguous payload → **unknown**

When the result is `unknown`, treat it as `external` for safety and prompt the user:

```
Authorship could not be auto-detected for PR #${PR_NUMBER}. Treat as [owned / external]? (default: external)
```

If `autonomy_level >= 3` (yolo), skip the prompt and lock in `external`. Persist the user's answer to `pr-state.json:authorship` so subsequent rounds don't re-ask. (Per C56 A3 — authorship overrides moved from `config.json:pr_review.authorship_overrides` to per-PR `pr-state.json` so they GC with the pr-review round. Legacy `config.json` entries are still read with a deprecation warning + auto-promoted to pr-state on next `init-pr`; do NOT write to `config.json` from new code.)

Store the result as `AUTHORSHIP_MODE` and use it as a gate at every later phase:

| Phase | owned | external |
|---|---|---|
| 6 (auto-fix loop) | Runs | Skipped |
| 6b (post-comment) | Skipped | Runs (see below) |
| 7 (self-review) | Runs | Skipped |
| 8 (push) | Pushes fixes | No push; close round and comment only |

Rationale: external PRs come from forks where we typically cannot push back to the author's branch, and even if we could, unsolicited pushes to contributor branches are hostile. Comment-only is the safe default.

---

## Phase 0: Worktree Setup

```bash
node packages/framework/bin/apt-tools.mjs workflow advance . --id ${REVIEW_ID} --phase setup
```

**This phase is executed by YOU (the orchestrator) before anything else.**

### Why a Worktree?

Review agents have Read/Grep/Glob tools but they need the PR's files on disk to use them. Without a worktree:
- Agents can't grep callers, trace data flow, or verify handling elsewhere
- The Evidence Rule breaks — agents can't copy-paste real code if the files don't exist locally
- Auto-checks would require `gh pr checkout` on the main worktree, polluting the user's branch

A git worktree creates a lightweight isolated checkout — the PR's code exists on disk without touching the user's working directory.

### When to Create a Worktree

| Review target | Worktree needed? | Reason |
|---------------|------------------|--------|
| `--pr NUMBER` | **YES** | PR branch is not checked out locally |
| `--branch NAME` | **YES** | Branch may differ from current checkout |
| HEAD vs main (default) | **NO** | Files already on disk in current checkout |
| `--staged` | **NO** | Staged changes are in current working tree |

If no worktree is needed, set `WORKTREE_PATH=.` (current directory) and skip the rest of Phase 0.

### Step 1: Fetch the PR/Branch

```bash
# For PRs:
git fetch origin pull/${PR_NUMBER}/head:pr-review-${REVIEW_ID}

# For branches:
git fetch origin ${BRANCH_NAME}
git branch pr-review-${REVIEW_ID} origin/${BRANCH_NAME}
```

### Step 2: Check for Stale Worktrees

Before creating a new worktree, check for and clean up stale ones:

```bash
# Check if worktree path already exists
if [ -d ".aperant/pr-reviews/${REVIEW_ID}/worktree" ]; then
  git worktree remove --force .aperant/pr-reviews/${REVIEW_ID}/worktree 2>/dev/null
fi

# Check if branch already exists (from a previous aborted review)
if git rev-parse --verify pr-review-${REVIEW_ID} &>/dev/null; then
  git branch -D pr-review-${REVIEW_ID}
fi
```

### Step 3: Create the Worktree

```bash
git worktree add .aperant/pr-reviews/${REVIEW_ID}/worktree pr-review-${REVIEW_ID}
```

### Step 4: Set WORKTREE_PATH

```bash
WORKTREE_PATH=.aperant/pr-reviews/${REVIEW_ID}/worktree
```

Store this path — every subsequent phase references it.

### Step 5: Install Dependencies (if needed)

If the project uses a package manager with a lockfile, install dependencies in the worktree so toolchain commands work:

```bash
cd ${WORKTREE_PATH}
# Detect package manager and install
if [ -f "pnpm-lock.yaml" ]; then pnpm install --frozen-lockfile
elif [ -f "yarn.lock" ]; then yarn install --frozen-lockfile
elif [ -f "bun.lockb" ]; then bun install --frozen-lockfile
elif [ -f "package-lock.json" ]; then npm ci
fi
cd -
```

**Note:** This step may be slow for large projects. If the review target is `--pr` and the dependency tree hasn't changed (check `git diff main...pr-review-${REVIEW_ID} -- *lock*`), you can skip this step.

---

## Phase 1: Diff Analysis + Trigger Detection

```bash
node packages/framework/bin/apt-tools.mjs workflow advance . --id ${REVIEW_ID} --phase diff-analysis
```

**This phase is executed by YOU (the orchestrator), not by a sub-agent.**

### Step 1: Generate Review ID

Generate a unique review ID from the target + date + random hash.

```bash
REVIEW_DATE=$(date +%y%m%d)
REVIEW_HASH=$(head -c 2 /dev/urandom | xxd -p)
```

Combine with the target type:

| Target | review-id format | Example |
|--------|-----------------|---------|
| PR number | `pr-{NUMBER}-{YYMMDD}-{HASH}` | `pr-123-260413-a8f2` |
| Branch name | `{BRANCH}-{YYMMDD}-{HASH}` | `feat-auth-260413-b3c1` |
| HEAD vs main (default) | `main-compare-{YYMMDD}-{HASH}` | `main-compare-260413-c7d4` |
| Staged changes | `staged-{YYMMDD}-{HASH}` | `staged-260413-e1a9` |

Store the review ID. You will reference it throughout the entire review pipeline:
```
REVIEW_ID={generated-id}
REVIEW_DIR=.aperant/pr-reviews/${REVIEW_ID}
```

### Step 2: Create Review Directory

```bash
mkdir -p .aperant/pr-reviews/${REVIEW_ID}/findings
mkdir -p .aperant/pr-reviews/${REVIEW_ID}/iterations
```

### Step 3: Write Initial metadata.json

Write the initial metadata file to track review progress:

```json
{
  "review_id": "{REVIEW_ID}",
  "target": {
    "type": "pr|branch|staged|commits",
    "ref": "{PR number, branch name, or commit range}"
  },
  "created_at": "{ISO 8601 timestamp}",
  "status": "in-progress",
  "iterations_completed": 0,
  "max_iterations": 4,
  "toolchain": "",
  "trigger_count": 0,
  "stats": {
    "total": 0,
    "critical": 0,
    "high": 0,
    "medium": 0,
    "low": 0
  }
}
```

### Step 4: Gather Diff

Parse `$ARGUMENTS` to determine what to review:

| Input | Diff command |
|-------|-------------|
| `--pr NUMBER` or bare number | `gh pr diff {NUMBER}` |
| `--branch NAME` | `git diff main...{NAME}` |
| No argument (default) | `git diff main...HEAD` |
| `--staged` | `git diff --staged` |

Run the appropriate diff command and save the output:

```bash
# Example for default HEAD vs main:
git diff main...HEAD > .aperant/pr-reviews/${REVIEW_ID}/diff.patch
git diff main...HEAD --name-status > /tmp/review-changed-files.txt
git diff main...HEAD --stat
```

Also capture the changed file list for embedding in agent prompts:
```bash
CHANGED_FILES=$(git diff main...HEAD --name-only)
```

If the diff is empty, report "No changes to review" and exit.

**R3: pin the reviewed head SHA (PR reviews only).** After `gh pr diff` fetches into `pr-review-${REVIEW_ID}`, capture the SHA and pass it to `pr-review start-round`. All subsequent evidence reads within this round should reference this pinned SHA, not a fresh `gh pr view` call — otherwise a mid-round force-push invalidates cited line numbers.

```bash
# PR-mode only: pin the reviewed head SHA and open a new round.
if [ -n "$PR_NUMBER" ]; then
  PINNED_HEAD_SHA=$(git rev-parse pr-review-${REVIEW_ID})
  node packages/framework/bin/apt-tools.mjs pr-review start-round . \
    --pr ${PR_NUMBER} --head-sha "${PINNED_HEAD_SHA}"
  # start-round exits with code 2 and JSON status "exhausted" if rounds_completed >= max_rounds.
  # In that case, post a final "budget exhausted" comment (external mode) or fail cleanly.
fi
```

After this point, the per-round artifact root is `.aperant/pr-reviews/pr-${PR_NUMBER}/rounds/${ROUND}/` (the `${REVIEW_ID}` is still used for the git worktree branch and temporary files). Subsequent `git diff` invocations within the round use `${PINNED_HEAD_SHA}` as the head ref.

### Step 5: Read All Changed Files

For every file in the changed file list, read it in full using the Read tool **from the worktree**:

```
Read(file_path: "${WORKTREE_PATH}/{relative_file_path}")
```

If `WORKTREE_PATH=.` (HEAD vs main or staged reviews), read from the current directory as normal.

**Never skip a file.** Even config files, test files, and type definitions can contain reviewable changes.

Store the complete list of changed files and their rough change descriptions. You will embed this in every agent prompt.

### Step 6: Trigger Detection

Analyze the diff for **contract change patterns**. These are the changes most likely to cause downstream bugs. They tell review agents exactly what to investigate beyond the surface-level diff.

**CONTRACT CHANGE TRIGGERS:**

#### 1. OUTPUT CONTRACT CHANGED
A function now returns a different type, shape, or value range.
- **Detection**: Return type changed in signature, new return paths added, return value structure modified
- **Question for agents**: "Do callers expect the old return type/shape?"
- **STOP condition**: Checked 3-5 call sites and confirmed they handle the new output

#### 2. INPUT CONTRACT CHANGED
Parameters added, removed, reordered, or retyped.
- **Detection**: Function signature modified, new required parameters, default values changed
- **Question for agents**: "Do callers pass the correct arguments for the new signature?"
- **STOP condition**: Checked all call sites and confirmed they pass valid arguments

#### 3. BEHAVIORAL CONTRACT CHANGED
Internal behavior differs in timing, ordering, concurrency, or flow.
- **Detection**: Async added/removed, operation ordering changed, concurrency introduced
- **Question for agents**: "Does code after the call assume the old ordering/timing?"
- **STOP condition**: Checked 3-5 dependent call sites

#### 4. SIDE EFFECT CONTRACT CHANGED
Observable effects added or removed (events, cleanup, logging, network calls).
- **Detection**: Event emissions added/removed, logging added/removed, cleanup functions modified
- **Question for agents**: "Did callers depend on the removed effect? Do they handle the new one?"
- **STOP condition**: Checked all listeners/subscribers for the changed effect

#### 5. FAILURE CONTRACT CHANGED
Error handling differs (throws vs returns null, new error types, different error shape).
- **Detection**: Try/catch modified, error types changed, null returns added where throws existed
- **Question for agents**: "Can callers handle the new failure mode?"
- **STOP condition**: Checked all callers for compatible error handling

#### 6. STATE LIFECYCLE CHANGED
New state variables, flags, or counters introduced or modified.
- **Detection**: New boolean flags, counters, status enums, state machine transitions
- **Question for agents**: "Is this state properly initialized, updated, AND reset across ALL code paths?"
- **STOP condition**: Found both SET and RESET paths, or confirmed RESET is missing

**For each trigger found, generate a trigger instruction block:**

```
TRIGGER: {TYPE} -- {function/variable name} {description of the change}
EXPLORATION REQUIRED: {specific question to investigate using Read/Grep tools}
STOP WHEN: {condition that satisfies the investigation}
```

**Example trigger instructions:**
```
TRIGGER: FAILURE CONTRACT -- parseConfig() now throws ConfigError instead of returning null
EXPLORATION REQUIRED: Read all callers of parseConfig() and verify they have try/catch for ConfigError. Check if any caller uses `if (result === null)` pattern.
STOP WHEN: Verified all callers handle the new error type, or confirmed a caller is missing the handler.

TRIGGER: STATE LIFECYCLE -- isLoading flag added to ChatStore
EXPLORATION REQUIRED: Grep for all usages of isLoading to verify it has SET (true), RESET (false), and is properly initialized. Check if any code path sets it to true without a corresponding reset.
STOP WHEN: Found complete lifecycle (init + set + reset) or confirmed a path is missing.
```

Save all trigger instructions to the triggers file:

```bash
# Write triggers.json with structure:
# {
#   "triggers": [
#     {
#       "type": "OUTPUT_CONTRACT_CHANGED",
#       "target": "parseConfig()",
#       "description": "Now throws ConfigError instead of returning null",
#       "exploration": "Read all callers...",
#       "stop_condition": "Verified all callers..."
#     }
#   ],
#   "trigger_instructions_text": "TRIGGER: FAILURE CONTRACT -- parseConfig()..."
# }
```

If no triggers detected, save an empty triggers array and set `trigger_instructions_text` to `"No contract change triggers detected."`.

### Step 7: Detect Toolchain

Detect the project's toolchain for use in auto-checks and fix verification. Run detection **in the worktree** so it picks up the PR's config files:

```bash
node packages/framework/bin/apt-tools.mjs toolchain-detect ${WORKTREE_PATH}
```

If `toolchain-detect` is not available, detect manually by checking for config files **in `${WORKTREE_PATH}`**:

1. **Linter**: `biome.json`/`biome.jsonc` (biome), `.eslintrc*`/`eslint.config.*` (eslint)
2. **Formatter**: `biome.json` (biome), `.prettierrc*` (prettier)
3. **Type checker**: `tsconfig.json` (tsc)
4. **Test runner**: `vitest.config.*` (vitest), `jest.config.*` (jest)
5. **Build**: Check `package.json` for `build` script
6. **Package.json scripts**: Fall back to `lint`, `typecheck`, `test`, `build` scripts

Construct a toolchain commands object:

```json
{
  "lint": "npx biome check .",
  "typecheck": "npx tsc --noEmit",
  "test": "npx vitest run",
  "build": "pnpm build",
  "all": "npx biome check . && npx tsc --noEmit && npx vitest run"
}
```

Save to `.aperant/pr-reviews/${REVIEW_ID}/toolchain.json`.

**Cache-bypass variant (FRAMEWORK-BUG-006 Part B).** `toolchain-detect` emits an additive
`no_cache_command` field on `typecheck` (and `test`) when the project's verify command is turbo-backed
(e.g. `typecheck: { tool, command, no_cache_command: "pnpm run typecheck --force" }`). A turbo-backed
verify can return a STALE CACHE HIT ("replaying logs") that reports PASS while a fixer-introduced broken
import sits on disk — sighted on PR #244, where only a `--force` (no-cache) typecheck exposed it.
**Whenever a `no_cache_command` exists for a check, every POST-FIX verification (auto-checks re-runs,
the Phase 6 fixer verification, and the Phase 7 self-reviewer) MUST use it instead of the plain
`command`** so a stale cache cannot mask a broken import. Non-turbo projects have no `no_cache_command`
(plain `tsc`/`vitest` have no cache to bust) and use the plain command unchanged.

Update `metadata.json` with the toolchain summary and trigger count.

---

## Phase 2: Auto-Checks

```bash
node packages/framework/bin/apt-tools.mjs workflow advance . --id ${REVIEW_ID} --phase auto-checks
```

Spawn 1 Agent to run the detected toolchain commands and report results.

**Spawn the auto-checks agent:**

```
Agent(
  name: "auto-checks",
  description: "Run automated lint, typecheck, and test checks",
  prompt: <see AUTO-CHECKS AGENT PROMPT below>,
)
```

Wait for the agent to complete. Read its brief summary.

**If auto-checks report failures:**

First, classify each failure:

| Classification | When | Action |
|---------------|------|--------|
| **PR-caused** | Failure is in a file changed by the diff | Becomes a finding (severity by type below) |
| **Pre-existing** | Failure is in unchanged files or infra | Note in report, do NOT create findings |
| **Environment** | Dependency install failed, module not found in worktree | Skip auto-checks entirely, note in metadata |

For PR-caused failures, severity:
  - Lint errors: `medium` (unless they indicate real bugs, then `high`)
  - Type errors: `high` (type safety is broken)
  - Test failures: `critical` (existing behavior is broken)
  - Build failures: `critical` (code cannot ship)

For PR-caused failures only:
- Include failure context in the trigger instructions for Phase 3 review agents
- Append to trigger instructions: `"AUTO-CHECK FAILURES: {summary of failures}. Factor this into your review -- the codebase has broken checks that may affect your analysis."`

**If auto-checks pass OR only have pre-existing/environment failures:** Proceed to Phase 3 with no additional finding context. Note any pre-existing failures in `metadata.json` under `auto_checks_notes`.

### AUTO-CHECKS AGENT PROMPT

```
You are an automated checks agent spawned by the /apt:pr-review orchestrator.

## Working Directory

All commands MUST run inside the worktree: {WORKTREE_PATH}
Run `cd {WORKTREE_PATH}` before executing any toolchain command.

## Your Job

Run all project checks using the detected toolchain and report results.

## Toolchain Commands

Run these commands in order FROM THE WORKTREE. If a command fails, continue running the remaining commands.

```bash
cd {WORKTREE_PATH}
```

1. Lint: {LINT_COMMAND}
2. Typecheck: {TYPECHECK_COMMAND}  (use toolchain.json `typecheck.no_cache_command` if present — FRAMEWORK-BUG-006: a turbo-backed typecheck can replay a stale cached PASS and mask a broken import)
3. Tests: {TEST_COMMAND}  (use toolchain.json `test.no_cache_command` if present, same cache-bypass rationale)

## Output

Write your DETAILED results to {REVIEW_DIR}/findings/auto-checks.md using this format:

```markdown
# Auto-Checks Results

**Review ID:** {REVIEW_ID}
**Status:** PASS | FAIL

## Results

### Lint
- **Status**: PASS | FAIL
- **Command**: {command}
- **Output** (if failed):
```
{full error output}
```

### Typecheck
- **Status**: PASS | FAIL
- **Command**: {command}
- **Output** (if failed):
```
{full error output}
```

### Tests
- **Status**: PASS | FAIL
- **Command**: {command}
- **Output** (if failed):
```
{full error output}
```

## Summary
- Passed: {N} of 3
- Failed: {list of failed checks}
```

Return ONLY a brief summary to the orchestrator:
"STATUS: {PASS/FAIL} | lint: {P/F} | typecheck: {P/F} | tests: {P/F}"

DO NOT return full error output in your response -- write it to the file.
```

---

## Phase 3: Parallel Review

```bash
node packages/framework/bin/apt-tools.mjs workflow advance . --id ${REVIEW_ID} --phase specialist-review
```

**CRITICAL: Spawn all 6 review agents in ONE message using 6 parallel Agent tool calls.**

Each agent receives:
1. The changed file list
2. The full git diff (or a reference to `{REVIEW_DIR}/diff.patch` -- for large diffs, reference the file)
3. All trigger instructions from Phase 1 (including auto-check failure context if applicable)
4. The path to write findings: `{REVIEW_DIR}/findings/{name}.md`
5. A reference to read `SCHEMA.md` for finding format details

**For each agent, construct the prompt by reading the corresponding agent definition file and filling in placeholders.**

### Agent Spawning Pattern

In a SINGLE message, make 6 Agent tool calls:

```
Agent(
  name: "security-reviewer",
  subagent_type: "apt-pr-review-security",
  description: "Security review of PR changes",
  prompt: "{CONSTRUCTED_PROMPT_WITH_FILLED_PLACEHOLDERS}",
)

Agent(
  name: "quality-reviewer",
  subagent_type: "apt-pr-review-quality",
  description: "Code quality review of PR changes",
  prompt: "{CONSTRUCTED_PROMPT_WITH_FILLED_PLACEHOLDERS}",
)

Agent(
  name: "logic-reviewer",
  subagent_type: "apt-pr-review-logic",
  description: "Logic and correctness review of PR changes",
  prompt: "{CONSTRUCTED_PROMPT_WITH_FILLED_PLACEHOLDERS}",
)

Agent(
  name: "codebase-fit-reviewer",
  subagent_type: "apt-pr-review-codebase-fit",
  description: "Codebase fit and convention review of PR changes",
  prompt: "{CONSTRUCTED_PROMPT_WITH_FILLED_PLACEHOLDERS}",
)

Agent(
  name: "test-integrity-reviewer",
  subagent_type: "apt-pr-review-test-integrity",
  description: "Test integrity review of PR changes",
  prompt: "{CONSTRUCTED_PROMPT_WITH_FILLED_PLACEHOLDERS}",
)

Agent(
  name: "performance-reviewer",
  subagent_type: "apt-pr-review-performance",
  description: "Performance review of PR changes",
  prompt: "{CONSTRUCTED_PROMPT_WITH_FILLED_PLACEHOLDERS}",
)
```

### Constructing Agent Prompts

For each of the 6 specialist agents, read the agent definition from `.claude/agents/apt-pr-review-{name}.md` and fill in these placeholders:

| Placeholder | Value |
|-------------|-------|
| `[FINDINGS_PATH]` | `.aperant/pr-reviews/{REVIEW_ID}/findings/{agent-name}.md` |
| `[TRIGGER_INSTRUCTIONS]` | The complete trigger instruction text from Phase 1 step 6 (or `"No contract change triggers detected."`) |
| `[CHANGED_FILES]` | The list of changed files from Phase 1 step 4 |
| `[GIT_DIFF]` | The git diff output from Phase 1 step 4 |
| `[WORKTREE_PATH]` | The worktree path from Phase 0 (or `.` for HEAD vs main / staged reviews) |
| `[LOCKED_DECISIONS]` | **(R6)** ≤500-token digest from `.aperant/context/notes/{task-id}.md` if a task-id can be derived from the PR/branch. Empty string if no ledger found. |

**R6 — Decision ledger inject (required):** Before spawning the 6 specialist agents, look for `.aperant/context/notes/{task-id}.md` (where task-id is parseable from the PR branch name or the latest task directory). If found, read the file and extract a ≤500-token digest of locked decisions. Pass it into every specialist prompt as `[LOCKED_DECISIONS]` so reviewers don't flag decisions the team already settled. If no ledger exists, pass an empty string and skip the block.

Append to each agent prompt:

```
## Working Directory

All file reads and searches MUST target the worktree: {WORKTREE_PATH}
When using Read, Grep, or Glob tools, prefix file paths with {WORKTREE_PATH}/.
The diff is at: {REVIEW_DIR}/diff.patch (use as a guide for what changed).
Read actual source files in the worktree — do NOT rely on the diff alone.

## Locked Decisions (R6 — respect or flag as out-of-scope)
{LOCKED_DECISIONS}

## Review Context

Review ID: {REVIEW_ID}
Review Directory: {REVIEW_DIR}

## Changed Files
{CHANGED_FILES}

## Git Diff (Reference -- Read actual files in the worktree for full context)
{GIT_DIFF}

Use the diff as a GUIDE, but ALWAYS Read actual files from {WORKTREE_PATH} before reporting findings.

## Finding Output Path
Write your findings to: {FINDINGS_PATH}

## Schema Reference
Read .claude/skills/apt-pr-review/SCHEMA.md for the complete finding format specification.
```

### Handling Large Diffs

If the diff exceeds approximately 3000 lines:
- Do NOT embed the full diff in each agent prompt (this wastes agent context)
- Instead, embed only the changed file list and tell agents to read `{REVIEW_DIR}/diff.patch` directly
- Append to the prompt: `"The diff is large. Read {REVIEW_DIR}/diff.patch for the full diff, or read individual changed files directly."`

### Waiting for Completion

Wait for all 6 agents to complete. Each returns a brief 3-5 line summary:
- Finding count
- Severity breakdown
- Most critical issue (if any)

Note the summaries for use in Phase 5 prioritization, but do NOT read the full finding files yet. The finding files are for Phase 4 (validator) and Phase 5 (synthesis).

---

## Phase 4: Finding Validation

```bash
node packages/framework/bin/apt-tools.mjs workflow advance . --id ${REVIEW_ID} --phase validation
```

Spawn 1 Agent to re-investigate every finding from all review agents.

**Why validate?** Review agents working from a diff can produce false positives -- issues that look real in the diff but are handled elsewhere, or code that has changed since the diff was captured. The validator independently verifies each finding against the actual codebase.

### Spawn the Validator Agent

```
Agent(
  name: "finding-validator",
  subagent_type: "apt-pr-review-validator",
  description: "Re-investigate and validate all review findings",
  prompt: "{CONSTRUCTED_PROMPT_WITH_FILLED_PLACEHOLDERS}",
)
```

Read the validator agent definition from `.claude/agents/apt-pr-review-validator.md` and fill in:

| Placeholder | Value |
|-------------|-------|
| `[REVIEW_DIR]` | `.aperant/pr-reviews/{REVIEW_ID}` |

Append to the prompt:

```
## Working Directory

All file reads and searches to verify findings MUST target the worktree: {WORKTREE_PATH}
When using Read, Grep, or Glob tools, prefix file paths with {WORKTREE_PATH}/.
This ensures you are validating against the actual PR code, not the user's working directory.

## Validation Context

Review ID: {REVIEW_ID}
Review Directory: {REVIEW_DIR}

Read all finding files from {REVIEW_DIR}/findings/ (excluding validated.md if it exists).
The finding files are: security.md, quality.md, logic.md, codebase-fit.md, test-integrity.md, performance.md

For each finding in each file, run the hypothesis-validation process from your instructions.

Write your validated results to: {REVIEW_DIR}/findings/validated.md
```

Wait for the validator to complete. It returns a brief summary:
```
VALIDATED: X confirmed | Y dismissed | A ambiguous | J author_judgment
```

### Step E: Deterministic Evidence Rule (R6)

After the LLM validator writes `findings/validated.md`, run the deterministic Evidence Rule check. This is a code-level guardrail against two classes of false positive that the LLM validator reliably misses:

1. **Fabricated / paraphrased evidence** — the reviewer's `Evidence:` code block was not a literal substring of the cited file range. Caught by literal substring match (whitespace-normalized).
2. **Multi-file claims without cross-file evidence** — title or description cites more than one file but only the primary file has an evidence snippet. Caught by the file-extension-token regex + `cross_file_evidence` array check.

```bash
node packages/framework/bin/apt-tools.mjs validate-evidence \
  .aperant/pr-reviews/${REVIEW_ID}/findings/ \
  --worktree ${WORKTREE_PATH}
```

The command mutates the findings markdown in place, stamping each finding with one of:
- `confirmed_valid` (unchanged) — evidence passed the literal + cross-file checks
- `dismissed_evidence_not_literal` — primary evidence wasn't literal
- `dismissed_multi_file_evidence_missing` — multi-file claim without matching `cross_file_evidence`

The JSON summary on stdout (`{validated, rejected, rejections: [...]}`) should be logged. Rejected findings never reach Phase 6/6b. This is the primary defense against the /apt:pr-review FIT-class regression where paraphrased evidence hit production PR comments.

---

## Phase 5: Synthesis

```bash
node packages/framework/bin/apt-tools.mjs workflow advance . --id ${REVIEW_ID} --phase synthesis
```

**This phase is executed by YOU (the orchestrator), not by a sub-agent.**

### Step 1: Read Validated Findings

Read `.aperant/pr-reviews/{REVIEW_ID}/findings/validated.md` using the Read tool.

Parse the validated findings into three groups:
- **Confirmed valid** -- these proceed to fix/report
- **Dismissed (false positives)** -- these are counted in stats but not acted on
- **Needs human review** -- these are included in the report with a flag

### Step 2: Cross-Validate

Check which findings were independently flagged by multiple review agents. To do this:
- Read each original finding file (`security.md`, `quality.md`, etc.)
- Compare file + line + issue description across agents
- When the same issue appears in multiple agent findings, mark it with `flagged_by: [agent1, agent2]`
- Cross-validated findings have stronger signal -- note this in the review.json

### Step 3: Deduplicate

For findings that overlap (same file, same line range, same fundamental issue):
- Merge into a single finding
- Keep the most detailed description
- Combine `flagged_by` arrays
- Keep the highest severity among duplicates

### Step 4: Group for Fix Tasks

Group confirmed findings into fix assignments by file overlap (max 3 groups):

1. **By file overlap**: Issues touching the same files go in the same group
2. **By logical domain**: Related concerns together (e.g., all import pattern issues)
3. **By complexity**: Simple fixes batched, complex fixes get dedicated groups

### Step 5: Compute Scores

Score each dimension 1-10 based on validated findings:

| Dimension | What lowers the score |
|-----------|----------------------|
| Security | Security findings (critical = -4, high = -2, medium = -1) |
| Quality | Quality findings (same scale) |
| Logic | Logic findings (same scale) |
| Architecture | Codebase-fit findings (same scale) |
| Performance | Performance findings (same scale) |

Start at 10 for each dimension. Subtract based on finding severity. Floor at 1.

### Step 6: Determine Assessment

| Assessment | Condition |
|-----------|-----------|
| `approve` | Zero critical or high findings remaining after validation. |
| `request-changes` | One or more critical or high findings remain. |
| `comment` | Only medium/low findings remain. Informational review. |

### Step 7: Write review.json

Write the structured review output to `.aperant/pr-reviews/{REVIEW_ID}/review.json`:

```json
{
  "review_id": "{REVIEW_ID}",
  "assessment": "approve|request-changes|comment",
  "summary": "1-2 sentence human-readable assessment of the changes",
  "scores": {
    "security": 9,
    "quality": 8,
    "logic": 8,
    "architecture": 9,
    "performance": 8
  },
  "findings": [
    {
      "id": "SEC-001",
      "severity": "high",
      "category": "security",
      "title": "...",
      "description": "...",
      "impact": "...",
      "file": "...",
      "line": 42,
      "evidence": "...",
      "evidence_context": { "...": "..." },
      "suggested_fix": "...",
      "fixable": true,
      "flagged_by": ["security", "quality"],
      "validation_status": "confirmed_valid"
    }
  ],
  "stats": {
    "files_reviewed": 12,
    "findings_total": 5,
    "critical": 0,
    "high": 1,
    "medium": 3,
    "low": 1,
    "false_positives_dismissed": 2
  },
  "iterations": {
    "total": 0,
    "findings_per_iteration": []
  },
  "created_at": "{ISO timestamp}",
  "completed_at": null
}
```

### Step 8: Print Intermediate Report

Before proceeding to Phase 6, output an intermediate report to the user:

```
Review Synthesis (Before Auto-Fix)

Auto-Checks: {PASS/FAIL}
  lint: {P/F} | typecheck: {P/F} | tests: {P/F}

Validated Findings:
  CRITICAL: {N}
  HIGH: {N}
  MEDIUM: {N}
  LOW: {N}

False Positives Dismissed: {N}
Evidence Ambiguous: {A}
Author Judgment Required: {J}

Cross-Validated (multiple agents agreed):
  {list of findings flagged by 2+ agents}

Assessment: {approve / request-changes / comment}

Proceeding to auto-fix {N} fixable issues in {M} groups...
```

**IMPORTANT: Immediately proceed to Phase 6 if any fixable issues were found and autonomy allows it.** Do not ask the user unless the assessment is `request-changes` with critical findings.

### Step 9: Cross-Model FP Gate (external mode only — R6)

**Only runs when `AUTHORSHIP_MODE == "external"`.** Owned-mode PRs skip this step — the auto-fix path is reversible, so a single-model false positive has a bounded blast radius (a diff commit can be reverted). External-mode findings are going straight to a public PR comment the author will read, so a second opinion from a different model is worth the latency.

For every finding with `validation_status == "confirmed_valid"` AND `severity ∈ {high, medium}`:

1. Read the cited file range (`file` + `evidence_context.line_range_examined`). Include 5 lines of context on either side.
2. Render the prompt from `packages/framework/skills/apt-pr-review/prompts/fp-gate-codex.md`, substituting:
   - `{FINDING_JSON}` — the finding's full JSON (severity, title, description, evidence, file, line).
   - `{FILE_SNIPPET}` — the lines read in step 1.
   - `{CLAUDE_MD}` — first 4k characters of the project's `CLAUDE.md` (for project conventions).
   - `{REVIEWER_EVIDENCE}` — the finding's `evidence` field verbatim.
3. Invoke codex using the shape configured at `.aperant/config.json:router.llm.providers.codex`:
   ```bash
   codex exec --input-file /tmp/fp-gate-${FINDING_ID}.md
   ```
   Parse the JSON response `{verdict, reasoning, evidence_quote, severity_assessment}`.
4. If `verdict == "falsify"`, set the finding's `validation_status = "dismissed_cross_model_falsification"` and copy the codex response into `fp_gate_result`. If `verdict == "confirm"`, pass through unchanged.

**Graceful degradation:** if codex CLI is missing (`host-detect` capability check), or the call times out, write a `fp-gate-degraded-${TIMESTAMP}.json` event to `.aperant/events/` (same pattern as `fs-lock` timeouts) and let findings through unchanged — the deterministic Evidence Rule check above (Phase 4 Step E) is the non-negotiable floor; the cross-model gate is an additional, best-effort guard.

**Critical findings bypass this gate.** Suppressing a critical regression because codex disagrees is a worse failure mode than a false-positive critical comment — a human will read it either way.

Typical cost: 3-8 codex calls per external round. Keep `--timeout-ms` conservative (15000) so a flaky codex session doesn't stall the whole pipeline.

---

## Phase 6: Auto-Fix Loop

```bash
node packages/framework/bin/apt-tools.mjs workflow advance . --id ${REVIEW_ID} --phase fix-loop
```

### Authorship Gate (R4)

**If `AUTHORSHIP_MODE == "external"`, SKIP this entire phase and jump to Phase 6b.** External PRs run a comment-only pipeline — we never push fixes to contributor branches. The `workflow advance --phase fix-loop` call still runs so the workflow state machine stays consistent, but the fix work itself is skipped.

For `AUTHORSHIP_MODE == "owned"`, continue normally.

### Principle: Fix Everything

AI agents have near-zero marginal cost per fix. The framework fixes **all** confirmed findings — critical, high, medium, and low — regardless of autonomy level. There is no severity gate and no "report for manual resolution" path. If a finding was confirmed by the validator, it gets fixed.

**Hard rule — context budget is NOT a valid skip reason.** The orchestrator MAY NOT defer, drop, downgrade, or "punt for follow-up" any confirmed finding because its own context window feels heavy, because the run has already produced many fixes, or because it judges marginal value low. The query loop auto-compacts on its own — trust it. Per-finding fix work happens inside fix agents (separate context windows), so orchestrator pressure does not justify cutting their inputs. The only valid skip reasons are the ones enumerated in Key Principle #2 (`fixable: false`, critical finding without user approval, file assigned to another fixer). Any other skip is an unauthorized policy deviation — emit all confirmed findings to fixers, then let the audit gate adjudicate outcomes.

| Autonomy | Action |
|----------|--------|
| `< 2` | Show findings summary before fixing. Then proceed with auto-fix. |
| `>= 2` | Proceed with auto-fix immediately. |

Read the autonomy level from `.aperant/config.json` to determine whether to show findings first:

```bash
cat .aperant/config.json
```

If config is missing or unreadable, default to autonomy `2` (fix immediately).

### Critical Finding Gate

Critical findings ALWAYS require explicit user confirmation before any fix attempt, regardless of autonomy level.

For each critical finding:
1. Display the finding to the user (id, title, file, line, evidence, suggested fix)
2. Ask: "Fix this critical finding? (y/n)"
3. Track the response as `CRITICAL_APPROVAL` for each finding

Only findings with `CRITICAL_APPROVAL = true` are sent to fix agents.

Findings marked `fixable: false` are never sent to fix agents.

### Round Budget Model (R5)

`/apt:pr-review` uses two different loop shapes depending on `AUTHORSHIP_MODE`. The overall budget is the same (4 attempts max with a discovery/verification split) but the unit differs:

- **`owned` mode (same as before R5):** one `/apt:pr-review` invocation runs **up to 4 iterations in a single session** — the inner Iteration Loop below. Iterations 1-2 are discovery, 3-4 are verification-only. This is the existing behavior.
- **`external` mode:** one `/apt:pr-review` invocation runs **exactly one round** against the current head SHA, then posts the Phase 6b comment and exits. Rounds persist across invocations in `pr-state.json:rounds_completed` (tracked by `pr-review start-round`). Rounds 1-2 are discovery, 3-4 are verification. Max rounds default to 4 (configurable via `pr_review.max_rounds`).

When `rounds_completed >= max_rounds`, `pr-review start-round` returns `{status: "exhausted", max_rounds}` and exits non-zero. The orchestrator must then exit cleanly and post a final "budget exhausted — no further review rounds will be attempted" comment in external mode, or stop iterating in owned mode.

#### Regression-From-Fix Carve-Out (verification rounds)

In verification phase (round/iteration >= 3) the default rule is "don't emit new findings — only verify existing ones." This prevents endless fishing. BUT: if the author's fix commits between rounds introduce a CRITICAL-severity regression, suppressing it would ship a real bug. The carve-out:

A specialist agent MAY emit a new finding in verification phase if ALL three hold:
1. Severity is **critical**.
2. The code is in the diff between `PRIOR_ROUND_HEAD_SHA` and the current head SHA, AND the diff modifies (not just adds to) that code.
3. The finding is not present in the prior round's `findings/validated.json`.

The agent marks the finding with `regression_from_fix: true` and `regression_from_fix_round: {PRIOR_ROUND}`. The validator (Phase 4) honors this field and does NOT dismiss the finding as "out-of-scope for verification phase." Medium/low regressions are silently skipped — they'll be caught by the author or by a future cycle.

Each of the 6 Phase 3 specialist agent definitions carries a `## Regression Detection (verification rounds only)` block with this carve-out written out.

### Iteration Loop (owned mode — max 4 iterations, discovery/verification split)

This is the owned-mode path. External-mode invocations run Phase 6b instead of this loop and return control to the caller. The loop has two distinct phases to prevent infinite new-issue discovery:

- **Iterations 1-2 (discovery):** Fresh re-review may find new issues beyond the original set. All new findings are added to the backlog and fixed.
- **Iterations 3-4 (verification-only):** Re-review ONLY checks whether previously found issues are resolved. New discoveries are ignored — they do not enter the backlog, UNLESS they satisfy the regression-from-fix carve-out above. If all prior issues are confirmed fixed, break immediately.

```
for ITERATION in 1 2 3 4:
  1. Create iteration directory
  2. Group findings into fix assignments (max 3 groups)
  3. Spawn fix agents ONE AT A TIME (never in parallel — FRAMEWORK-BUG-006):
       for each group: spawn fixer → wait → audit (Step 4b) → next group
  4. (folded into step 3's per-fixer wait)
  5. Generate new diff
  6. If fixes were applied: spawn fresh review on new diff
  7. If ITERATION <= 2: accept new findings into the backlog
     If ITERATION > 2: only check prior findings — ignore new discoveries
  8. If 0 outstanding findings: BREAK
  9. If findings remain: continue to next iteration
```

#### Step 1: Create Iteration Directory

```bash
mkdir -p .aperant/pr-reviews/${REVIEW_ID}/iterations/${ITERATION}
mkdir -p .aperant/pr-reviews/${REVIEW_ID}/iterations/${ITERATION}/findings
```

#### Step 2: Group Findings into Fix Assignments

Take all confirmed, fixable findings and group them into up to 3 independent groups by file overlap:
- Issues touching the same files go in the same group
- Each group gets a list of finding IDs and the files they cover
- No two groups should share files (prevents concurrent edit conflicts)

#### Step 3: Spawn Fix Agents

> **HARD RULE (FRAMEWORK-BUG-006): NEVER run pr-review fixers in parallel in a shared worktree.**
> All fixers share ONE worktree, ONE git index, and ONE branch. git's index is not a
> concurrency-safe surface — concurrent `git add` / `git commit` / self-audit `git log`/`git diff`
> from sibling fixers either SWEEP one fixer's staged files into another's commit (PR #114
> entanglement) or CLOBBER another fixer's working-tree edits entirely (PR #244 work-loss). Spawn
> fixers **one at a time**: spawn fixer-1, wait for it to return, run the Step 4b audit-fixer gate on
> it, THEN spawn fixer-2, and so on. At most one fixer touches the shared worktree at any moment.

**Capture the iteration-start SHA BEFORE spawning fixers** (used by both the Step 4b audit gate AND the per-fixer Pre-Return Self-Audit added in BUG-016 — fixers now commit atomically per finding, and the audit window is scoped by this SHA):

```bash
ITERATION_START_SHA=$(cd "${WORKTREE_PATH}" && git rev-parse HEAD)
```

For each fix group, spawn a fix agent. Read the agent definition from `.claude/agents/apt-pr-review-fixer.md` and fill in:

| Placeholder | Value |
|-------------|-------|
| `[REVIEW_DIR]` | `.aperant/pr-reviews/{REVIEW_ID}` |
| `[FIX_ASSIGNMENT]` | List of finding IDs and files for this group |
| `[N]` | Current iteration number |
| `[FIXER_NAME]` | `fixer-1`, `fixer-2`, or `fixer-3` |
| `[TOOLCHAIN_COMMANDS]` | Commands from `toolchain.json` (the `all` command, or individual commands). **For the post-fix typecheck/test, pass the `no_cache_command` variant when `toolchain.json` provides one** (FRAMEWORK-BUG-006: a turbo-backed verify can replay a stale cached PASS and mask the fixer's own broken import). |
| `[CRITICAL_APPROVAL]` | `true` or `false` for each critical finding in this group |
| `[PARALLEL_FIXERS]` | Always `false` (FRAMEWORK-BUG-006). Fixers are spawned one at a time in the shared worktree — never concurrently. The placeholder is retained only so the fixer-agent Coordination Protocol can assert "you are the only fixer touching this worktree right now." |
| `[WORKTREE_PATH]` | The worktree path — fix agents edit files HERE, not in the user's working directory |
| `[ITERATION_START_SHA]` | The SHA captured BEFORE spawning fixers (see Step 4b's `ITERATION_START_SHA=$(...)` capture). Required for BUG-016 commit-discipline Pre-Return Self-Audit. |

```
Agent(
  name: "fixer-1",
  subagent_type: "apt-pr-review-fixer",
  description: "Fix review findings group 1",
  prompt: "{CONSTRUCTED_PROMPT_WITH_FILLED_PLACEHOLDERS}",
)
```

If there are 2-3 groups, spawn the fixers **sequentially — one fixer at a time, never in parallel**
(FRAMEWORK-BUG-006). Concretely, loop over the groups:

```
for each fix group (in order):
  1. Spawn the fixer for THIS group only (a single Agent call in its own message)
  2. Wait for it to return its brief status line (Step 4)
  3. Run the Step 4b audit-fixer gate on it
  4. Only THEN spawn the fixer for the next group
```

Do NOT batch multiple `Agent(...)` fixer calls into one message — that is the parallel-shared-worktree
race this bug fixes. The atomic-per-finding commit discipline (BUG-016) and the Step 4b audit gate are
UNCHANGED; they now simply run without a concurrent sibling mutating the index.

#### Step 4: Wait for Completion

Wait for the **current** fix agent to return its brief status (you spawned only one this turn):
```
FIXED: X | FAILED: Y | SKIPPED: Z | VERIFICATION: PASS/FAIL
```

#### Step 4b: Audit Fixer Output (Hallucination Gate)

On 2026-04-20 a live fix agent returned `FIXED: 2 | FAILED: 0 | SKIPPED: 0 | VERIFICATION: PASS` and wrote a convincing 140-line `fixes-applied.md` — but the worktree had zero edits matching the claimed files. The orchestrator trusted the self-reported status line and nearly shipped two fabricated fixes. This step is the deterministic gate that closes that hole: for the fixer that just returned in Step 4 (fixers run one at a time — FRAMEWORK-BUG-006), run the audit CLI and cross-check the fixer's claims against the actual git state BEFORE spawning the next group's fixer.

**Capture the iteration-start SHA BEFORE spawning fixers** so the audit can cover committed fixes (bug-5, 2026-04-23 — fixers that commit their edits before returning would otherwise present an empty working tree and trip a false-positive hallucination verdict):

```bash
# Before Step 3 (Spawn Fix Agents), capture:
ITERATION_START_SHA=$(cd "${WORKTREE_PATH}" && git rev-parse HEAD)
```

Then, for each returned fixer, run:

```bash
node packages/framework/bin/apt-tools.mjs pr-review audit-fixer . \
  --review-dir "${REVIEW_DIR}" \
  --iteration "${ITERATION}" \
  --fixer "${FIXER_NAME}" \
  --worktree "${WORKTREE_PATH}" \
  --status-line "${STATUS_LINE}" \
  --since-sha "${ITERATION_START_SHA}"
```

`--since-sha <sha>` is **authoritative** and strongly recommended: when supplied, the audit window is the UNION of uncommitted diff (`git diff --cached` ∪ `git diff` ∪ `git ls-files --others`) AND committed diff (`git diff ${sinceSha}..HEAD --name-only`). This makes the gate robust to fixers that commit their edits before returning, and it scopes the committed window to exactly the CURRENT iteration.

If `--since-sha` is **omitted**, the CLI falls back to `git merge-base HEAD origin/main` (then `origin/master`) as a **best-effort** default. Semantics of each branch of the fallback chain:

1. **`--since-sha <sha>` supplied** → audit window is `[sha..HEAD]` ∪ uncommitted. This is the ONLY branch that bounds the committed window to the current iteration. `observed.since_sha_source = "flag"`. No stderr hint.
2. **`--since-sha` omitted AND `git merge-base HEAD origin/main` (or `origin/master`) resolves** → audit window is `[merge-base..HEAD]` ∪ uncommitted. This widens the committed window to the **entire PR branch**, not the current iteration. A no-op fixer in iteration N can therefore still be stamped `verified` if ANY prior iteration committed matching files. `observed.since_sha_source = "merge-base"`. No stderr hint. **Orchestrators MUST pass `--since-sha` to avoid this degradation** — relying on the merge-base fallback silently loses per-iteration verification strictness.
3. **`--since-sha` omitted AND `origin/main` is not fetched locally (no `refs/remotes/origin/main`)** → `merge-base` errors, `tryResolveDefaultSinceSha` returns `null`, and the audit stays on the legacy uncommitted-only window. `observed.since_sha = null`, `observed.since_sha_source = null`. **Stderr HINT fires** on any empty-diff hallucination verdict, pointing at `--since-sha` as the escape hatch.
4. **`--since-sha` omitted AND HEAD has no common ancestor with `origin/main` or `origin/master`** (unrelated histories, e.g. an orphan branch) → same as case (3): `merge-base` errors in the try/catch, `null` returned, legacy window + stderr hint on empty-diff verdicts.

Rule of thumb: pass `--since-sha "${ITERATION_START_SHA}"` on every audit invocation. The merge-base fallback exists only so a missing flag doesn't flip every standard-branch-workflow repo into a false-positive hallucination — it is NOT a replacement for the explicit flag.

The CLI returns a JSON envelope with `verdict` ∈ `verified | hallucinated | skipped`, writes `iterations/${ITERATION}/audit.json` (append-only), and bumps `metadata.json.fixer_hallucinations_total` on every `hallucinated` outcome. The envelope's `observed.since_sha` + `observed.since_sha_source` fields record which SHA was used and where it came from (`flag` | `merge-base` | `null`). The same envelope is used for the fixer and the self-reviewer (Phase 7) — `--fixer self-reviewer` reads from `self-review.md`.

**Canonical fix-report filename per fixer (BUG-030):** each fixer agent writes to `fixes-applied-${FIXER_NAME}.md` (e.g. `fixes-applied-fixer-1.md`). The audit CLI resolves candidates in strict order: `fixes-applied-group{N}.md` (legacy group-named, pre-BUG-030 reviews) → `fixes-applied-fixer-{N}.md` (NEW fixer-named) → `fixes-applied-${FIXER_NAME}.md` (exact match, preferred for non-numeric fixer names) → `fixes-applied.md` (legacy unscoped, single-fixer reviews only). Writing the canonical name eliminates the cross-fixer false-positive `hallucinated` verdict that fired when fixers shared `fixes-applied.md` (a per-fixer report path is still correct now that fixers run sequentially — each fixer owns its own report file).

| Verdict | Orchestrator Action |
|---------|---------------------|
| `verified` | Proceed to Step 5 for this fixer. |
| `skipped` | Proceed to Step 5 for this fixer. Log the entry in `audit.json` / `metadata.json.fixer_verdicts[]` — not a hallucination, the fixer legitimately skipped all findings (e.g. all `fixable: false`). |
| `hallucinated` | Do **NOT** count the fixer's claimed fixes as resolved — leave those findings in the outstanding backlog. If `iteration <= 2` AND the fixer is not yet one-strike: re-spawn the same fixer with an `[HALLUCINATION_CALLOUT]` block prepended to its prompt quoting the `audit.json` entry and stating "your last run reported PASS but the worktree showed no edits; you MUST actually edit files this time." Mark the fixer one-strike in `audit.json`. If the fixer hallucinates twice OR `iteration > 2`, set `metadata.status = "needs-human-review"` and mark the unresolved findings `validation_status: "needs_human_review"`. Do NOT silently ship the review. |

Fail-closed: on parse/IO errors (missing `fixes-applied.md`, malformed status line, unreadable worktree) the CLI returns `verdict: "hallucinated"` with a human-readable `reason`. Trust that envelope — do not re-parse the fixer's markdown from the orchestrator context. This keeps the orchestrator context light: the gate is a deterministic Node CLI, not an LLM judgement.

#### Step 5: Read Fix Reports and Generate New Diff

Read each fixer's report from `.aperant/pr-reviews/${REVIEW_ID}/iterations/${ITERATION}/fixes-applied.md`.

Generate the new diff showing what was fixed (from the worktree):
```bash
cd ${WORKTREE_PATH} && git diff > ../${REVIEW_ID}/iterations/${ITERATION}/diff.patch
```

Or if `WORKTREE_PATH=.`:
```bash
git diff > .aperant/pr-reviews/${REVIEW_ID}/iterations/${ITERATION}/diff.patch
```

#### Step 6: Fresh Re-Review (if fixes were applied)

**IMPORTANT: Each iteration spawns FRESH review agents to prevent anchoring bias.**

If fixes were applied:

1. Capture the new diff (post-fix changes vs the original base)
2. Spawn fresh review agents (same 6 specialists) on the NEW code state
3. Each fresh agent writes to `.aperant/pr-reviews/${REVIEW_ID}/iterations/${ITERATION}/findings/`
4. Spawn a fresh validator on the new findings
5. Read validated results

This ensures review agents are not biased by previous findings -- they see the code fresh.

**For re-review iterations, ONLY spawn agents whose categories had findings in the previous round.** If security had 0 findings and no fixes touched security-relevant code, do not re-spawn the security agent. This saves time.

#### Step 7: Apply Discovery/Verification Split

**Iterations 1-2 (discovery):** Merge any new confirmed findings from the re-review into the outstanding backlog. These will be fixed in the next iteration.

**Iterations 3-4 (verification-only):** Ignore any newly discovered findings — they are not added to the backlog. Only check whether previously identified findings are now resolved. If `apt:pr-review` surfaces genuinely new issues at this stage, they'll be caught if the user runs another review cycle or during the next PR review.

#### Step 8: Check for Convergence

After applying the split:
- If 0 outstanding findings remain: **BREAK the loop.** All issues resolved.
- If findings remain and iterations left: continue to the next iteration.
- If findings remain and all 4 iterations exhausted: mark remaining as `evidence_ambiguous` (or `author_judgment_required` if the blocker is design-intent, not evidence — see apt-pr-review-validator.md for the split, R6).

#### Step 9: Update Stats

After each iteration, update `metadata.json`:
```json
{
  "iterations_completed": {ITERATION},
  "stats": { "...updated counts..." }
}
```

### If All 4 Iterations Exhausted

If findings still remain after 4 iterations:
- Set remaining findings' `validation_status` to `evidence_ambiguous` or `author_judgment_required` (R6 — see the validator agent's criteria table)
- Include them in the final report
- Set metadata status to `needs-human-review` (metadata key name kept for backward compat; the per-finding enum value is the new split)
- Proceed to Phase 7

---

## Phase 6b: Post Comment to GitHub (external PRs only)

**Only runs when `AUTHORSHIP_MODE == "external"`.** Owned PRs skip straight to Phase 7.

Instead of pushing fixes, external PRs receive a single structured comment per round summarizing confirmed findings, dismissed false positives, and validator notes. Readers (the author, other maintainers, and future rounds of this same skill) rely on the comment being machine-parseable, so it ends with a tagged HTML footer.

### Step 1: Render the Comment Body

Use the `external-comment.md` template (`packages/framework/skills/apt-pr-review/templates/external-comment.md`) and substitute:

- `{ROUND}` — current round number (from `pr-state.json:rounds_completed`)
- `{REVIEW_ID}` — this round's review id
- `{CONFIRMED_FINDINGS_BY_SEVERITY}` — grouped markdown: CRITICAL → HIGH → MEDIUM → LOW
- `{DISMISSED_FPS}` — transparency block listing validator-dismissed findings + reason
- `{AUTO_CHECK_SUMMARY}` — lint/typecheck/test pass-fail table

Write the rendered body to `.aperant/pr-reviews/pr-${PR_NUMBER}/rounds/${ROUND}/comment.md`.

### Step 2: Post to GitHub

```bash
COMMENT_BODY=".aperant/pr-reviews/pr-${PR_NUMBER}/rounds/${ROUND}/comment.md"
COMMENT_URL=$(gh pr review ${PR_NUMBER} --comment --body-file "${COMMENT_BODY}" 2>&1 | tail -n1)
BODY_HASH=$(shasum -a 256 "${COMMENT_BODY}" | awk '{print $1}')
```

If `gh pr review` fails, retry up to 3 times with 2-second backoff. If all retries fail, surface the error to the user — do NOT mark the round successful with a missing comment.

### Step 3: Record in pr-state.json + comments/

Append an entry to `pr-state.json:comment_timeline` via the `record-comment` subcommand — this wraps the mutation in `withFileLock`, so it is safe under concurrent agents:

```bash
node packages/framework/bin/apt-tools.mjs pr-review record-comment . \
  --pr ${PR_NUMBER} \
  --round ${ROUND} \
  --url "${COMMENT_URL}" \
  --body-hash "${BODY_HASH}" \
  --review-id "${REVIEW_ID}"
```

Do NOT hand-edit `pr-state.json` from the orchestrator. Every other state mutation in this skill goes through an `apt-tools pr-review …` subcommand; `comment_timeline` follows the same rule (review M1).

Also write `.aperant/pr-reviews/pr-${PR_NUMBER}/comments/round-${ROUND}-posted.json` with the full payload (round, url, posted_at, body_hash, body text). Both files are allowlisted by `.aperant/.gitignore` so they survive across machines.

### Step 4: Skip Phases 7 + fix-push path in Phase 8

External PRs run no self-review (nothing local was fixed) and no push in Phase 8. Proceed directly to Phase 8 `Step 0c: Close Round + Release Lock`.

---

## Phase 7: Self-Review

```bash
# self-review is optional (required: false) — if no fixes were applied, skip to Phase 8
# and the workflow runner will auto-skip this phase
node packages/framework/bin/apt-tools.mjs workflow advance . --id ${REVIEW_ID} --phase self-review
```

### Authorship Gate (R4)

**If `AUTHORSHIP_MODE == "external"`, SKIP this phase.** Nothing was committed locally, so there's nothing to self-review. The workflow marks `self-review` as optional so advancing past it is safe.

For `AUTHORSHIP_MODE == "owned"`, continue below.

Spawn 1 fresh Agent to review the fix diffs with zero context pollution.

### Gather Fix Diffs

Combine all fix diffs across iterations:

```bash
# Concatenate all iteration diffs
cat .aperant/pr-reviews/${REVIEW_ID}/iterations/*/diff.patch > /tmp/all-fix-diffs.patch
```

If no fixes were applied (Phase 6 was skipped or found nothing to fix), **skip Phase 7**.

### Spawn Self-Review Agent

Read the agent definition from `.claude/agents/apt-pr-review-self-reviewer.md` and fill in:

| Placeholder | Value |
|-------------|-------|
| `[FIX_DIFF]` | The combined fix diff content or path to the concatenated file |
| `[REVIEW_DIR]` | `.aperant/pr-reviews/{REVIEW_ID}` |
| `[TOOLCHAIN_COMMANDS]` | Commands from `toolchain.json`. **Use the `no_cache_command` variant for the typecheck/test when one is present** (FRAMEWORK-BUG-006: a turbo-backed verify can replay a stale cached PASS and mask a broken import the fixes introduced). |
| `[WORKTREE_PATH]` | The worktree path — self-reviewer works HERE |

```
Agent(
  name: "self-reviewer",
  subagent_type: "apt-pr-review-self-reviewer",
  description: "Self-review of all fix diffs with fresh eyes",
  prompt: "{CONSTRUCTED_PROMPT_WITH_FILLED_PLACEHOLDERS}",
)
```

Wait for the self-reviewer to return ONE of:
- `CLEAN` -- no issues found in the fixes
- `FIXED` -- found and fixed issues in the fixes
- `NEEDS_HUMAN` -- found issues that could not be auto-fixed

Read `.aperant/pr-reviews/${REVIEW_ID}/self-review.md` for the detailed report.

---

## Phase 8: Push Fixes, Cleanup Worktree, Final Report

```bash
node packages/framework/bin/apt-tools.mjs workflow advance . --id ${REVIEW_ID} --phase report
```

**This phase is executed by YOU (the orchestrator).**

### Step 0: Push Fixes to PR Branch (if worktree was used and fixes were applied)

**If `AUTHORSHIP_MODE == "external"`, SKIP Step 0.** External PRs have already posted their comment in Phase 6b — pushing to a fork's branch is not our job (and usually not permitted). Jump straight to Step 0b (cleanup) and Step 0c (close round + release lock).

If `WORKTREE_PATH != .` AND fixes were applied in Phase 6/7:

Get the PR head ref (for branch reviews, use the branch name directly):
```bash
PR_HEAD_REF=$(gh pr view ${PR_NUMBER} --json headRefName -q .headRefName)
```

#### Confirmation gate (BUG-009 fix)

Whether the push prompts for confirmation is controlled by `pr_review.confirm_before_push` in `.aperant/config.json` (tri-state: `ask` | `auto` | `never`; default when absent: `auto` at autonomy ≥ 2, `ask` at autonomy < 2). Mirrors the `pr_review.suggest_after_ship` precedent in `/apt:ship` Section 7.1 — `ask` is honest about the irreversible-ish nature of the push (re-triggers CI on origin, visible to teammates); `auto` honors yolo pipelines.

```bash
confirm_mode=$(jq -r '.pr_review.confirm_before_push // empty' .aperant/config.json)
autonomy=$(jq -r '.autonomy.default // 2' .aperant/config.json)
if [ -z "$confirm_mode" ]; then
  if [ "${autonomy}" -ge 2 ]; then confirm_mode="auto"; else confirm_mode="ask"; fi
fi
```

Branch on `confirm_mode`:

- **`never`** — Skip push entirely. Print "fixes left in worktree; push manually with `git -C ${WORKTREE_PATH} push origin HEAD:${PR_HEAD_REF}`". Proceed to Step 0b cleanup.
- **`auto`** — Proceed silently to the catch-all + push below. No prompt.
- **`ask`** — Emit the structured question: `Fixes are ready in the worktree. Push to ${PR_HEAD_REF}? (y/n)`. On `n` or any non-`y`: same skip behavior as `never`. On `y` (or empty enter): proceed.

`confirm_before_push` is purely additive — it does not affect `authorship_override`, `authorship_overrides`, `max_rounds`, or `suggest_after_ship` in the same `pr_review` block.

#### Catch-all + push (BUG-010 fix)

The fixer pipeline normally produces atomic per-finding commits, so the worktree is clean by the time Step 0 runs. The catch-all commit below exists ONLY to capture uncommitted residue (rare — e.g. a fixer's pre-return self-audit failed to detect a stray edit). Guard it before committing:

```bash
cd ${WORKTREE_PATH}

# Guard 1: skip the catch-all entirely if the tree is clean.
# Per-finding commits with proper messages are strictly better history than a single catch-all.
if ! git diff --quiet || ! git diff --cached --quiet || [ -n "$(git ls-files --others --exclude-standard)" ]; then
  # Guard 2: explicit allowlist instead of `git add -A`.
  # `git add -A` would sweep orchestrator-level Read/Write side-effects
  # (metadata.json, audit.json, validated.md, etc.) into the PR. Bad.
  # Stage ONLY files in the review's touched set, computed from the iteration diffs.
  REVIEW_TOUCHED=$(git diff --name-only ${ITERATION_START_SHA:-HEAD~4}..HEAD 2>/dev/null \
                   | grep -v '^\.aperant/' \
                   | tr '\n' ' ')
  if [ -n "$REVIEW_TOUCHED" ]; then
    # shellcheck disable=SC2086
    git add -- $REVIEW_TOUCHED
  fi
  # Only commit if `git add --` actually staged something.
  if ! git diff --cached --quiet; then
    git commit -m "fix: auto-fix review findings from /apt:pr-review (review ${REVIEW_ID})"
  fi
fi

git push origin HEAD:${PR_HEAD_REF}
cd -
```

Two guards. The outer `if` skips the whole catch-all path when the tree is clean. The inner allowlist (filter to files in the iteration-diff scope, minus anything under `.aperant/`) prevents `git add` from sweeping arbitrary orchestrator side-effects into the PR. Both guards default to the safe no-op shape on the happy path (fixers committed atomically, clean tree).

### Step 0b: Cleanup Worktree

After pushing (or if no fixes were applied), clean up:

```bash
# Remove the worktree
git worktree remove .aperant/pr-reviews/${REVIEW_ID}/worktree --force 2>/dev/null

# Delete the temporary branch
git branch -D pr-review-${REVIEW_ID} 2>/dev/null
```

If `WORKTREE_PATH = .`, skip this step (no worktree was created).

### Step 0c: Close Round + Release Lock (PR reviews only)

If `PR_NUMBER` is set (we're reviewing a GitHub PR), close the round and release the PR-level lock. Close-round writes a durable snapshot to `.aperant/pr-reviews/pr-${PR_NUMBER}/rounds/${ROUND}/pr-state-snapshot.json` (committed) and clears the live lock:

```bash
if [ -n "$PR_NUMBER" ]; then
  node packages/framework/bin/apt-tools.mjs pr-review close-round . \
    --pr ${PR_NUMBER} --round ${ROUND}
fi
```

Close-round is idempotent on state but writes a fresh snapshot each time. If the review errored out before Step 0c, a subsequent `/apt:pr-review` invocation can still take the lock (stale after TTL) and re-open the round.

### Step 1: Read All Artifacts

Read the following files:
- `.aperant/pr-reviews/${REVIEW_ID}/review.json` (from Phase 5)
- `.aperant/pr-reviews/${REVIEW_ID}/self-review.md` (from Phase 7, if exists)
- `.aperant/pr-reviews/${REVIEW_ID}/iterations/*/fixes-applied.md` (all iteration reports)
- `.aperant/pr-reviews/${REVIEW_ID}/findings/validated.md` (validated findings)

### Step 2: Update review.json

Update the review.json with final data:

1. Recalculate finding counts (some may have been fixed)
2. Update `iterations.total` and `iterations.findings_per_iteration`
3. Set `completed_at` timestamp
4. Update `stats.false_positives_dismissed` count
5. Recalculate `assessment` based on remaining unfixed findings:
   - `approve`: 0 critical, 0 high remaining
   - `request-changes`: any critical or high remaining
   - `comment`: only medium/low remaining, or all issues were fixed

Write the updated review.json.

### Step 3: Update metadata.json

Update the metadata:
```json
{
  "status": "completed",
  "iterations_completed": {final count},
  "stats": { "...final counts..." }
}
```

If any findings have `validation_status` ∈ {`evidence_ambiguous`, `author_judgment_required`} (R6 — replaces the old `needs_human_review` status), set metadata status to `"needs-human-review"` instead of `"completed"`.

### Step 4: Print Final Report

Output the human-readable summary:

```
=============================================
  /apt:pr-review -- Review Complete
=============================================

  Review ID: {REVIEW_ID}
  Assessment: {APPROVE / REQUEST CHANGES / COMMENT}

  Scores:
    Security:      {score}/10
    Quality:       {score}/10
    Logic:         {score}/10
    Architecture:  {score}/10
    Performance:   {score}/10

  Findings: {total} ({critical} critical, {high} high, {medium} medium, {low} low)
  False Positives Dismissed: {N}
  Fix Iterations: {N}
  Self-Review: {CLEAN / FIXED / NEEDS_HUMAN}
  Fixer Hallucinations: {count}

  Report: .aperant/pr-reviews/{REVIEW_ID}/review.json
=============================================
```

The `Fixer Hallucinations` line is sourced from `metadata.json.fixer_hallucinations_total` (bumped by the Phase 6 Step 4b audit gate). When the count is `> 0`, also list each hallucinated fixer + iteration under the Remaining Issues block so the operator can audit the gate trace.

If the assessment is `request-changes`, also list the remaining unfixed findings:

```
  Remaining Issues (require attention):
    [{severity}] {id} -- {title}
      {file}:{line}
      {description (first 100 chars)}
```

If the assessment is `approve` and fixes were applied, list what was auto-fixed:

```
  Auto-Fixed Issues:
    {id} -- {title} ({file}:{line})
```

### Step 5: Destroy Workflow

Clean up the workflow gate now that the review is complete:

```bash
node packages/framework/bin/apt-tools.mjs workflow destroy . --id ${REVIEW_ID}
```

---

## Key Principles

### 1. Evidence-Based, Not Confidence-Based
Agents do NOT assign confidence percentages. Either they have evidence (actual code snippet from Read tool) or they do not have a finding. The validator independently verifies every finding against real code.

### 2. Fix Everything Found
If it was found and confirmed, fix it. ALL severities. "This is minor" is NOT a valid skip reason. **Neither is "my context window is heavy" or "I've already done a lot this run"** — orchestrator context pressure never authorizes dropping a confirmed finding (see Phase 6 → Principle: Fix Everything). The only valid skip reasons are: `fixable: false`, critical findings without user approval, or files assigned to another fixer.

### 3. Findings Flow Through Files
Agents write to `.aperant/pr-reviews/{review-id}/findings/`. The orchestrator reads with the Read tool. Fix agents read finding files directly. This keeps the orchestrator context under 30%.

### 4. Agents Are Autonomous
Fix agents and the self-reviewer handle the full fix-verify-stage pipeline. They do NOT come back to the orchestrator for approval (except critical findings). The self-reviewer fixes its own findings internally.

### 5. Fresh Eyes for Each Iteration
Each fix loop iteration spawns FRESH review agents. This prevents anchoring bias -- agents in iteration 2 do not know what iteration 1 found. They see the code with fresh perspective.

### 6. Trigger-Driven Exploration Is Mandatory
When trigger instructions are provided, review agents MUST investigate them even if the surface-level diff looks correct. Triggers point to contract changes that cause downstream bugs -- the most dangerous class of defects.

### 7. The Evidence Rule
**NO EVIDENCE = NO FINDING.** A finding without a literal code snippet in the `evidence` field is automatically rejected by the validator. Agents must copy-paste actual code from the Read tool, not describe it from memory.

### 8. Scope Discipline
Review agents ONLY review code changed in the diff. Pre-existing issues in unchanged code are NOT reported. This prevents noise and keeps findings actionable.

### 9. Fixer Claims Are Audited Against Git State
Fix agents self-report their outcome via a status line (`FIXED: X | FAILED: Y | SKIPPED: Z | VERIFICATION: PASS/FAIL`). That self-report is **not trusted on its own** — Phase 6 Step 4b runs `apt-tools pr-review audit-fixer` after every fixer returns, which cross-checks the claims against `git diff --cached --name-only ∪ git diff --name-only ∪ git ls-files --others`. A fixer that claims PASS with an empty diff is marked `hallucinated` deterministically and its findings stay in the backlog. This is the defense against the 2026-04-20 incident where an agent fabricated a 140-line fix report with zero matching edits.

**Dual-layer defense, explicitly:** the fixer and self-reviewer agent prompts include a Pre-Return Self-Audit step that asks them to run `git diff --stat` and refuse to emit `VERIFICATION: PASS` on an empty diff. This is **belt-and-suspenders only** — the same LLM that hallucinated a 140-line fix report would happily hallucinate a clean `git diff` output. Only the orchestrator-driven, out-of-process `apt-tools pr-review audit-fixer` check is a trusted defense. The self-audit catches the honest cases; the gate catches everything else.

---

## Common Mistakes (And How to Avoid Them)

### Mistake 1: Embedding Full Findings in Agent Summaries

**Wrong:** Reading the full finding text from agent responses and processing it in orchestrator context.
**Right:** Agent responses are brief (3-5 lines). All detail goes to files. Orchestrator reads files via Read tool only when needed.

### Mistake 2: Spawning the Phase 3 REVIEW Agents Sequentially

**Wrong:** Spawn security agent, wait, spawn quality agent, wait, etc.
**Right:** Spawn all 6 review agents in ONE message using 6 parallel Agent tool calls. They run concurrently.

> Scope note: this applies ONLY to the Phase 3 review specialists, which are read-only (Read/Grep/Glob,
> no Edit/Bash, no git writes) and therefore cannot race the index. The Phase 6 FIXERS are the
> opposite — they MUST be spawned one at a time (FRAMEWORK-BUG-006). Never parallelize fixers.

### Mistake 3: Skipping the Validator

**Wrong:** Sending raw review findings directly to fix agents.
**Right:** Always run the validator first. Review agents produce false positives. The validator catches them. Only validated findings go to fixers.

### Mistake 4: Re-Using Stale Agents for Re-Review

**Wrong:** Telling the same review agents to "look again" after fixes.
**Right:** Spawn FRESH agents for each re-review iteration. Fresh agents have no memory of previous findings and no anchoring bias.

### Mistake 5: Auto-Fixing Critical Findings Without Approval

**Wrong:** Letting fix agents change critical findings without user confirmation.
**Right:** Critical findings ALWAYS require explicit user confirmation. This is a hard gate regardless of autonomy level.

### Mistake 6: Sending Large Diffs in Agent Prompts

**Wrong:** Embedding a 5000-line diff in every agent prompt (6 copies = 30000 lines of context waste).
**Right:** For large diffs (>3000 lines), tell agents to read `{REVIEW_DIR}/diff.patch` directly. Only embed the changed file list.

### Mistake 7: Skipping Fixes Based on Autonomy

**Wrong:** Skipping Phase 6 entirely when autonomy < 2.
**Right:** Always fix all confirmed findings. At autonomy < 2, show findings summary first, then proceed with auto-fix. The only difference is whether findings are displayed before fixing — not whether they get fixed.

### Mistake 7a: Dropping Findings Because Orchestrator Context Feels Heavy

**Wrong:** Mid-Phase-6, the orchestrator decides "my window is getting heavy after 6 specialist rounds + Round 1 fixes — I'll skip the minor findings and report a partial fix." This is an unauthorized policy deviation, even when phrased as "judgment call" or "context-budget reasons."
**Right:** Spawn fix agents for **every** confirmed finding regardless of orchestrator context state. Fix work happens in the fixer's window, not yours. The query loop auto-compacts when it needs to — trust it. If you genuinely cannot fit the spawn calls, that is a framework bug to file (not a license to ship a partial fix); emit all findings to fixers in the smallest batches possible and let the audit gate adjudicate. Severity, "diminishing returns," and your own token pressure are NOT skip authorities — only the three reasons in Key Principle #2 are.

### Mistake 8: Forgetting to Update metadata.json

**Wrong:** Writing review.json but leaving metadata.json stuck at `"status": "in-progress"`.
**Right:** Update metadata.json at every phase transition. Final state should be `"completed"` or `"needs-human-review"`.

### Mistake 9: Processing All Agent Files in Orchestrator Context

**Wrong:** Reading all 6 finding files + validated.md in one go, causing context bloat.
**Right:** In Phase 5 synthesis, read validated.md first (it has the condensed verdicts). Only read original agent files if you need to resolve a specific cross-validation question.

### Mistake 10: Spawning Fixers for Non-Fixable Findings

**Wrong:** Including findings with `fixable: false` in fix agent assignments.
**Right:** Filter fix assignments to only include findings with `fixable: true` AND `validation_status: "confirmed_valid"`.

---

## Argument Reference

| Argument | Description | Example |
|----------|-------------|---------|
| `--pr NUMBER` | Review a pull request by number | `/apt:pr-review --pr 42` |
| `--branch NAME` | Review a branch vs main | `/apt:pr-review --branch feat/auth` |
| `--staged` | Review staged changes only | `/apt:pr-review --staged` |
| (no argument) | Review HEAD vs main | `/apt:pr-review` |
| `--no-fix` | Skip the auto-fix phase (Phase 6) | `/apt:pr-review --no-fix` |
| `--focus AREA` | Prioritize findings in a specific area | `/apt:pr-review --focus security` |

---

## Configuration Reference

### .aperant/config.json fields used by /apt:pr-review

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `autonomy` | number | 2 | Controls display behavior. `< 2` = show findings before fixing. `>= 2` = fix immediately. All findings are always fixed regardless of autonomy. |
| `pipeline.skip_review` | boolean | false | If true, `/apt:run` skips the review phase. Does not affect direct `/apt:pr-review` invocation. |
| `review.max_iterations` | number | 4 | Maximum fix-review iterations before stopping. |
| `review.optional_agents` | string[] | [] | Reserved for future use. Additional specialist agents to spawn beyond the core 6. |

---

## Agent Tool Reference

The orchestrator spawns agents using Claude Code's `Agent` tool. Each agent call follows this pattern:

```
Agent(
  name: "{human-readable-name}",
  subagent_type: "{agent-definition-filename-without-extension}",
  description: "{brief description of what this agent does}",
  prompt: "{the complete prompt with all placeholders filled in}",
)
```

### Agent Definitions (File Locations)

| Agent | Definition File | Tools | Phase |
|-------|----------------|-------|-------|
| Auto-checks | (inline prompt in this SKILL.md) | Bash, Read | 2 |
| Security reviewer | `.claude/agents/apt-pr-review-security.md` | Read, Write, Grep, Glob | 3 |
| Quality reviewer | `.claude/agents/apt-pr-review-quality.md` | Read, Write, Grep, Glob | 3 |
| Logic reviewer | `.claude/agents/apt-pr-review-logic.md` | Read, Write, Grep, Glob | 3 |
| Codebase-fit reviewer | `.claude/agents/apt-pr-review-codebase-fit.md` | Read, Write, Grep, Glob | 3 |
| Test-integrity reviewer | `.claude/agents/apt-pr-review-test-integrity.md` | Read, Write, Grep, Glob | 3 |
| Performance reviewer | `.claude/agents/apt-pr-review-performance.md` | Read, Write, Grep, Glob | 3 |
| Finding validator | `.claude/agents/apt-pr-review-validator.md` | Read, Write, Grep, Glob | 4 |
| Fix agent (x3) | `.claude/agents/apt-pr-review-fixer.md` | Read, Write, Edit, Bash, Grep, Glob | 6 |
| Self-reviewer | `.claude/agents/apt-pr-review-self-reviewer.md` | Read, Write, Edit, Bash, Grep, Glob | 7 |

### Tool Scoping

Review agents (Phase 3) have Read/Write/Grep/Glob. Write is used ONLY for writing findings to files — agents must NOT modify source code. This separation is intentional:
- Review agents write their findings to `.aperant/pr-reviews/{review-id}/findings/{name}.md`
- They do NOT have Edit or Bash — they cannot modify source code or run commands
- All source code changes go through the fix pipeline (Phase 6) with proper verification

Fix agents and self-reviewer have full editing tools (Read/Write/Edit/Bash/Grep/Glob). They:
- Apply fixes
- Run verification (lint, typecheck, tests)
- Stage changes (`git add`)
- Revert failed fixes (`git checkout -- {file}`)
