Review Multiple PRs (Parallel)

Use this skill when the user provides one or more PR numbers or URLs to review. Unlike /code-review (which processes PRs sequentially), this skill fans out to parallel agents — one per PR — then merges their findings into a unified summary.


Step 0 — Verify gh auth and repo access

  1. Run gh auth status. If unauthenticated, stop and tell the user.
  2. Resolve the repo identity: if PR URLs were provided, extract owner/repo from them. Otherwise run gh repo view --json nameWithOwner -q .nameWithOwner to get the repo from the current directory context. Store this as {owner}/{repo} — it is required for API calls in Step 5.
  3. Run gh repo view {owner}/{repo} to confirm read access. If it fails, stop and tell the user.

Step 1 — Identify and classify the PRs

PR Discovery (when no PRs are specified)

Use gh and the GitHub search API to find open PRs by the user’s teammates that are awaiting review. Run these steps:

  1. Resolve the org: extract the org from the {owner}/{repo} resolved in Step 0 (e.g. ProjectAussie).

  2. Find teammate usernames: ask the user for a list of names or GitHub handles to search for. If they provide display names (e.g. “Claire”, “Tom McT”), resolve them to GitHub logins by searching org members:
    gh api 'orgs/{org}/members' --paginate --jq '.[] | select(.login | test("{name}"; "i")) | .login'
    
  3. Find open PRs by those authors:
    gh search prs --state open --author {login} --json number,title,url,author,repository --limit 20
    

    Run one search per author. Collect all results.

  4. Filter to actionable PRs only — for each PR, fetch its review status and check whether the current user has already reviewed:

    a. Get the current user’s login:

       gh api user --jq .login
    

    b. Get PR metadata:

       gh pr view <number> -R <owner/repo> --json reviewDecision,isDraft,reviewRequests
    

    c. Get the current user’s most recent review on this PR (if any) and the latest commit date:

       gh api repos/{owner}/{repo}/pulls/{number}/reviews --jq '[.[] | select(.user.login == "{current_user}")] | sort_by(.submitted_at) | last | {state: .state, submitted_at: .submitted_at}'
    
       gh api repos/{owner}/{repo}/pulls/{number}/commits --jq 'last | .commit.committer.date'
    

    d. Keep only PRs where all of the following are true:

    e. Skip PRs where the current user is already waiting on the author:

  5. Bucket by requester type (reuse the current user’s login from step 4a — do not fetch it again):**
  6. Present the list to the user grouped by author and bucket, with URLs. If any PRs were skipped because the current user is waiting on the author, list them separately:

    Skipped (waiting on author after your CHANGES_REQUESTED):

    • {owner}/{repo}#{number} — {title} — last reviewed {date}, no new commits since

    Then ask: “Should I review all of these, or select a subset? I can also include the skipped PRs if you want to re-review them.”

  7. Once the user confirms the set, continue with the normal Step 1 classification flow using those PRs.
Relationship Definition Review strategy
Stacked Each PR targets the previous PR’s branch Review in merge order; pass raw diff context forward
Parallel Multiple PRs for the same feature, split by concern Review independently; flag integration risks
Batch / Unrelated Unrelated changes reviewed together (e.g. release batch) Review fully independently

Step 2 — Fan out: one agent per PR

Before fanning out, check if .review-suppressed.md exists in the current working directory. If it does, read it and pass its contents to each agent so they can skip previously suppressed findings.

Resolve $TEMP_DIR here — this is its canonical resolution point for this skill. Use the same commands shown in Step 5’s “Write the draft file” section, then mkdir -p -m 700 "$TEMP_DIR". Step 5 reuses this same directory; it does not re-resolve it.

Resolve a RUN_ID for this invocation, exactly once, and persist it immediately:

RUN_ID="$(date +%s)-$$"
printf '%s' "$RUN_ID" > "$TEMP_DIR/current-run-id"

