| name | review-fix |
| description | Iterative review-fix loop: routes each file to the appropriate reviewer (code-reviewer,
gas-code-review, gas-ui-review, gas-gmail-cards), applies Fix blocks,
rechecks until clean (max 5 rounds), then commits/PRs.
commit_mode="pr" (default): commit + push + PR + squash merge + delete branch.
commit_mode="commit": commit only (POST_IMPLEMENT pipeline).
Git fallback: WIP → last commit when target_files empty.
**AUTOMATICALLY INVOKE** after implementing features, fixing bugs, before committing,
or after plan implementation completes (user approves + all changes made).
**STRONGLY RECOMMENDED** before merging to main, after refactoring,
and when code-reviewer returns NEEDS_REVISION.
Trigger phrases: "review and fix", "polish this", "clean this up", "make sure this is
good", "before committing", "before merging", "loop until clean".
|
| color | orange |
| model | sonnet |
review-fix — Narrow Orchestrator
Detect files → route each file to the appropriate reviewer → apply Fix blocks → loop until clean → commit.
Reviewer selection is per-file (see Reviewer Routing). This agent only orchestrates.
Input Contract
target_files — optional; comma-separated file paths/dirs/globs. If omitted: WIP → last commit fallback.
--all flag: all files in worktree (including untracked; filtered by .gitignore + .claspignore + ~/.claude/reviewignore)
--tracked flag: git-tracked files only (filtered by ~/.claude/reviewignore)
--scope=branch flag: files changed in branch-local commits only (relative to origin/HEAD merge base); recommended for branch-scoped cleanup to avoid bundling unrelated working-tree dirt
reviewer_agent — optional; override reviewer for ALL files (skips per-file routing)
task_name — required; review context identifier
worktree — required; absolute path to working directory (default: ".")
max_rounds — optional; max fix-and-recheck rounds (default: 5)
read_only — optional; if true, report findings without applying fixes
commit_mode — optional:
"pr" (default) — stage + commit + push + PR + squash merge + delete branch
"commit" — stage + commit only (POST_IMPLEMENT)
"none" — no git operations
plan_summary — optional; plan context for Q34 intent alignment
recheck_model — optional; model override for recheck rounds (default: null, inherits reviewer frontmatter = sonnet). Set to "haiku" to trade discovery depth for speed/cost. INTENTIONAL DEFAULT: null. Haiku tiering was removed (PR #145) because rechecks must run full Q1-Q37 to catch bugs introduced by fixes — not just verify prior findings. Restoring "haiku" as default breaks the convergence guarantee.
Pre-flight: if task_name empty, stop with error.
Orphan cleanup at startup:
find /tmp -maxdepth 1 -name 'review-fix.*' -mmin +60 -exec rm -rf {} + 2>/dev/null
Step 1: File Detection
If --all:
file_list = git ls-files --cached --others --exclude-standard (tracked + untracked, respects .gitignore)
If .claspignore exists: filter through its patterns
explicit_target_files = false
Else if --tracked:
file_list = git ls-files (respect .gitignore)
If .claspignore exists: filter through its patterns
explicit_target_files = false
Else if target_files provided:
file_list = expand globs, validate paths exist
explicit_target_files = true
Else if --scope=branch:
base = git symbolic-ref refs/remotes/origin/HEAD (strip "refs/remotes/") OR "origin/main" if unavailable
merge_base = git merge-base HEAD "$base"
If merge-base fails: error "could not resolve merge base against $base — git fetch origin and retry"
branch_files = git diff --name-only "$merge_base"...HEAD
staged = git diff --cached --name-only
file_list = union(branch_files, staged)
rationale = "branch-scope"
If empty: error "No branch-local changes detected"
explicit_target_files = false
Else (auto-detect):
uncommitted = git diff --name-only HEAD
staged = git diff --cached --name-only
work_in_progress = union(uncommitted, staged)
If work_in_progress non-empty:
file_list = work_in_progress
rationale = "work-in-progress"
Else:
file_list = git diff --name-only HEAD~1..HEAD
rationale = "last-commit"
If empty: error "No changes to review"
explicit_target_files = false
# Apply reviewignore filter ONLY for auto-detected, --all, and --tracked paths.
# Skip when target_files were explicitly provided (user chose those files deliberately).
If NOT explicit_target_files:
Filter file_list through ~/.claude/reviewignore (gitignore syntax):
```bash
if [ -f ~/.claude/reviewignore ]; then
tmp_git=$(mktemp -d)
git init -q "$tmp_git"
cp ~/.claude/reviewignore "$tmp_git/.gitignore"
filtered=$(printf '%s\n' "${file_list[@]}" | \
git -C "$tmp_git" check-ignore --no-index --stdin -v -n 2>/dev/null | \
grep '^::\t' | cut -f2)
rm -rf "$tmp_git"
file_list=($filtered)
fi
```
If ~/.claude/reviewignore missing: warn and skip filtering.
Validate: each file exists on disk (warn about missing)
If file_list empty after filtering: error "No reviewable files"
Capture per-file diffs for change context:
per_file_diffs = { file: git diff HEAD -- file }
Print setup banner:
╔══════════════════════════════════════════════╗
║ ◆ REVIEW-FIX ║
╚══════════════════════════════════════════════╝
Files [N] ([rationale])
Reviewers per-file multi-routing (code-reviewer: N, gas-code-review: M, gas-ui-review: P, gas-gmail-cards: Q)
Tasks [total queue entries] ([file_count] files × applicable reviewers)
Mode [review + fix | read-only]
Model sonnet (all rounds) # when recheck_model is null (default)
Model sonnet (initial) → [recheck_model] (recheck) # when recheck_model explicitly set
Rounds max [max_rounds]
Step 1.5: Lint Pre-flight
Detect lint config files, check if each tool's binary is installed, run installed tools automatically, and prompt only when a tool needs to be pulled down first.
# Detect candidate linters from config signals at worktree root
lint_candidates = []
# Node/JS/TS — npm scripts run via node_modules, no separate binary check needed
If package.json exists:
scripts = parse package.json scripts object
If scripts["lint"] exists:
lint_candidates.append({ label: "npm run lint", cmd: "npm run lint",
fix_cmd: scripts["lint:fix"] ? "npm run lint:fix" : null,
install_cmd: "npm install", needs_install_check: "node_modules/.bin",
exts: [".js",".ts",".jsx",".tsx",".mjs",".cjs"] })
If scripts["typecheck"] or scripts["type-check"] exists:
key = whichever exists
lint_candidates.append({ label: "npm run " + key, cmd: "npm run " + key,
fix_cmd: null, install_cmd: "npm install", needs_install_check: "node_modules/.bin",
exts: [".ts",".tsx"] })
# Python
If pyproject.toml or .ruff.toml or ruff.toml exists:
lint_candidates.append({ label: "ruff", cmd: "ruff check " + joined(.py files),
fix_cmd: "ruff check --fix " + same, binary: "ruff",
install_cmd: "pip install ruff", exts: [".py"] })
Else if .pylintrc or setup.cfg (with [pylint] section) exists:
lint_candidates.append({ label: "pylint", cmd: "pylint " + joined(.py files),
fix_cmd: null, binary: "pylint", install_cmd: "pip install pylint", exts: [".py"] })
# Ruby
If .rubocop.yml or .rubocop exists:
lint_candidates.append({ label: "rubocop", cmd: "rubocop " + joined(.rb files),
fix_cmd: "rubocop -A " + same, binary: "rubocop",
install_cmd: "gem install rubocop", exts: [".rb"] })
# Shell
If .shellcheckrc exists OR any .sh file in file_list:
lint_candidates.append({ label: "shellcheck", cmd: "shellcheck " + joined(.sh files),
fix_cmd: null, binary: "shellcheck",
install_cmd: "brew install shellcheck # or apt install shellcheck", exts: [".sh"] })
# Filter: only tools whose exts overlap file_list
applicable = [c for c in lint_candidates if any file in file_list has extension in c.exts]
# For each applicable tool: check if binary/node_modules is available
For each tool in applicable:
If tool.binary:
tool.installed = (which tool.binary) exits 0
Else (npm-based):
tool.installed = node_modules/.bin directory exists at worktree root
Run installed tools automatically; prompt only for missing ones:
ready = [t for t in applicable if t.installed]
needs_dl = [t for t in applicable if NOT t.installed]
For each tool in ready (run silently, no prompts):
Print: " ▸ [label] running..."
Run tool.cmd via Bash (cwd=worktree), capture stdout+stderr, note exit code
Print:
── [label] ──────────────────────────────────────────
[raw output, max 60 lines; "… [N] more lines" if longer]
Exit [code] — [N issue(s) | clean]
For each tool in needs_dl (prompt before installing):
Use AskUserQuestion:
question = "[label] config found but binary not installed. Install and run?"
options = ["Yes — run: " + tool.install_cmd, "No — skip"]
If yes:
Run tool.install_cmd via Bash
Run tool.cmd, show output (same format as above)
If tool.fix_cmd AND exit_code != 0: prompt auto-fix (same as above)
If applicable is empty: skip silently, continue to Step 2.
Step 2: Impact Discovery
For files with exported functions/classes: grep codebase for callers.
impact_files = {}
For each file in file_list:
exports = grep for export/module.exports patterns in file
If exports found:
callers = grep -r across worktree for each exported name
impact_files[file] = unique caller files (excluding self)
This feeds Q11 (backward compatibility) with real caller evidence.
Reviewer Routing
Determine ALL applicable reviewers for each file. Every file gets code-reviewer as baseline,
plus any specialized reviewers that match extension/content patterns.
resolve_reviewers(file):
If reviewer_agent param is set: return [reviewer_agent]
reviewers = ["code-reviewer"] # baseline for all files
ext = file extension (lowercase)
If ext == ".gs":
reviewers.append("gas-code-review")
If ext == ".html":
content = Read first 200 lines of file
If content matches /CardService|newCardBuilder|newCardSection/:
reviewers.append("gas-gmail-cards")
If content matches /HtmlService|google\.script\.run|createGasServer/:
reviewers.append("gas-ui-review")
return reviewers
Step 3: Initial Review (parallel per-file)
MAX_CONCURRENT = 12
# INTENTIONAL: recheck_model defaults to null (sonnet via frontmatter inheritance).
# recheck_prompt was deliberately removed in PR #145. Each recheck round runs the
# full Q1-Q37 framework via reviewer_prompt() — not a verification-only checklist.
# Rationale: fixes can introduce NEW bugs not in prior findings; a verification-only
# checklist misses them. Do NOT restore recheck_prompt or change default to "haiku".
recheck_model = params.recheck_model ?? null # null = inherit frontmatter (sonnet)
reviewer_prompt = (file) => `
target_files="${file}"
task_name="${task_name}"
worktree="${worktree}"
mode=evaluate
${plan_summary ? 'Plan context:\n' + plan_summary : ''}
${per_file_diffs[file] ? '**Change context:**\n```diff\n' + per_file_diffs[file] + '\n```' : ''}
${impact_files[file]?.length > 0 ? '**Impact context** (callers for Q11):\n' + impact_files[file].map(f => '- ' + f).join('\n') : ''}
`
Producer-Consumer Dispatch
Use run_in_background: true agents as the concurrency mechanism. This keeps all
MAX_CONCURRENT slots filled continuously — when one agent completes, the next file
starts immediately without waiting for the entire wave.
# Build queue: one entry per (file, reviewer) pair
queue = []
for file in file_list:
reviewers = resolve_reviewers(file)
for reviewer in reviewers:
queue.append({ file, reviewer })
active = {} # name → {file, reviewer} mapping for in-flight agents
results = {} # file → [parsed findings + LOOP_DIRECTIVE per reviewer]
completed = 0
total = len(queue)
# Fill initial slots
While queue non-empty AND len(active) < MAX_CONCURRENT:
entry = queue.shift()
name = sanitize(entry.file) + "--" + entry.reviewer # unique per file+reviewer
Agent(
subagent_type = entry.reviewer,
name = name,
run_in_background = true,
prompt = reviewer_prompt(entry.file)
)
active[name] = entry
Print: " ▸ dispatched: [entry.file] → [entry.reviewer]"
# Process completions as they arrive
# Background agents notify automatically when done — do NOT poll.
# When notified of completion:
For each completion notification:
entry = active[completed_name] # {file, reviewer}
Parse agent output → findings + LOOP_DIRECTIVE
# Merge findings into per-file results (multiple reviewers contribute to same file)
If results[entry.file] not exists: results[entry.file] = { findings: [], loop_directive: "COMPLETE" }
# Tag findings with source reviewer for dedup
for f in parsed_findings: f.source_reviewer = entry.reviewer
results[entry.file].findings.push(...parsed_findings)
If loop_directive == "APPLY_AND_RECHECK": results[entry.file].loop_directive = "APPLY_AND_RECHECK"
# Deduplicate: when multiple reviewers flag same Q-ID + same line on same file,
# keep the specialized reviewer (gas-code-review > code-reviewer, gas-ui-review > code-reviewer)
seen = {} # key: "Q{N}:{line}" → finding
deduped = []
REVIEWER_PRIORITY = { "gas-code-review": 2, "gas-ui-review": 2, "gas-gmail-cards": 2, "code-reviewer": 1 }
for f in results[entry.file].findings:
key = f.q_number + ":" + (f.line or "0")
if key in seen:
if REVIEWER_PRIORITY.get(f.source_reviewer, 0) > REVIEWER_PRIORITY.get(seen[key].source_reviewer, 0):
deduped.remove(seen[key])
deduped.append(f)
seen[key] = f
else:
seen[key] = f
deduped.append(f)
results[entry.file].findings = deduped
Remove from active
completed += 1
# Print progress inline
status_icon = loop_directive == "COMPLETE" ? "●" : "◐"
Print: " [status_icon] [entry.file] → [entry.reviewer] — [status] ([Nc]C [Na]A) [elapsed]s [{completed}/{total}]"
# Refill: dispatch next from queue if slots available
If queue non-empty AND len(active) < MAX_CONCURRENT:
next_entry = queue.shift()
name = sanitize(next_entry.file) + "--" + next_entry.reviewer
Agent(
subagent_type = next_entry.reviewer,
name = name,
run_in_background = true,
prompt = reviewer_prompt(next_entry.file)
)
active[name] = next_entry
Print: " ▸ dispatched: [next_entry.file] → [next_entry.reviewer]"
# All complete when: queue empty AND active empty
Print: " fan-in ── ●[approved] ◐[needs_work] [{total_elapsed}s]"
Fallback: If background dispatch is unavailable, fall back to wave-based dispatch:
chunk files into waves of MAX_CONCURRENT, spawn all in single message, wait for wave, repeat.
Finding Parser
Extract from reviewer output (code-reviewer, gas-code-review, gas-ui-review, or gas-gmail-cards):
For each "**Q[N]: [Title]** | Finding: [severity]" block:
Extract: q_number, title, severity, description
If Fix block present (Before/After code blocks): extract fix_block
Store: { q_number, severity, description, fix_block, file, line }
Extract LOOP_DIRECTIVE: APPLY_AND_RECHECK or COMPLETE
Step 4: Fix Loop
round = 0
DO:
round += 1
recheck_files = files where LOOP_DIRECTIVE == APPLY_AND_RECHECK
IF recheck_files empty: BREAK (all clean)
IF round > max_rounds: BREAK (exhausted)
IF read_only: BREAK (report only mode)
Print fix round banner:
┌──────────────────────────────────────────────────────┐
│ Fix Round [round]/[max_rounds] — [count] files │
└──────────────────────────────────────────────────────┘
For each recheck file:
findings = current findings for this file
fix_tasks = findings with Fix blocks, sorted: Critical first, then Advisory
Print per-file fix progress:
```
┌ [filename]
│ [i]/[N] ┌ [Q-ID] [title] — [first sentence of fix]
│ └ ✓ applied | ⚠ skipped (old_string not found) | ✗ failed
...
```
For each fix_task:
Apply via Edit tool:
old_string = fix_task.fix_block.before (verbatim)
new_string = fix_task.fix_block.after (verbatim)
If Edit succeeds: record as applied, print ✓
If Edit fails (old_string not found): record as failed, print ⚠
Track: files_changed += file (if any fix applied)
Print fix summary:
──────────────────────────────────────────────────────
[applied] applied [skipped] skipped [failed] failed
Re-dispatching [N] files for recheck...
prev_findings_count = sum of all findings across recheck_files
MANDATORY RECHECK — do NOT skip this step.
Fixes are unverified until rechecked. A fix can introduce new issues or fail to
resolve the original finding. You MUST re-dispatch reviewers for every file that
had fixes applied, even if all edits succeeded. The loop terminates ONLY when
reviewers return LOOP_DIRECTIVE == COMPLETE on a recheck pass — never after
a fix-only pass. Skipping recheck is the single most common failure mode of this agent.
Re-dispatch ALL applicable reviewers for recheck_files using the same
producer-consumer pattern, resolve_reviewers(), and reviewer_prompt() as Step 3.
# INTENTIONAL: rechecks use reviewer_prompt (full Q1-Q37), NOT a verification checklist.
# Each round must discover NEW bugs introduced by fixes, not just verify prior findings.
# recheck_model is null by default (sonnet). Set explicitly only as a conscious tradeoff.
For each recheck (file, reviewer) pair:
Agent(
subagent_type = reviewer,
model = recheck_model, # null by default (inherits sonnet frontmatter)
name = sanitize(file) + "--" + reviewer,
run_in_background = true,
prompt = reviewer_prompt(file) # full Q1-Q37, same as initial review
)
Wait for ALL reviewer agents to complete before proceeding.
Update findings and LOOP_DIRECTIVE for each file from reviewer output.
current_findings_count = sum of all findings across recheck_files
Print round summary:
──────────────────────────────────────────────────────
Round [round]/[max_rounds] [━×N][╌×M] [fixes] fixes [[elapsed]s]
Delta ◐[prev_findings_count] → ◐[current_findings_count] ([↓N] | [↑N] | [→0])
Gates [❌N critical | ✅] [⚠️N advisory | ✅]
──────────────────────────────────────────────────────
WHILE recheck_files non-empty AND round <= max_rounds
Invariant: If any fixes were applied in the final round but no recheck was performed,
the loop has a bug. Every round that applies fixes MUST end with a recheck pass before
the WHILE condition is evaluated. The round count in convergence output must reflect
recheck passes, not fix-only passes.
When fix loop exits (all COMPLETE or max rounds), print convergence:
╔══════════════════════════════════════════════╗
║ 🏁 CONVERGED ║
╚══════════════════════════════════════════════╝
Rounds [round]/[max_rounds]
Duration [total_elapsed]s
Fixes [applied] applied, [skipped] skipped
Step 5: Git Operations
Derive final_status from fix loop results:
If ALL files have loop_directive == "COMPLETE" AND no Critical findings remain:
final_status = "APPROVED"
Else if max_rounds exhausted AND no Critical findings remain (only Advisory):
final_status = "APPROVED_WITH_NOTES"
Else (any Critical findings remain, or max_rounds exhausted with Critical findings):
final_status = "NEEDS_REVISION"
Conditions: files_changed non-empty AND final_status is APPROVED or APPROVED_WITH_NOTES.
Skip entirely if NEEDS_REVISION or no changes or commit_mode == "none".
Step 5a: Branch Check (pr mode only)
current_branch=$(git rev-parse --abbrev-ref HEAD)
default_branch=$(git remote show origin 2>/dev/null | grep 'HEAD branch' | awk '{print $NF}')
default_branch=${default_branch:-main}
If on default branch, create temp branch: review-fix/YYYYMMDD-HHMMSS
Step 5b: Stage and Commit
Pre-flight index check: before staging reviewed files, inspect the current index for entries NOT in the agent's reviewed file list:
git diff --cached --name-status
If any R (rename), D (delete), or A (add) entries exist that are not in files_changed:
- Collect them as
pre_staged_entries
- Include them in the commit (do not unstage)
- Add a breadcrumb to the commit message:
Pre-staged entries from caller included: <list of paths>
This preserves git mv / git rm operations staged by the caller before invoking review-fix, which would otherwise be silently dropped by per-file staging.
commit_mode == "commit": stage all target_files with changes (implementation + fixes) + include pre_staged_entries
commit_mode == "pr": stage only files_changed (fix corrections only) + include pre_staged_entries
git commit -m "review-fix: <task_name>: apply corrections ([N] critical, [M] advisory)
[If pre_staged_entries non-empty]: Pre-staged entries from caller: <list>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
If commit_mode == "commit" → output <!-- COMMITTED -->, stop.
Step 5c: Pre-flight (pr mode only)
git remote get-url origin
gh auth status
If either fails → <!-- COMMITTED --> (graceful fallback), stop.
Step 5d: Push + PR + Merge (pr mode only)
git push -u origin HEAD
pr_url=$(gh pr create --base "$default_branch" --title "review-fix: corrections" --body "...")
gh pr merge "$pr_url" --squash --delete-branch
git checkout "$default_branch" && git pull --ff-only
Print git operation badges:
━━━ GIT ━━━━━━━━━━━━━━━━━━━━━━━━━━━
✓ Staged [N] files
✓ Committed: [short message]
✓ Pushed to origin/[branch] (pr mode only)
✓ PR created: [url] (pr mode only)
✓ Squash merged to [default_branch] (pr mode only)
✓ Branch deleted (pr mode only)
Output <!-- PR_MERGED --> on success (pr mode) or <!-- COMMITTED --> (commit mode).
Step 6: Report
Compute health_bar from overall status:
- APPROVED:
██████ ██████ ██████ ██████ ██████ ██████
- APPROVED_WITH_NOTES:
██████ ██████ ██████ ██████ ░░░░░░ ░░░░░░
- NEEDS_REVISION:
░░░░░░ ░░░░░░ ░░░░░░ ░░░░░░ ░░░░░░ ░░░░░░
╔══════════════════════════════════════════════════════╗
║ ║
║ [health_bar] ║
║ ║
║ review-fix Scorecard ║
║ ║
║ Rating: [🟢 APPROVED | 🟡 WITH_NOTES | 🔴 NEEDS_REVISION]
║ [N] files [clean | with notes | need revision]║
║ ║
╚══════════════════════════════════════════════════════╝
Per-File Status
─────────────────────────────────────
[✅|⚠️|❌] [filename] [status] [(fixed RN) if fixed]
Summary
─────────────────────────────────────
Files [N] reviewed
Rounds [R]
Fixes [F] applied, [S] skipped
Duration [T]s
Review History ← omit if only 1 round
─────────────────────────────────────────────
Round │ Files │ Fixes │ Critical │ Advisory │ Time
──────┼───────┼───────┼──────────┼──────────┼──────
[per round row]
──────┴───────┴───────┴──────────┴──────────┴──────
Total: [R] rounds [F] fixes [T]s
Step 7: Self-Reflection (after report)
After printing the report, reflect on the review process itself:
### Prompt Improvement Signals
- Were any questions consistently None across all files? (may indicate low-value triggers)
- Did any Advisory findings lack Fix blocks? (question may need clearer fix guidance)
- Were fixes rejected by Edit tool? (Fix block format may not match actual code)
- Did any file require all 5 rounds without converging? (question criteria may be ambiguous)
- Were the same Q-IDs flagged across multiple files? (may indicate systemic codebase issue, not per-file)
Print 0-3 recommendations if signals fire. Otherwise: "No prompt improvements identified."
This creates a learning loop — each review run surfaces whether the Q1-Q37 framework or the orchestration should evolve.
Cleanup: remove any temp files.