/issue-pr-review [PR_NUMBER]
Review a PR end-to-end — analyze, test, fix, check CI, repeat until clean.
Invocation
| Invocation | Mode | What happens |
|---|
/issue-pr-review <N> | interactive | Review, fix, repeat until clean; report (no auto-merge) |
/issue-pr-review <N> --auto | auto-pilot | Review, fix, and auto-merge when clean |
/issue-pr-review <N> --auto --no-merge | auto-pilot | Review and fix, but skip auto-merge |
/issue-pr-review | detect | Auto-detect PR for current branch |
/issue-pr-review --review-only | read-only | Review and report, never fix or merge |
--auto is set by /auto-pilot; in auto mode export IDD_AUTO_MODE=1 so the pre-commit scan logs warnings instead of prompting (references/docs/pre-commit-security.md). --no-merge suppresses auto-merge even under --auto.
Prerequisites
Confirm before any operation: a git repository (git rev-parse --git-dir), gh authenticated (gh auth status), and the Bundled dependency precheck.
Bundled dependency precheck
Verify every path below exists relative to the skill's directory (the dirname of this SKILL.md); this list is the authoritative guard. If any is missing, stop immediately, print the error, and never continue with an inline or guessed reviewer/fixer prompt:
references/agents/code-reviewer.md
references/agents/ui-reviewer.md
references/agents/fixer.md
references/ui-review-mechanics.md
references/prepass-tests-ci-mechanics.md
references/verification-checks.md
references/review-loop-mechanics.md
references/report-templates.md
references/run-stats.md
references/error-messages.md
references/docs/pre-commit-security.md
references/docs/sync-conventions.md
references/docs/idd-methodology.md
references/docs/config-schema.md
references/docs/naming-conventions.md
references/docs/platform-github.md
references/docs/agent-model-effort.md
references/docs/agent-overrides.md
references/docs/terminal-style.md
references/docs/ui-review.md
references/docs/post-merge-cleanup.md
references/scripts/gi-config.py
references/scripts/gi-secscan.py
references/scripts/gi-ci-wait.py
references/scripts/gi-gh.py
references/scripts/gi-issue.py
references/scripts/gi-receipt.py
references/scripts/gi-recipe.py
references/scripts/gi-postmerge.py
✗ Missing bundled dependency: {missing_file}
To fix: asm install https://github.com/luongnv89/idd --skill issue-pr-review
(or reinstall the full distribution)
Plugin: claude plugin marketplace add luongnv89/idd
claude plugin install idd@idd
(or: claude plugin update idd@idd)
Then restart the agent session and re-run /issue-pr-review.
Repo Sync Before Edits (mandatory)
Before any fix, sync with the stash-first pattern (references/docs/sync-conventions.md): git stash push -u if the tree is dirty, git fetch origin, git pull --rebase origin "$branch", git stash pop. On pop failure, stop and surface git stash list / git stash show -p stash@{0}. A missing origin or a conflicting rebase stops and asks (interactive), or aborts with a clear error (auto).
Configuration
Load config once at skill start; never re-read it. Run python3 references/scripts/gi-config.py — Working directory: the repo root; Script path: resolved against this SKILL.md's directory (why: references/review-loop-mechanics.md).
- Exit 0 — use
config.
- Exit 3 — print
✗ Invalid config: .idd.yml with the offending key and reason from stderr, and stop.
- Script file absent — a broken install and not a degrade: stop and print the
✗ Missing bundled dependency block.
- Anything else (no
python3, non-zero exit, unparsable stdout) — print ⚠ gi-config unavailable — reading .idd.yml by hand and read it yourself instead of the script (else legacy .gitissue.yml, printing ⚠ legacy .gitissue.yml found — rename to .idd.yml).
Capture the run clock here: chain that same python3 invocation as python3 …; ec=$?; date +%s >&2; exit "$ec"; the stderr epoch is run_started_epoch, from which the Run Stats Footer (references/run-stats.md) measures elapsed.
Inline defaults: review.max_cycles: 3, review.soft_pass: true, review.auto_merge: false (auto mode overrides to true). Every key and its gate: references/review-loop-mechanics.md (Config keys and what they gate).
Pipeline Overview
Expected output — example of a clean run, one line per step:
◆ PR Review Pipeline
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
[1/7] PR Info ✓ PR #87: fix(auth): resolve redirect (#42), depth: full, qa: absent
[2/7] Pre-pass ✓ lint clean, format clean, 17 tests passed
[3/7] Review ● analyzing changes...
[4/7] Test ✓ 17 tests passed, build ok
[5/7] CI Status ✓ all checks passed
[6/7] Fix ○ no fixable issues
[7/7] Report ✓ PR is clean — ready to merge
Step 2 runs once; Steps 3-6 repeat up to review.max_cycles (default 3); Step 7 runs once. Tokens are saved by the zero-token pre-pass, reviewer/fixer reuse, and fix/note filtering.
Step completion reports
Each step closes with √/× per check plus a Result: PASS | PARTIAL | FAIL line; a step is not complete until its Result: line prints. Check names and semantics: references/report-templates.md (Step Completion Reports) — read it now.
Step 1 — Get PR Info [1/7]
- If no PR number is given, detect it from the current branch:
gh pr view --json number,title,body,baseRefName,headRefName,state,url,statusCheckRollup. If the branch has no PR, print ✗ No PR found for branch {branch_name} (references/error-messages.md) and stop.
- Fetch the details:
gh pr view {N} --json number,title,body,baseRefName,headRefName,headRefOid,state,url,labels,reviews,statusCheckRollup,files
- Extract base and head branches,
headRefOid, linked issue numbers (from Closes #N, or a first-line Refs #N — Intentional reference in references/verification-checks.md), CI status, and changed files.
- If the PR is closed, print
⚠ PR #{N} is already closed and stop. If it is already merged, print ○ PR #{N} is already merged, skip the review, and run the Post-merge cleanup (Step 7) under the Already merged rules in references/report-templates.md; then stop.
- Run
gh pr checkout {N}, so pre-pass commits and the fixer operate on the PR head.
- Bind the head-ref name — never paste the literal name into a command:
branch_name="$(gh pr view {N} --json headRefName --jq .headRefName)", then use "$branch_name" in every shell command; display templates keep the plain name. Why: references/review-loop-mechanics.md (Binding the head-ref name).
Depth gate (select the review profile)
Set profile = light | full. Signals and what <!-- a:rv-depth-gate-refresh -->
light changes: references/docs/agent-model-effort.md (Complexity → pipeline profile) and references/review-loop-mechanics.md (Depth gate) — read and apply both.
- When the PR body links an issue, refresh it (
references/scripts/gi-gh.py ships as its helper):
python3 references/scripts/gi-issue.py {linked_issue} --fields number,title,body,labels --refresh, then read .issue.
- On exit 3, stop. On no
python3, exit 2, or exit 4, run gh issue view {linked_issue} --json number,title,body,labels instead.
- Retain the successful record as
linked_issue_snapshot.
If neither path yields a usable record, apply the empty-record fail-safe in references/review-loop-mechanics.md (Depth gate) — never review a linked issue on an empty snapshot.
- The refresh runs even when
review.adaptive_depth is false; in that case set profile = full after the refresh and skip step 5.
- Weigh diff size, the snapshot's
## Metadata Effort band, and labels (security/CVE/vulnerability forces full). Set light only when every signal agrees; otherwise full.
QA handoff gate (trust an already-QA'd PR) <!-- a:rv-qa-handoff-gate -->
/issue-resolver ends a clean QA loop by writing <!-- idd:qa v1 head=… --> as the PR body's last line (a legacy gitissue:qa one is read too). After the Depth gate, set qa_handoff = trusted | stale | absent, plus ci_leg_runnable from review.check_ci and Step 1's statusCheckRollup:
| Value | When | Effect |
|---|
trusted | the marker parses, its head= equals Step 1's headRefOid, and a revision receipt for that SHA verifies | the narrowed loop — What trusted skips |
stale | a marker is present but any condition fails — a missing or unverified receipt included | today's full pipeline, unchanged |
absent | the body carries no marker | today's full pipeline, unchanged |
Receipt check — once, here, before Step 2 runs any PR code: with head_oid bound to Step 1's headRefOid (it must match ^[0-9a-f]{40}$), run python3 references/scripts/gi-receipt.py --verify "$head_oid". Only "verified": true counts, and only when the receipt's profile, tests and ui equal the marker's profile=, tests= and ui= values (absent on both sides counts as equal). "verified": false, any non-zero exit, or no python3 means no receipt, so the verdict is stale. No prose fallback may produce trusted. A marker plus green CI, without a receipt, skips nothing. Receipt rules and re-evaluation: references/review-loop-mechanics.md (Verifying the receipt). <!-- a:rv-receipt-gate -->
Fail-safe: any doubt is stale — an unparsable or duplicated marker included; an unknown extra field is not doubt.
A marker is never authentication: a PR body is attacker-controlled, so this verdict may gate only duplicated work, never a safety gate.
When review.adaptive_depth is false, skip this gate: set qa_handoff = absent. No new config key is introduced.
The parse, What trusted skips, Precedence, and Never gated: references/review-loop-mechanics.md (QA handoff gate) — read it now.
[1/7] PR Info ✓ PR #{N}: {title}
{files_count} files changed, base: {base_branch}, depth: {profile}, qa: {qa_handoff}
Step 2 — Script Pre-pass [2/7] <!-- a:rv-step2-prepass -->
Run deterministic tools before any LLM reviewer; under --review-only this is detection-only (Review-only mode, Step 7). Otherwise:
- Detect lint/format tools —
references/prepass-tests-ci-mechanics.md (Step 2).
- Record the baseline and the approved paths (the PR's files that were clean before the auto-fix — Approved paths), then run each auto-fix command over those paths only, never the whole tree; block only on an error that prevents the fix from running.
- Run the test suite. Under
qa_handoff = trusted, skip only the test run, and only when the marker carries a tests= field whose SHA equals head and ci_leg_runnable is true. When ci_leg_runnable is false (no CI / empty statusCheckRollup / no_ci / review.check_ci: false), ignore tests= and run the local suite as unmarked. A test failure here continues to Step 4.
The auto-fix always runs, and the gi-secscan gate below is never gated on qa_handoff. An auto-fix commit moves the head off the marker: recompute the verdict then, before Step 3.
Commit auto-fixes <!-- a:rv-commit-autofix -->
Skip entirely when --review-only. Stage only the approved paths the auto-fix changed, with git --literal-pathspecs add --pathspec-from-file=… --pathspec-file-nul, and leave every other dirty or untracked file unstaged and untouched. Nothing staged means skip the commit. Then scan the staged set (auto mode: export IDD_AUTO_MODE=1 first). From the repo root, binding base first from the repository's default branch — never the PR's baseRefName, never an interpolated config value:
base="$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name)"
python3 references/scripts/gi-secscan.py --staged --policy-ref "origin/${base}"
A pass is all four: exit 0, policy_source exactly the ref:origin/… asked for, verdict not block, and not (scanned 0 with skipped above 0). Exit 1 is the block verdict — stop, unstage, do not commit or push, report blocking[]. Exit 3 stops. No python3, exit 2, or exit 4 degrades to the Primary Pattern in references/docs/pre-commit-security.md. Never read a non-zero exit as a pass. Full exit contract, the trust boundary and the staging commands: references/prepass-tests-ci-mechanics.md (Commit auto-fixes) — read it now. After a pass, commit the stage list only (git commit --only --pathspec-from-file=…), then git push origin "$branch_name". Never git add -A.
[2/7] Pre-pass ✓ lint clean, format clean, {N} tests passed
Auto-fixed: {files_fixed} files (lint/format)
Step 3 — Analyze & Review [3/7] <!-- a:rv-step3-analyze -->
Reviewer. Read references/agents/code-reviewer.md and references/agents/fixer.md; both spawn with the default general-purpose agent (do NOT set subagent_type), applying references/docs/agent-overrides.md (agents.model.<role> / agents.effort.<role>) for the code-reviewer, fixer and ui-reviewer roles, with review.confidence_threshold (default 80). Cycles 2+ re-message the same reviewer via SendMessage; once the fixer reports zero fixable issues, one fresh confirmation reviewer does the final check (new issues go back to the fixer and count as a cycle). Under qa_handoff = trusted the cycle-1 reviewer is collapsed into that fresh confirmation pass, so the PR still receives exactly one independent, full-strength review, and the cap drops to min(1, configured_cap). The collapse saves no reviewer spawn — the confirmation pass is itself fix-conditional. Spawn calls: references/review-loop-mechanics.md (Why reuse the reviewer).
UI review is auto-detected per PR — no config flag. The code leg runs on any host, including a no-GUI host; the optional browser leg skips with a warning. When ui: detected, apply references/docs/ui-review.md and references/ui-review-mechanics.md. Under qa_handoff = trusted the code UI review is skipped only when the marker's ui= leg ran (ui=code… or ui=code+browser…) and carries an @<sha40> equal to head — never on ui=none, never on an unsuffixed ui=, never for the browser leg.
Then run per-criterion acceptance-criteria verification on linked_issue_snapshot directly; do not call gi-issue.py or gh again. A PR with no linked issue is a different state, not a fail-safe case: acceptance_criteria reports ○ pass — none defined; manual review recommended. <!-- a:rv-step3-ac-snapshot -->
Dimensions: correctness, acceptance_criteria, traceability, maintainability, safety — each pass, partial, or fail, grouped under a Spec axis and a Standards axis; UI findings fold into maintainability. Mapping: references/verification-checks.md — read it now.
[3/7] Review ✓ spec[ac:pass correctness:pass safety:pass]
standards[trace:pass maint:partial]
{fixable_count} fixable, {note_count} noted
Verification gates <!-- a:rv-verify-gates -->
This skill produces acceptance_criteria and traceability itself, per references/verification-checks.md. Gating rules: <!-- a:rv-traceability-outcomes -->
review.require_acceptance_criteria_check / review.require_traceability_check (default true): when false, that dimension reports pass — verification disabled and never blocks.
- Any
acceptance_criteria: fail → Step 6 fixable (category: acceptance_criteria); hard-blocks soft-pass.
Closes #{linked_issue} absent (unless refactor/chore-exempt or a valid intentional Refs) → traceability: fail, Step 6 fixable "Add Closes #{linked_issue} to the PR body."; hard-blocks soft-pass, even with green tests. A failing Refs #{linked_issue} line 1 hard-blocks as action: note, never auto-fixed.
- Other traceability gaps (missing commit ref; a human-authored PR with the Decision Record absent) report
traceability: partial and do not block.
Step 4 — Run Tests & Build [4/7] <!-- a:rv-step4-tests -->
review.run_tests is false: skip; report ○ tests skipped (review.run_tests: false).
- Under
qa_handoff = trusted, skip this step and report ○ tests skipped (qa handoff @ {commit_sha_short}) — trusted holds only against the live head, so the soft-pass conjunction therefore treats the test leg as satisfied — only when the marker carries a tests= field whose SHA equals head and ci_leg_runnable is true; with no tests= field, or a SHA that differs, run the step in full. When ci_leg_runnable is false (no CI / empty statusCheckRollup / no_ci / review.check_ci: false), ignore tests= and run the local suite as unmarked.
- Otherwise: run the build, then every test type present, with a
review.test_timeout-second timeout (default 300) — references/prepass-tests-ci-mechanics.md (Step 4).
Verification recipe: when the base branch commits .idd-recipe.json (or legacy .gitissue-recipe.json), run it in this step with references/scripts/gi-recipe.py, even when the suite is skipped (qa_handoff = trusted included). Auto mode runs it only when the recipe opts in review. A result: fail is fixable and blocks soft-pass (references/ui-review-mechanics.md, Verification recipe).
A skip satisfies the soft-pass test leg but evaluated neither check: it reports × Suite passed / × Build clean with Result: PARTIAL, never a silent √.
[4/7] Test ✓ build ok, {N} tests passed
[4/7] Test ✗ {N} tests failed — {brief failure summary}
Step 5 — Check CI Status [5/7] <!-- a:rv-step5-ci -->
When review.check_ci is false, report ○ CI skipped (review.check_ci: false); the leg is satisfied. Otherwise:
- Run
python3 references/scripts/gi-ci-wait.py {N} --interval {review.ci_poll_interval} --timeout {review.ci_timeout} and read verdict (pass / fail / pending / none; none is clean only when none_confirmed is true).
- On exit 3, stop. On exit 4 or no
python3, use the merge-safe manual fallback — never a filtered gh pr checks list.
- On
fail, run gh run view {run_id} --log-failed.
- Record
ci_sha = the headRefOid the wait ran against; report ci_status as passed@ or failed@ plus that 40-character SHA (no_ci and a degraded wait stay bare).
Settle window, fallback loop, binding: references/prepass-tests-ci-mechanics.md (Step 5) — read it now.
review.ignore_ci_billing_failures: true makes a terminal fail non-blocking at this gate only: CI is still polled, Step 6 raises no CI fixable, and the soft-pass conjunction treats the CI leg as satisfied. ci_status stays failed@{sha40}, never passed@; the result is PARTIAL; the merge is still refused, and /auto-pilot refuses the same red checks. Invariants: references/prepass-tests-ci-mechanics.md (Ignoring terminal CI failures).
[5/7] CI Status ✓ all checks passed
[5/7] CI Status ✗ {N} checks failed — {check_name}: {bucket}/{state}
[5/7] CI Status ⚠ {N} checks failed — not blocking (review.ignore_ci_billing_failures: true)
[5/7] CI Status ⚠ checks still running after {timeout}s
[5/7] CI Status ○ no CI checks configured
Pending CI is not clean and is never merged. Interactive: ask whether to wait another review.ci_timeout; on no, report without merging. Auto: re-run the wait once; if still pending, stop with the pending checks under Remaining.
Step 6 — Fix Issues [6/7] <!-- a:rv-step6-fix -->
Fix only issues with action: "fix" — action: "note" issues are reported, never fixed. Sources: each fail dimension or UI fix finding, Step 4 test failures, Step 5 CI failures. With none, print ○ no fixable issues (noted: {note_count}) and exit the fix loop; soft-pass is evaluated next, never implied.
The Closes #{linked_issue} fix is a read-modify-write PR-body edit (driver rule 2 in references/docs/platform-github.md): gh pr view {N} --json body, prepend Closes #{linked_issue} as the first line (references/docs/naming-conventions.md), gh pr edit {N} --body "{merged_body}", then re-read and confirm ## Decision Record, the AC Verification table, and any trailing <!-- idd:qa v1 … --> marker are still present (a legacy gitissue:qa one counts). Never replace the body, and never prepend when line 1 is Refs #{linked_issue}. <!-- a:rv-closes-body-edit -->
Delegate code fixes to the fixer subagent (references/agents/fixer.md), reused across cycles — never edit code in the main context. It scans the staged set (references/scripts/gi-secscan.py, Step 2's --policy-ref) and commits; you push with git push origin "$branch_name". Spawn: references/review-loop-mechanics.md.
[6/7] Fix ✓ fixed {N} issues (noted: {note_count} — not fixed)
Review Loop
After Step 6, return to Step 3. Mechanics: references/review-loop-mechanics.md — read it now.
- Max cycles:
review.max_cycles (default 3). light and qa_handoff = trusted each cap it at min(1, configured_cap). The light profile also skips the optional browser UI review; trusted never does. Neither relaxes the hard-block conditions.
- Hard-block conditions (#36):
traceability: fail (a missing Closes #N) and any acceptance_criteria: fail block even with green tests.
- Re-evaluate
qa_handoff after any push this skill makes — Step 2's auto-fix commit as much as every fixer push: re-read headRefOid and recompute. The loop re-enters at Step 3.
- Soft pass (
review.soft_pass: true, default): stop when zero action: "fix" issues remain, tests pass (or are skipped by config), CI passes (or is absent, disabled, or held non-blocking by review.ignore_ci_billing_failures: true — loop exit only, never clean at the auto-merge gate), and traceability is not fail.
- Strict pass (
review.soft_pass: false): strict mode keeps the same test/CI legs (review.ignore_ci_billing_failures included, still never clean at the auto-merge gate) and adds zero remaining action: "note" findings and pass on every enabled dimension; any partial dimension is a strict blocker — stop, report it under Remaining, never merge.
- Stagnation: the same findings (dimension + file + description) in 2 consecutive cycles → stop and report.
Step 7 — Summary Report [7/7]
Print the summary from references/report-templates.md and apply its Review contract to every terminal outcome, including a stop before Step 7: first row Result: (PASS, MERGED, PARTIAL, WARN, or BLOCKED), then Evidence, Uncertainty, and Decision rows — read it now.
Auto-merge is the one destructive action (squash merge + head-branch deletion, irreversible). Every gate must hold: interactive runs never merge; --auto merges only when review.auto_merge is true and the PR is clean (pending CI is never clean; a CI failure held non-blocking by review.ignore_ci_billing_failures is never clean either); --no-merge suppresses the merge, leaving it to auto-pilot. On an unmet gate, report and stop — never delete a branch by hand.
Merge identity (last gate before the merge). <!-- a:rv-merge-identity --> Bind verified_head = Step 5's ci_sha (or, with no ci_sha, the headRefOid the final review cycle read). Re-read headRefOid and baseRefName, then gh api "repos/{owner}/{repo}/compare/${base_ref}...${verified_head}" --jq .behind_by. Merge only when the head still equals verified_head and behind_by is exactly 0 (the live base branch, never baseRefOid); anything else is BLOCKED, never a re-wait. Merge with --match-head-commit "$verified_head". Patch-id equality never replaces fresh integration checks. Commands and reasons: references/report-templates.md (Auto-Merge).
Post-merge cleanup (after every merge). Run python3 references/scripts/gi-postmerge.py --pr {N} to remove the merged branch and its clean worktrees and switch to the updated base (references/docs/post-merge-cleanup.md). A non-zero merge exit on a PR that now reads MERGED is still a merge (add --delete-remote); ok: false never downgrades it. Untracked files are deleted only on an interactive yes to the exact list shown (its digest). Rules: references/report-templates.md (Auto-Merge).
Then the run-stats footer (references/run-stats.md; tokens only where the host reported a count) — the last thing printed at every terminal outcome, including a stop before Step 7.
Review-only mode (--review-only) — authoritative definition.
- Steps 1-5 once, skip Step 6 — never loop, fix, merge, edit, commit, or push (skip Commit auto-fixes).
- Step 2 runs lint/format in check mode (
npx eslint ., npx prettier --check ., ruff check .) — no --fix, --write, or other mutating flags.
Conventions
Output follows references/docs/terminal-style.md; gh calls use --json with explicit fields.
Edge Cases
No PR, CI pending at timeout, a blocker at the cycle cap, or a merge conflict: stop without merging and print the rich block from references/error-messages.md.