(the trailing $$ — the shell’s own PID — is required, not decorative: date +%s alone has one-second resolution, and two people starting this skill on the same branch in the same second would otherwise collide). The current-run-id file exists because RUN_ID is otherwise conversation-only state, exactly the kind of thing this whole fix exists to stop relying on — if context gets compacted between Step 2 and a later step, re-deriving RUN_ID from scratch (date +%s again) produces a different value that matches nothing already on disk. Whenever RUN_ID is needed and isn’t already in hand, read it from $TEMP_DIR/current-run-id rather than recomputing it. Never overwrite this file or recompute RUN_ID more than once per invocation.

Every output file this run writes is namespaced agent-output-pr-{number}-{RUN_ID}.json — never the bare agent-output-pr-{number}.json. This namespacing, not deletion, is what keeps stale results from a previous pass (this is a repeat-invocation skill — Step 8 expects re-runs after new commits) from silently passing validation, and it keeps two concurrent runs on the same branch from clobbering each other’s output files. Old files from finished runs are removed by Step 8’s cleanup step, not by deleting on sight here.

Resolve the literal absolute path for $TEMP_DIR (and compute RUN_ID) in your own context before building any agent prompt. A spawned agent has no access to your shell variables — if the prompt text contains $TEMP_DIR or $RUN_ID literally, the agent’s own shell will expand them to empty and either fail to write or write to the wrong place. Every path handed to an agent below must already be the resolved absolute string with RUN_ID substituted in.

Maintain the PR → validated-output-path mapping as a file, not only in conversation — write {TEMP_DIR}/run-manifest-{RUN_ID}.json (a flat {"123": "/abs/path/to/agent-output-pr-123-<RUN_ID>.json", ...}). Every time an agent’s output validates successfully (including after a retry supersedes an original — see “Handling agent failures” below), update it by reading the current contents of the file, merging in the one new entry, and writing the whole object back — never write a fresh object from memory of what you think it already contains; that silently drops every PR recorded in a turn you no longer have in context. The manifest, like the per-PR output files it points at, must survive context compaction — a mapping that exists only in conversation state reintroduces the exact problem this whole fix exists to solve, just one layer up. Step 5 reads this manifest rather than guessing filenames. If the manifest is ever missing when Step 5 needs it (e.g. compaction wiped the conversation and this file was somehow never written), first recover RUN_ID itself from $TEMP_DIR/current-run-id (do not recompute it — see above), then reconstruct the manifest by globbing {TEMP_DIR}/agent-output-pr-*-{RUN_ID}*.json and, for each PR number, preferring a -retry1 match over the base path if both exist — do not re-dispatch agents whose valid output is already sitting on disk. Delete the manifest in Step 8’s cleanup along with everything else.

Spin up one Agent per PR using subagent_type: general-purpose. Name each agent after a unique American outlaw from the 1800s–1900s (e.g. Jesse James, Belle Starr, Black Bart, Dutch Schultz, Pretty Boy Floyd, Billy the Kid, Bonnie Parker). Names must be unique per session — do not reuse a name even if reviewing many PRs.

Each agent receives a self-contained prompt with:

  1. The PR number, repo ({owner}/{repo}), is_draft flag, and author login (all from Step 1’s metadata fetch) — with an instruction to copy author verbatim into its JSON output rather than re-fetching or guessing it.
  2. Instructions to:
  3. For stacked PRs only: fetch the prior layer’s diff yourself — gh pr diff <prior-number> > {TEMP_DIR}/diff-pr-<prior-number>-{RUN_ID}.txt — before dispatching this agent, and pass it the absolute path to that file plus an instruction to read it for context on what the prior layer changed. Do not have the agent embed the diff inside its own JSON output: asking an LLM to hand-escape a multi-thousand-line diff as a JSON string is one of the most likely ways to produce malformed output, and the orchestrator can fetch this diff itself in one command with zero risk of a bad escape.
  4. Draft PR behavior: if is_draft: true, the agent must still review and produce findings, but must set verdict: "COMMENT" unconditionally, and must not post anything to GitHub itself. All posting — draft or not — happens only in Step 6, after human approval of the draft file. The summary must clearly label the PR as a draft.
  5. The review dimensions to evaluate (see Step 3 below)
  6. The finding severity scale (see Step 4 below)
  7. The resolved absolute path {TEMP_DIR}/agent-output-pr-{number}-{RUN_ID}.json (with {TEMP_DIR} and {RUN_ID} substituted for real values — never the literal strings $TEMP_DIR / $RUN_ID) and an instruction to write its full structured JSON there (see Output Contract below).
  8. This verbatim closing instruction: “Once you have written the JSON to that file, your final message must be only the file path you wrote, plus the word DONE — nothing else. Do not repeat the JSON in your final message; the file is the only place it needs to exist.”

Run all agents in parallel (single message, multiple Agent tool calls; this uses the default run_in_background: true) unless the PRs are stacked — in that case, run them sequentially in merge order with run_in_background: false, so each agent’s prior-layer diff file (item 3 above) exists before the next one is dispatched.

Wait for every dispatched agent’s completion notification before evaluating any output file. A background Agent call returning is dispatch, not completion — see the HARD GUARD pattern this repo already uses in /critique. Do not treat “some agents have reported, others haven’t” as license to start Step 5’s file check or to retry an agent that simply hasn’t finished yet; retrying a still-running agent races it onto the same output file and can produce a torn or overwritten write. Track every dispatched agent by name and confirm completion for all of them (or a hung-agent determination per “Handling agent failures” below) before moving on.

Output Contract

Each agent must return a JSON object with exactly these fields:

{
  "pr": 123,
  "title": "...",
  "author": "github-login",
  "verdict": "APPROVE",
  "is_draft": false,
  "ci_status": "passing",
  "ci_failures_introduced_by_pr": false,
  "findings": [
    {
      "severity": "BLOCKER",
      "file": "path/to/file.ts",
      "line": 42,
      "body": "..."
    }
  ],
  "prior_discussions": [
    {
      "author": "<github username of the person who raised the concern>",
      "summary": "brief description of the prior comment or concern",
      "status": "accepted | unresolved | addressed_in_code",
      "original_severity": "BLOCKER | HIGH | MEDIUM | LOW | QUESTION",
      "file": "path/to/file.ts (optional — only if the concern was about a specific file; fold into `summary` if present, since Step 5's Prior Discussions table has no File column and never renders this on its own — same treatment as `line` below)",
      "line": "42 (optional — fold into `summary` instead if omitted; Step 5's Prior Discussions table has no Line column, so this field is never rendered on its own)",
      "reasoning": "why this status was assigned — e.g. 'reviewer replied OK to defer' or 'no response from reviewer after author acknowledged'"
    }
  ],
  "summary": "2–4 sentence summary of what the PR does and overall quality"
}

Valid verdict values: "APPROVE", "REQUEST_CHANGES", "COMMENT".

Valid severity values for findings: "BLOCKER", "HIGH", "MEDIUM", "LOW", "QUESTION". Agents must never assign IRRELEVANT — that label is set exclusively by the human in the draft file.

Markdown formatting in body fields: When a comment body contains multiple points, questions, or items, use proper markdown list syntax — NOT inline numbering like 1) ... 2) .... Use actual newlines with 1. / 2. markers so GitHub renders them as a list. Inline 1) 2) formatting renders as a single unbroken paragraph on GitHub.

Line number constraint: every finding with a line value must reference a line that actually appears in the diff output from gh pr diff. Do not invent or approximate line numbers — a line number not in the diff will cause the GitHub API to reject the comment with a 422 error.

How to verify a line is in the diff: Parse the @@ hunk headers from gh pr diff --color=never. Each header has the form @@ -old_start,old_count +new_start,new_count @@. For each hunk, maintain a running new_line counter initialized to new_start. For each subsequent line in the hunk: if it starts with ` ` (context) or + (added), add (path, new_line) to the valid set and increment new_line; if it starts with - (deleted), skip it without incrementing (it has no RIGHT-side line number); if it starts with \ (the \ No newline at end of file sentinel), skip it without incrementing. Reset the counter when a new @@ header is seen. Binary files produce no hunk lines and therefore an empty valid-line set for that path — any finding referencing a binary file path will be correctly demoted to the review body. A finding is only valid if its (path, line) pair appears in this set. If you cannot confirm a line is in the diff, omit the line field entirely — it will land in the review body instead of as an inline comment.

Handling agent failures

Each agent writes its full structured JSON to {TEMP_DIR}/agent-output-pr-{number}-{RUN_ID}.json and returns only a path-plus-DONE confirmation as its final message (Step 2, items 7–8). The file is the sole durable source of truth — Step 5 reads it, not the conversation. Only evaluate an output file after that specific agent has reported completion (see the wait instruction at the end of Step 2); a missing file at that point, or a file that fails validation below, is a failure and must be handled exactly like a parse failure. Do not rely on catching a result out of conversation context: context gets compacted, and completion notifications for a parallel batch of background agents can arrive out of order.

Validate every agent’s output file before using it — presence of a parseable object alone is not enough:

Detecting a hung original dispatch (parallel, background agents only): record the wall-clock time at dispatch by appending a line to $TEMP_DIR/dispatch-times-{RUN_ID}.txt (echo "{pr-number} $(date +%s)" >> ...) rather than only noting it in conversation — the same compaction risk that motivated current-run-id applies here. On any later turn where a given agent still hasn’t reported completion, run date +%s again and compare against that PR’s recorded dispatch time; once the difference exceeds 600 seconds, treat it as hung and proceed to step 2 below. This clock-based check only applies to agents dispatched with run_in_background: true — it is meaningless for a stacked PR’s sequential (foreground) original dispatch or for any retry (both always use run_in_background: false, item below): a foreground Agent call blocks the current turn until it returns, so there is no “later turn” on which to re-check the clock. For those, a failure to return is a harness-level condition outside this skill’s control, not something this skill’s clock check can detect — if a foreground call never returns, there is no later step to reach. Delete dispatch-times-{RUN_ID}.txt in Step 8’s cleanup along with everything else.

On any validation failure (missing file, empty/truncated content, parse failure, or a check above failing) — including on the hung-original determination above:

  1. Check whether the automatic retry has already been spent for this PR. If {TEMP_DIR}/agent-output-pr-{number}-{RUN_ID}-retry1.json already exists (or an agent is already dispatched to write it), the one automatic retry below has already happened — attempt salvage on that retry file (step 2 below) and, if that fails, go straight to step 4; do not dispatch a second automatic retry (do not re-enter step 3).
  2. Attempt salvage first, but only if a file exists to salvage — skip this step entirely for a missing/never-written file and go straight to the retry below. Strip a leading/trailing code fence ( json ` / ` ) or extract the outermost balanced {...} substring from the file’s contents, then re-validate against the checks above. If salvage succeeds, overwrite the output file with the salvaged, valid JSON before continuing — Step 5 reads the file, not whatever you fixed only in your own context, so a repair that isn’t written back is invisible downstream. A stray fence around otherwise-valid JSON is the most likely deviation — don’t burn a full re-review over a cosmetic wrapper.
  3. If salvage doesn’t produce valid output (or wasn’t applicable), retry that one agent — this is the one automatic retry step 1 checks for. Never fall back to reviewing the PR yourself — that isn’t a substitute for the agent’s structured process and defeats the point of fanning out. Retry with:
  4. If the retry also fails validation, stop — per step 1, there is no second automatic retry — and tell the user which PR’s agent failed twice and exactly why (empty output, parse failure, hang, or the specific failed field).

Do not proceed to Step 5 with a fabricated or self-authored substitute for any PR’s findings.

Because draft-PR agents never post to GitHub themselves (Step 2, item 4 — all posting happens in Step 6 after human approval), retrying a draft PR’s agent carries no risk of duplicate GitHub comments.

Step 3 — Review dimensions (per PR)

Each agent evaluates:

Correctness

Security

Design & Simplicity

Readability & Maintainability

Test Coverage

Prior Discussion & Deferred Decisions

Agents must complete this section before forming a verdict. Omitting it is a review failure — populate the prior_discussions array in the Output Contract even if empty.

Data sources

Use all three data sources together — each one shows different things:

Source What it shows Command
PR conversation comments Top-level discussion gh pr view <number> --comments
Formal review state APPROVED, CHANGES_REQUESTED, DISMISSED per reviewer gh api repos/{owner}/{repo}/pulls/{number}/reviews
Inline review thread comments Line-level reviewer concerns gh api repos/{owner}/{repo}/pulls/{number}/comments

Identifying deferred or prior concerns

Scan reviewer comment threads (not PR description, not commit messages) for phrases that indicate deferral: “we’ll fix this later”, “out of scope for this PR”, “follow-up ticket”, “known issue”, “accepted risk”, or similar.

Do not treat the following as deferral signals:

Assigning severity to prior concerns

Prior reviewer comments do not use this skill’s severity taxonomy. When a prior concern has no explicit severity, assign one based on impact:

Document your severity assignment and reasoning in the reasoning field of the prior_discussions entry.

Status classification rules

Status Criteria
accepted The reviewer who raised the concern (not the author) explicitly accepted the deferral — e.g., replied “OK to defer”, “fine for now”, submitted a new APPROVED review after discussion. Author acknowledgment alone is not sufficient. If the reviewer went silent after the author acknowledged, classify as unresolved — reviewer silence does not equal acceptance.
addressed_in_code The concern was addressed by a code change. You must verify this — cross-reference the PR diff to confirm the fix is actually present. A comment saying “fixed” or “done” without a corresponding code change means the status is unresolved, not addressed_in_code.
unresolved Anything else: no reply, author disputed it without reviewer resolution, reviewer went silent, or the formal review state is still CHANGES_REQUESTED and not DISMISSED.

Formal review state handling

Verdict interaction

What to surface

Populate the prior_discussions array in the Output Contract for every prior concern found. Include a dedicated “Prior Discussions” subsection in the review output listing each item, its status, original severity, and your reasoning.

Performance (only if relevant)

Step 4 — Finding severity scale

Level Meaning
BLOCKER Must be fixed before merge. Correctness bug, security vuln, broken contract, CI failure introduced by this PR.
HIGH Serious design or reliability issue. Should fix; discuss if deferring.
MEDIUM Real improvement, not blocking. Author should address or explicitly accept risk.
LOW / NIT Style, naming, minor cleanup. Don’t block merge over these.
QUESTION Unclear intent — ask before judging.

Do not manufacture findings to look thorough. If the code is good, say so.

Step 5 — Draft review file and get human approval

Reuse the $TEMP_DIR already resolved and created in Step 2 — do not re-derive it here. (If this step is somehow entered without having run Step 2 in this session, resolve it now using the commands shown below, treating that as a recovery path rather than the normal one.)

Before doing anything else in this step: if RUN_ID isn’t already in hand, read it from $TEMP_DIR/current-run-id — do not recompute it (see Step 2). Then read {TEMP_DIR}/run-manifest-{RUN_ID}.json (written and maintained during Step 2) and confirm it has one entry for every PR under review — read each PR’s JSON from the path recorded there, not from a filename guessed here (a retried PR’s validated file lives at a -retry1 path, not the original). If the manifest is missing, reconstruct it per Step 2’s recovery instruction rather than drafting from memory of a conversation result. If any PR still has no entry after that, stop and resolve it in Step 2 first.

Before posting anything to GitHub, write a draft markdown file and present it to the user for review and editing.

Write the draft file

For reference, $TEMP_DIR follows this shape:

TEMP_DIR=/tmp/<repo-name>/<branch-name>

Resolve them with exactly these commands — one code path, correct both inside and outside a worktree:

repo_name=$(basename "$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")")
branch_name=$(git branch --show-current)
# Detached HEAD returns an empty branch name.
[ -n "$branch_name" ] || branch_name="detached-$(git rev-parse --short HEAD)"
TEMP_DIR="/tmp/$repo_name/$branch_name"

Do not use git rev-parse --show-toplevel (returns the worktree path, not the main repo root) and do not use git rev-parse --git-common-dir | xargs dirname — outside a worktree that yields ., so TEMP_DIR silently becomes /tmp/./<branch-name> and the repo-name namespacing is lost with no error.

Create it if absent (mkdir -p -m 700 "$TEMP_DIR"). Branch names contain slashes, so $TEMP_DIR is a nested path — mkdir -p is required, not optional. Write all findings to $TEMP_DIR/review-draft-{timestamp}.md (e.g. $TEMP_DIR/review-draft-2026-04-14T15-44.md). This keeps the file outside the repo and prevents accidental commits. The file has two sections per PR: a Changes Summary and the Proposed Comments.

File format:

# Review Draft — {date}

---

## {owner}/{repo}#{number} — {title}

**Author:** {login} | **CI:** passing / failing / pending{, introduced by this PR: yes/no — only append this clause when CI is failing}

**Review Event:** `APPROVE` / `REQUEST_CHANGES` / `COMMENT` ← _edit this to control the formal GitHub review event submitted for this PR_

### Changes Summary

<3–6 sentence plain-English description of what the PR actually does  not the PR description copy-pasted, but your own read of the diff. What files changed, what behavior changed, what was added or removed.>

### Proposed Comments

#### Prior Discussions

| Author | Summary | Status | Orig. Severity | Reasoning |
|--------|---------|--------|---------------|-----------|
| @reviewer-login | brief description of concern | accepted / unresolved / addressed_in_code | BLOCKER / HIGH / MEDIUM / LOW / QUESTION | why this status was assigned |

#### Inline Comments

| File | Line | Severity | Comment |
|------|------|----------|---------|
| `path/to/file.ts` | 42 | BLOCKER | {your identity} says: ... |

#### Body Comments (no confirmed diff line — will post to the review body, not inline)

| File | Severity | Comment |
|------|----------|---------|
| `path/to/file.ts` | MEDIUM | {your identity} says: ... |

Only present this subsection if at least one finding omitted `line` per the Output Contract's line-number constraint. Do not force a line-less finding into the Inline Comments table with a blank Line cell.

---

## Summary (for human reviewer only — NOT posted to GitHub)

> **Overall:** APPROVE / REQUEST_CHANGES / COMMENT
>
> | PR | Title | Review Event | Blockers | Highs |
> |----|-------|-------------|----------|-------|
> | #123 | ... | APPROVE | 0 | 1 |
>
> **Relationship:** Stacked / Parallel / Batch  ← _omit this line when reviewing a single PR_
>
> **Integration Concerns:** ...  ← _omit this line when reviewing a single PR_

Include every PR in the file, in order. Leave the summary at the bottom. When reviewing a single PR, omit the Relationship and Integration Concerns lines — they are meaningless without multiple PRs.

Present and wait for approval

Tell the user:

“Draft written to {full path to file in $TEMP_DIR}. Open it, make any edits you want — remove findings, soften wording, add context. The Review Event field on each PR controls the formal GitHub review action (APPROVE / REQUEST_CHANGES / COMMENT) — change it if you disagree with my recommendation. To permanently suppress a finding so it is never raised again on this PR, change its severity to IRRELEVANT — I will skip posting it and record it in .review-suppressed.md. Tell me to post when ready, or say ‘post as-is’.”

Do not proceed to Step 6 until the user explicitly says to post. This gate is not optional — the whole point is to let the human adjust before anything hits GitHub.

After approval

Re-read the (possibly edited) draft file before posting — use its content as the source of truth for what gets posted, not the original agent outputs. Then delete the draft file after posting completes.

Step 6 — Post inline comments and submit review

After human approval, re-read the draft file. For each PR:

  1. Parse the Review Event field from the draft file header. Valid values: APPROVE, REQUEST_CHANGES, COMMENT. The human may have changed this from the agent’s original recommendation — always use the value in the file, not the agent’s original verdict.

  2. Collect IRRELEVANT findings before posting. Scan the draft file for any row — in the Inline Comments table, the Body Comments table, or the Prior Discussions table — whose severity (or Orig. Severity for Prior Discussions) is IRRELEVANT. For each one:
  3. Assemble the review body from the Body Comments table. Every row in Step 5’s “Body Comments” subsection that isn’t marked IRRELEVANT (findings the agent deliberately left without a line because it couldn’t confirm one against the diff) must end up in the posted review body — never dropped, but an IRRELEVANT row from item 2 is still skipped here, not posted. Start building a ### Additional Comments section from these rows now, as bullets in the form - **{file}** — {comment body} — this table has no line column by construction, so do not invent one. Item 4 below adds more bullets to this same section, in the different form - **{file}:{line}** — {comment body} (those rows do have a confirmed line), for any inline comment that fails line validation; the merged result is used when posting in items 5–6.

  4. Pre-validate line numbers before posting. For each PR that has inline comments, fetch its diff and build the valid RIGHT-side line set. Apply the algorithm from the Output Contract’s “How to verify a line is in the diff” section. The pseudocode below illustrates the logic — apply it when constructing the API payload, not as runnable code:

    # PSEUDOCODE — apply this logic mentally when building the payload
    # gh pr diff <pr> -R <repo> --color=never
    valid = {}          # path → set of right-side line numbers
    current_path = None
    new_line = 0
    for raw in diff.splitlines():
        if raw matches r'^\+\+\+ b/(.+)':
            current_path = match.group(1)
            valid[current_path] = set()
            new_line = 0          # reset per file
        elif raw matches r'^@@ -\d+(?:,\d+)? \+(\d+)':
            new_line = int(match.group(1))   # reset per hunk
        elif current_path is None:
            continue
        elif raw starts with '+' or ' ':
            valid[current_path].add(new_line)
            new_line += 1
        elif raw starts with '-' or '\\':
            pass   # no right-side line number
    

    For each proposed inline comment, check if (path, line) is in the valid set:

  5. Post inline comments with the review event using gh api. Use side: "RIGHT" for all inline comments. Only post comments for lines confirmed in step 4. Use the content from the approved draft file — not the raw agent output. Skip any finding marked IRRELEVANT. Set body per the rule in item 6 below (GitHub rejects a COMMENT-event review with an empty body, so this applies here too, not only to the no-inline-comments case).
gh api repos/{owner}/{repo}/pulls/{number}/reviews \
  --method POST \
  --field body="{body per item 6's rule}" \
  --field event="{REVIEW_EVENT}" \
  --field "comments[][path]=path/to/file.ts" \
  --field "comments[][line]=42" \
  --field "comments[][side]=RIGHT" \
  --field "comments[][body]={your identity} says: <finding>"

Where {REVIEW_EVENT} is the value read from the draft file’s Review Event field for that PR (APPROVE, REQUEST_CHANGES, or COMMENT).

  1. Every PR gets a posted review, regardless of event or whether it has inline comments — there is no case where nothing gets submitted. If there are no inline comments (all findings had a confirmed line, and ### Additional Comments is empty), submit the review without inline comments:
gh api repos/{owner}/{repo}/pulls/{number}/reviews \
  --method POST \
  --field body="{body per the rule below}" \
  --field event="{REVIEW_EVENT}"

Body rule (applies to both items 5 and 6): if ### Additional Comments has any bullets, use it as the body. If it’s empty, use "Reviewed by {your identity}" — never post an empty body; GitHub rejects event=COMMENT with a blank body, and a clean draft PR (Step 2 item 4 forces verdict: "COMMENT" unconditionally, per Step 7) is exactly the case most likely to have zero inline comments and an empty ### Additional Comments section, so this isn’t an edge case to skip.

Post reviews for all PRs.

Step 7 — Review event rules

The agent populates the Review Event field in the draft file as its initial recommendation. The human can override it before posting. These are the rules for the agent’s initial recommendation:

Draft PR precedence: if is_draft: true, the review event is always COMMENT regardless of unresolved prior concerns. Prior discussions are still analyzed and surfaced in the output — the draft status overrides the event, not the analysis.

Human override: Whatever value the human leaves in the Review Event field when they approve the draft is what gets submitted to the GitHub API. The agent’s recommendation is just a starting point.

Step 8 — Notify the user

After posting all reviews: