AI Diff Reviewer — Apply Review (sub-skill)
Companion to the ai-diff-reviewer skill. Where the
parent runs the review locally and open-pr
writes the pull request, this sub-skill closes the loop: it
reads the review the CI Action posted back on the PR, presents
the findings in the same format the local review uses, and — with
explicit consent — walks the developer through each finding to
apply, defer, or skip.
The design philosophy mirrors the family's:
- Parity of shape. The output uses the same
verdict → findings table → per-finding body → notes → recommendationstructure the parent skill emits. A developer who has seen one of the two knows how to read the other. When the CI leg found "SQL injection insrc/auth.ts:55", the summary looks identical whether it was your local agent or CI that surfaced it. - Read-only by default. Fetching + presenting the review never writes anything. Only when the developer explicitly asks to "walk through" or "apply the fixes" does the sub-skill open source files, and each individual apply still requires a yes.
- Multi-provider aware. This repo (and any consumer that opts
into the 4-leg matrix) posts up to four independent reviews per PR,
distinguished by
self-reviewed:<provider>labels. The sub-skill reads all live legs, attributes each finding to its leg, and surfaces cross-leg consensus ("agreed by 3/3 legs → strong signal; called by 1/3 → could be leg-specific"). - Never commits, never pushes. Applied fixes stay unstaged in the
working tree. Commit + push is the developer's judgment call,
matching
open-pr's trust boundary.
The single source of truth for the workflow this sub-skill implements
is docs/PR_REVIEW_WORKFLOW.md.
This sub-skill is that doc, executable.
When it fires
Read + present the review (default flow) — triggers:
- "What did the CI review say?"
- "Read the review on this PR"
- "Show me the review findings"
- "Did the bot approve or request changes?"
- "Which findings blocked the merge?"
- "The AI review just landed — summarize it"
Walkthrough mode (apply/defer/skip per finding) — triggers:
- "Apply the AI review's fixes"
- "Walk me through the findings"
- "Go through the review one finding at a time"
- "Help me address the critical findings"
- "Fix the review comments the bot left"
Scope filters — triggers:
- "Critical only" / "show me only the critical findings"
- "Warnings only" / "info-level only"
- "Just the ones from the
cursorleg" (multi-leg attribution) - "The findings we all three legs agreed on"
Fall through to a sibling skill when the developer:
- Wants a local review on uncommitted changes → parent
ai-diff-reviewerskill (default review flow). This sub-skill reads reviews that already exist on the PR; the parent produces a new one locally. - Wants to install the CI action →
setup. - Wants to customize the reviewer →
generate-extension. - Wants to author or refresh the PR body →
open-pr. - Wants to read human reviewer comments (not the bot's) → defer;
this skill filters to bot artefacts by design.
gh pr view --commentscovers human threads.
If the intent is ambiguous ("show me the review" on a repo where the Action posted 4 legs, or on a PR with both bot findings and human comments), ask ONE clarifying question before acting.
Step 0 — Trust boundary
This sub-skill's reads and writes are enumerated below. Nothing else happens.
Reads (always allowed, no consent needed):
git:git branch --show-current,git rev-parse HEAD,git status, andgit show <marker-sha>:<path>— the latter is Step 6b's primary pre-image source (used on everyapply, not just the empty-diffHunkcase) and is safe by construction: write-free, streamed to stdout, never touches the working tree, never creates or modifies a ref. Use it whenever the sub-skill needs SHA-pinned file content.gh:gh pr viewfor PR metadata,gh api graphqlfor reviews + comments. Notgh pr diff— it always emits current tip vs base with no way to pin a historical SHA, so it can't be used for pre-image or freshness checks (Step 6b covers this in detail); if the sub-skill needs SHA-pinned file content, usegit showinstead.- Local files under the current git checkout, via
Read/Grep/Glob— only for files a finding references.
Writes (only under the per-finding consent contract in Step 6):
- Edit source files — when the developer answers "apply" on a
finding that has a
\``suggestion` block, the sub-skill reads the target file, replaces the exact lines the suggestion covers, and writes the file back. Never touches lines outside the suggestion's range. - Create
.review/deferred.md(0-byte if empty, otherwise a single-line-per-deferral list) — only when the developer answers "defer" on any finding AND is prompted-and-agrees to persist. If the developer prefers ephemeral deferrals, the file is not created. - Append
.review/deferred.mdto.gitignore— only as a one-time follow-up the very first time.review/deferred.mdis created in this repo, AND only when the developer accepts the separate consent prompt in Step 6c. Never modifies existing.gitignorerules; only appends one line (plus a labeled comment identifying this sub-skill as the source) after the existing content.
It does not:
git add,git commit,git push,git checkout, or any history-rewriting operation.- Post replies, resolve conversations, dismiss reviews, approve, or
request changes on the PR — the sub-skill is read-only against
the GitHub PR-review surface. Replies to individual threads are a
separate concern (a future
respond-to-reviewsub-skill). - Call the LLM provider directly — it uses the coding agent that's already running you.
- Fabricate suggestions, expand suggestion blocks beyond what the bot wrote, or reason about "what the bot probably meant" when a suggestion is absent. If a finding has no suggestion, "apply" is not offered; the developer implements the fix themselves.
- Change any file outside the finding's
path:linescope on "apply". If a suggestion is malformed (e.g. the file no longer matches the expected context), the sub-skill refuses and surfaces the finding for manual application.
If the coding agent has broader powers (e.g. can run arbitrary bash), those come from the harness, not this sub-skill.
Step 1 — Detect context
Establish the mode (present vs. walkthrough vs. refuse) before fetching anything from GitHub.
# Current branch + head SHA
HEAD_BRANCH="$(git branch --show-current)"
HEAD_SHA="$(git rev-parse HEAD)"
HEAD_SHORT="$(git rev-parse --short HEAD)"
# Repo slug (owner/repo)
REPO="$(gh repo view --json nameWithOwner --jq .nameWithOwner 2>/dev/null || echo '')"
# PR for the current branch (if any)
PR_JSON="$(gh pr view --json number,url,state,headRefOid,labels,isDraft 2>/dev/null || true)"
PR_NUMBER="$(gh pr view --json number --jq '.number // empty' 2>/dev/null || true)"
PR_NUMBER is consumed by every subsequent gh / gh api graphql
invocation (Steps 2b, 3, 7). Extracted via gh pr view --json … --jq … (not standalone jq) so the sub-skill's dependency set
stays git + gh — the two bins already declared in
metadata.requires.anyBins, matching sibling sub-skills. If the
gh pr view call fails (no open PR on this branch), PR_NUMBER is
empty and the mode table below routes to refuse-soft.
Decide the mode from the state:
| State | Mode | Skill behavior |
|---|---|---|
On main / master / develop / trunk with no PR |
refuse | Ask the developer to switch to a feature branch that has an open PR. |
| Detached HEAD | refuse | Ask the developer to check out a branch first. |
PR_JSON empty (no PR for this branch) |
refuse-soft | Point at open-pr: "No open PR for <branch>. Open one first with the open-pr sub-skill, wait for CI to review, then re-run me." |
PR_JSON state is CLOSED or MERGED |
warn | The review artefacts still exist and are readable — offer to read anyway ("this PR is closed / merged; want the historical review or should I stop?"). Do not proceed silently. |
PR_JSON state is OPEN |
read | Go to Step 2. |
On gh missing or unauthenticated: surface the exact remediation
(brew install gh or gh auth login) and stop. Do not attempt to
scrape the PR page via a browser — the sub-skill is designed for
gh + GraphQL and there is no fallback path.
Step 2 — Fetch the review
This is the mechanical heart of the sub-skill. The GraphQL query below
is adapted from docs/PR_REVIEW_WORKFLOW.md
— the selection set is extended (createdAt, submittedAt,
startLine for multi-line-suggestion apply) but the filter rules
are the shared contract: if the doc says "skip isMinimized == true", this sub-skill does. If it says "anchor on the most recent
marker", this sub-skill does. Divergence in the selection set is
allowed and expected as this sub-skill's needs evolve; divergence in
the filter rules is a bug — fix the doc, then re-sync here.
2a. Identify the bot login
The Action collapses prior artefacts belonging to the user its
github-token authenticates as. For consumers using
secrets.GITHUB_TOKEN (default), that's github-actions[bot]; for
consumers using a PAT / automation account, it's the PAT owner's
login. Hardcoding github-actions[bot] silently mis-filters every
PAT consumer — Step 2c drops the real review author and Step 2d
then fires the "Missing (no marker found)" branch even though a
live review exists on the PR.
Resolve the login in this order (first non-empty wins):
AIPRR_BOT_LOGINenv var — explicit override; always respected.- The most recent non-minimized
<!-- ai-pr-reviewer-marker -->comment'sauthor.login— authoritative and self-configuring acrossgithub-actions[bot], PATs, and automation accounts. Read it from thecommentscollection you already fetch in Step 2b; pick the newest comment whose body starts with<!-- ai-pr-reviewer-marker -->. gh api user --jq .login— a last-resort fallback only when no marker exists on the PR (a first-run install where CI hasn't posted anything yet).- The literal string
github-actions[bot]— final default when even step 3 fails; a warning should be surfaced to the developer since this is almost always wrong for PAT consumers.
The Step 2b query already returns the top-level comments nodes
with author.login. To keep the sub-skill's runtime deps aligned
with metadata.requires.anyBins (git + gh only — no
standalone jq), issue a small dedicated gh api graphql call for
the marker-author read, using gh's built-in --jq filter to
extract the login without shelling out to jq:
BOT_LOGIN="${AIPRR_BOT_LOGIN:-}"
if [ -z "$BOT_LOGIN" ]; then
# Preferred: derive from the most recent non-minimized marker comment.
# Uses gh's built-in --jq (no standalone jq dependency).
BOT_LOGIN="$(gh api graphql \
-F owner="${REPO%%/*}" -F repo="${REPO##*/}" -F number="$PR_NUMBER" \
-f query='
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
comments(first: 100) {
nodes { body isMinimized createdAt author { login } }
}
}
}
}' \
--jq '[.data.repository.pullRequest.comments.nodes[]
| select((.isMinimized | not)
and (.body | startswith("<!-- ai-pr-reviewer-marker -->")))]
| sort_by(.createdAt) | reverse | .[0].author.login // empty' \
2>/dev/null)"
fi
if [ -z "$BOT_LOGIN" ]; then
# Fallback: current authenticated user (only accurate on first-run repos
# where no marker exists yet). Warn the developer that this is a guess.
BOT_LOGIN="$(gh api user --jq .login 2>/dev/null || echo 'github-actions[bot]')"
fi
gh api graphql --jq runs the filter inside gh itself — it does
not shell out to standalone jq, so this snippet stays inside the
git + gh-only dep constraint declared in metadata.requires.
Do NOT reintroduce a standalone jq | ... pipe here — the moment
you do, the primary marker-author path breaks on vanilla macOS /
minimal agents and the fallback (gh api user) mis-filters live
reviews for every PAT / automation-account consumer.
2b. Run the GraphQL query
gh api graphql -F owner="${REPO%%/*}" -F repo="${REPO##*/}" \
-F number="$PR_NUMBER" -f query='
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
comments(first: 100) {
nodes {
id
body
isMinimized
createdAt
author { login }
}
}
reviews(first: 100) {
nodes {
id
body
state
isMinimized
submittedAt
author { login }
commit { oid }
comments(first: 100) {
nodes {
id
body
path
line
startLine
diffHunk
isMinimized
}
}
}
}
}
}
}'
startLine is null for single-line inline comments and non-null for
multi-line ones (e.g. a \``suggestion``` block covering several
lines). When you later need the apply range for Step 6b, derive it as
start = startLine or line, end = line — the reviewer's line
field is the range end for multi-line comments and the anchor for
single-line ones. originalLine is the outdated-position field
(diff-relative for reviews that no longer point at the current file
state) and is not the multi-line-range start — do not use it here.
line: null is a distinct case. GitHub returns line: null (and
startLine: null) when the comment is anchored on a hunk that is no
longer present in the diff — the review was posted, the developer
force-pushed or rebased, and the anchor became orphaned. This shows
up precisely on the stale-review paths Step 2d allows after
acknowledgement. Because [startLine or line, line] would collapse
to [null, null] and every downstream comparison would either crash
or produce a bogus apply range, treat line: null as a read-only
finding:
- Include the finding in Step 4's presentation with a marker like
[⚠️ outdated anchor — apply disabled]. - In Step 6a's walkthrough, present the body normally.
- In Step 6's menu, never offer
apply— onlydefer,skip,discuss,stop. Do not attempt to reconstruct a range fromoriginalLine(it's diff-relative to a hunk that no longer exists; the numbers won't map cleanly onto today's file). - In Step 3a's consensus scoring, group
line: nullfindings only with otherline: nullfindings on the samepath— the range- overlap predicate is undefined for null ranges.
The diffHunk field carries the raw diff hunk the comment was
anchored on (the -, +, and context lines around the anchor). It
is used to render the surrounding patch when the developer needs
context — that display belongs in Step 4's presentation table
(next to the file/line) and/or Step 6a's walkthrough banner for
the current finding, NOT in Step 5 (which is only the top-level
routing menu: done / walk through / critical only / warnings and up / filter by leg / cancel). diffHunk also serves as an
optional
consistency check inside Step 6b — but it is not the primary
source of the expected pre-image; that role belongs to
git show <marker-sha>:<path> (see Step 6b for the full
derivation). Note the GraphQL schema does not expose a side
field on PullRequestReviewComment — side-of-diff lives on
PullRequestReviewThread.diffSide. Step 6b sidesteps this by
reading the reviewed-commit file content directly, which is
independent of hunk sides. In practice this is safe because
findings_to_gh_inline_comments() in scripts/reviewer.py posts
anchors with side: RIGHT by default (the "side": f.side or "RIGHT" line inside that function is the authoritative reference —
cite the function name, not a raw line number that will drift on
the next edit), so line / startLine refer to post-image file
positions that resolve cleanly
via git show. A rare side: LEFT anchor (removed-line comment)
will fail the git show-slice consistency check in Step 6b and
route to skip / discuss.
BOT_LOGIN is not a GraphQL variable — it's applied client-side
in Step 2c's filter. GitHub's GraphQL API rejects unused declared
variables (variableNotUsed), so declaring $bot: String! while only
using it in a follow-up jq filter fails the query outright. Keep
the login filter in Step 2c and the GraphQL selection set variable-free.
2c. Filter to live bot artefacts
Two collections come back — comments (top-level, includes tracking
markers) and reviews (with nested inline comments). Apply the
mandatory rules from docs/PR_REVIEW_WORKFLOW.md § Mandatory rules:
- Filter out
isMinimized == trueat every level (top-level comments, review bodies, inline review comments). - Filter authors to
$BOT_LOGINonly. - Anchor on the latest
<!-- ai-pr-reviewer-marker -->comments — plural when multiple legs ran. Each marker's body starts with the marker string; each carries the SHA its leg reviewed.
IAR note. The marker may contain an HTML-comment IterationState
JSON block and a short gen/round/policy annotation — ignore both when
collecting findings (they are telemetry, not review comments). The
inline comments you present are already post-IAR-dedup. If the
developer expected a finding that appeared in a local review but not
in CI, suggest applying full-review-please (the
iteration-escape-label, one-shot full review with state preserved)
or removing the applied-label then re-triggering for a full state
reset — see
docs/ITERATION_AWARENESS.md § 8.
Emit the filtered set to your working context as a list of
{leg_label, sha, review_body, inline_comments[]} records.
2d. Anchor + freshness check
Tracking markers are top-level issue comments — they have no
commit.oid. The SHA source depends on the marker state (the H3 tells
you which one: _Working…_ vs. ✅ done / 🚫 done vs. ❌ failed):
- Working markers (
render_tracking_body_working()inscripts/reviewer.py) — include aFull SHA: \`` line in the body. Parse it. This is the in-flight case; a Working marker means the review is still running or its terminal transition failed. - Done and failed markers (
render_tracking_body_done()/render_tracking_body_failed()) — do NOT renderFull SHA:; they only carry the 7-char short SHA in the H3 (AI review for \<7chars>`). Resolve the full SHA by joining the marker to its matching non-minimized review via the provider marker (both carry), then read the joined review'scommit.oid` — that is the primary SHA source for completed reviews (which is the common case). The 7-char prefix in the H3 is a useful cross-check but is ambiguous by itself.
If the joined review is missing when the marker says done or
failed (rare — the mid-flight transition raced), fall back to the
7-char H3 prefix and warn the developer that the review body could
not be paired.
Compare the resolved SHA against HEAD_SHA:
| Marker SHA | HEAD SHA | Interpretation |
|---|---|---|
| Matches HEAD | Matches | Review is for the current commit — proceed normally. |
Matches HEAD's parent (marker == HEAD~1) |
Matches | The developer has made one commit locally since the review; HEAD itself is a commit CI never saw. Warn: "The review is for <marker-sha>, but you've committed <HEAD> since then. Findings may already be resolved by your local commit; CI will re-review once you push. Continue anyway?" — proceed only after ack. Pre-image checks in Step 6b will still refuse silently-clobbering an already-edited line. |
| Older than HEAD's parent | New commits landed since the review | Warn: "The review is for <sha>, but HEAD is <newer>. Reading anyway, but the findings may be stale — CI will likely re-review. Continue?" |
| Newer than HEAD | Local branch is behind origin | Warn: "Origin has <sha>, your local HEAD is <older>. Consider git pull before applying." — proceed only after ack. |
| Missing (no marker found) | — | CI hasn't run yet OR the label-gate is missing OR all matrix legs' secrets are unset. See docs/PR_REVIEW_WORKFLOW.md § "I don't see any live review" for the diagnosis table. Stop; do not proceed with an empty review. |
Only exact-HEAD is fully authoritative; every other row above
(including marker == HEAD~1) requires an explicit developer
acknowledgement before Step 4 presents findings or Step 6b applies
anything. This closes the "silent green light" case where a single
uncommitted-since-CI commit would otherwise be treated as
authoritative.
Never fabricate findings when the review is missing or stale.
2e. Extract per-finding severity
GitHub inline review comments carry body only —
findings_to_gh_inline_comments() in scripts/reviewer.py does
not prefix severity into the comment body, so severity is not
recoverable from the inline comment itself. The authoritative
per-finding severity lives in the review summary body's findings
table:
### 2. Findings table
| # | Severity | File | Summary |
|---|------------|---------------------|----------------|
| 1 | 🚨 critical | `src/auth.ts:55` | SQL injection… |
| 2 | ⚠️ warning | `src/cache.ts:120` | Unbounded key… |
For each inline comment produced by the leg, join to the summary
table by matching on path:line:
Locate the findings table heading-agnostically. Scan
review_bodyfor a Markdown table whose header row contains all ofSeverity,File, andSummary(case-insensitive, in any column order). The heading right above the table varies —prompts/default.mdinstructs models to emit it under### 2. Findings table, while the local review flow uses## Findings, and consumers with custom prompts may use anything — so anchoring on the header row (not the heading text) is the only reliable strategy.Parse each row into
{severity, path, line, summary}. The severity cell is<emoji> <label>(🚨 critical,⚠️ warning,ℹ️ info); normalise to the bare label. TheFilecell is typically backticked (`src/auth.ts:55`, matching whatprompts/default.mdemits) and may additionally be bold (**`src/auth.ts:55`**); strip surrounding backticks AND surrounding**before splitting on:— otherwise the join against the GraphQLpathfield (unquoted) misses every row and every finding falls to theinfofallback below.For each inline comment, look up a matching row. Because a multi-line inline comment carries a
startLine(range start) andline(range end), while a summary table'sFilecell almost always cites a single line — usually the range start (e.g.`src/auth.ts:55`for a 55–58 suggestion), occasionally the end, occasionally an arbitrary line inside the range — the join must be range-aware, not exact-scalar:- Compute the comment's covered range as
[startLine or line, line]. (For single-line comments wherestartLineis null, that collapses to[line, line].) - A summary row matches when
row.path == comment.pathandrow.line ∈ [startLine or line, line](i.e. the row's cell falls anywhere inside the comment's covered range). - On match, set
comment.severity = row.severity.
The exact-scalar version this originally shipped with (matching only
row.line == comment.line) silently downgrades every multi-line critical toinfowhen the summary table citesstartLine, and those findings then disappear from acritical onlywalkthrough — quiet severity downgrade.- Compute the comment's covered range as
On no match (the model posted an inline anchor the summary table doesn't reflect, a stale/reordered table, or no matching table found at all), default to
infoand record that fact — the walkthrough banner in Step 6a surfaces the finding as[ℹ️ info (assumed — no summary row)]so the developer knows the tier is a fallback, not a model call.
The tracking comment's Highest severity: line is an
aggregate across the whole review — useful for the review-level
headline but never a per-finding value. Do not infer severity from
inline-body tone or keywords ("this is critical because…"); use the
summary table join or the info fallback.
The severity attached here is what Step 4's presentation table shows
and what Step 5's critical only / warnings and up filters gate on.
Step 3 — Attribute per leg
For consumer repos running a single provider (~95% of the population), skip this step — there's one live review; every finding belongs to "the review".
For repos running a multi-provider matrix (this repo, some enterprise
consumers), the mandatory rules require attributing each finding to
its leg via the self-reviewed:<provider> labels the Action applies
on success. Read the PR's labels:
gh pr view "$PR_NUMBER" --json labels --jq '.labels[].name' \
| grep '^self-reviewed:'
Map each live tracking marker to its leg. The marker + review body
contain provider-specific hints emitted by provider_marker() in
scripts/reviewer.py — an HTML marker of the form
<!-- ai-pr-reviewer-provider: <provider-id> --> (cursor,
claude-code, codex, or anthropic) — grep for the marker prefix
<!-- ai-pr-reviewer-provider: and read the value on the same line.
The self-reviewed:* label list confirms which legs completed. When 3 live reviews are
present and only 2 self-reviewed:* labels are set, the third leg is
either mid-flight or its post-review label step raced — surface this
as a note, don't refuse.
3a. Consensus scoring
Group findings by overlapping (path, range) — where each finding's
range is [startLine or line, line] (the same range predicate Step
2e uses for the severity join). Two findings belong to the same group
when their paths match AND their ranges overlap at all — i.e.
f1.start <= f2.end AND f2.start <= f1.end. Exact scalar (path, line) grouping silently misses multi-leg consensus when three legs
flag the same multi-line issue with slightly different anchors (e.g.
55–58 from one leg, line:55 from another, 56–59 from a third)
— those surface as three "single-leg" findings instead of the strong
signal the multi-leg design is built to expose. Keep the predicate
symmetric with Step 2e so the two flows agree on what "the same
finding" means.
Findings grouped by overlapping (path, range) |
Interpretation |
|---|---|
| All active legs called it | Strong signal — high confidence, agreed across legs. |
| Majority of active legs called it | Consensus — likely real, one leg missed it or filtered by extension. |
| Single leg called it | Provider-specific — could be a real finding one provider's training caught, or a leg-specific false positive. Present at nominal severity but note the single-leg source. |
Two legs disagree on severity (critical vs. info at overlapping anchors) |
Split call — surface both severities and both bodies; let the developer decide. |
Findings not shared across legs are still valid — they're just weaker signal than the ones every leg agreed on. Do not silently drop them.
Step 4 — Present the review
Emit the findings in the parent skill's exact format so the developer's mental model transfers cleanly. This is the parity contract.
4a. Single-leg output (single-provider consumer)
The outer fence uses four backticks so the nested ``` in
the finding-body placeholder below renders as literal code, not a
premature fence close.
## Verdict (from CI review on <PR URL>)
<one sentence — pulled verbatim from the review body's Verdict line
when present, otherwise inferred from the highest-severity finding.>
## Findings
| # | Severity | File | Summary |
|---|----------|------|---------|
| 1 | 🚨 critical | `src/auth.ts:55` | SQL injection in raw-string login query |
| 2 | ⚠️ warning | `src/cache.ts:120` | Unbounded cache key cardinality |
| 3 | ℹ️ info | `tests/utils.ts:12` | Helper could be reused from existing fixture |
### 1. `src/auth.ts:55` — 🚨 critical
<the finding body verbatim from the inline comment, including any
```suggestion block```>
### 2. `src/cache.ts:120` — ⚠️ warning
<...>
### 3. `tests/utils.ts:12` — ℹ️ info
<...>
## Notes (no inline anchor)
- <cross-cutting concerns, from the review body's Notes section>
**Recommendation:** approve / request-changes / comment-only
**Review SHA:** <sha> (matches HEAD ✓ | HEAD is 2 commits newer ⚠️)
4b. Multi-leg output (this repo + power consumers)
Add a Legs column to the findings table and a per-leg breakdown
at the bottom. Each leg's abbreviation (A = anthropic, C =
cursor, CC = claude-code, CO = codex) is derived from the
self-reviewed:* labels; document the mapping once at the top.
## Verdict (consensus across <n> legs)
<one sentence describing the consensus — "Blocking security fix
needed" when ≥ 2 legs called a critical; "Approve — no blockers"
when all legs are info-or-none.>
Legs on this PR: A=anthropic (~), C=cursor (~), CC=claude-code (~), CO=codex (~).
Live reviews: <n>. Findings shown are the union across all legs.
## Findings
| # | Severity | File | Summary | Legs |
|---|----------|------|---------|------|
| 1 | 🚨 critical | `src/auth.ts:55` | SQL injection | A / C / CO |
| 2 | 🚨 critical | `src/cache.ts:120` | Unbounded cache key cardinality | A |
| 3 | ⚠️ warning | `src/log.ts:44` | Log level normalization | A / C |
| 4 | ℹ️ info | `tests/utils.ts:12` | Helper could be reused | C |
| 5 | ℹ️ info | `docs/README.md:8` | Typo: "recieve" → "receive" | CO |
### 1. `src/auth.ts:55` — 🚨 critical [A / C / CO — 3/3 consensus]
<the finding body from the leg with the most detailed suggestion,
followed by a "See also" bullet with the other legs' bodies collapsed
by default. When legs disagree meaningfully, surface both bodies in
full.>
### 2. `src/cache.ts:120` — 🚨 critical [A — single-leg]
<body verbatim, with a note: "Only the anthropic leg called this
critical; other legs did not flag this path. Could be leg-specific
sensitivity or a real gap other providers' training missed.">
<... same shape for remaining findings ...>
## Notes (aggregated across legs)
- <deduplicated cross-cutting notes; if two legs said "the same"
thing in Notes, show it once.>
**Recommendation:** request-changes (blocking on 2 critical findings,
1 with 3-leg consensus)
**Review SHA:** <sha> (matches HEAD ✓)
4c. Notes on formatting
- Do not fabricate a Verdict line if the review body doesn't have one. Infer it from the highest-severity finding, and mark the inference: "Verdict (inferred from findings): …".
- Preserve suggestion blocks verbatim. If a finding has a
\``suggestion ... ```` block, keep the fence, keep the content, do not "clean up" whitespace. - Copy inline comment bodies exactly. Reviewers wrote what they wrote; do not paraphrase.
- Attach the review SHA + freshness marker at the bottom of every presentation. It's the single most important context for whether the findings are actionable.
Step 5 — Ask ONE routing question
After presenting the review, ask exactly one question to route the next action. Default choice must be the safest one (do nothing).
<n> finding(s) on your PR. What next?
- done → I've shown the summary; take it from here.
- walk through → I'll go finding-by-finding; you say apply/defer/skip.
- critical only → walk through, but only the <k> critical(s).
- warnings and up → walk through the ~<w> warning-or-critical findings.
- filter by leg → pick a specific leg (A / C / CC / CO) and walk its findings.
- cancel → stop. No walkthrough.
Response handling:
- done → go to Step 7 (summary + hand-off).
- walk through → go to Step 6 with all findings.
- critical only → go to Step 6 with
severity == criticalonly. - warnings and up → go to Step 6 with
severity in {critical, warning}. - filter by leg → ask which leg (A / C / CC / CO), then go to Step 6 with only that leg's findings.
- cancel → stop. Print nothing else.
If the developer's response is ambiguous, default to done (the minimally-disruptive choice) — do not silently open a walkthrough.
Step 6 — Walkthrough mode
For each finding in the filtered set, in severity order (critical
first, then warning, then info), present it in isolation with a
five-option prompt (apply is only shown when the finding carries a
\``suggestion```` block, so on findings without one the visible
menu collapses to four options).
6a. Per-finding presentation
The outer fence uses four backticks so the nested ```
suggestion-block placeholder inside renders as literal code, not a
premature fence close.
──────────────────────────────────────────────────────────
Finding <k> of <n>: <path>:<line> [<severity emoji> <severity>] [<legs>]
<finding body verbatim, wrapped ~80 chars>
<if there's a ```suggestion``` block:>
Proposed fix:
```suggestion
<content verbatim>
```
This replaces lines <start>–<end> in <path> with the block above.
Options:
- apply → I'll edit <path> to replace lines <start>–<end> with
the suggestion. You'll see the diff and confirm again
before I write. <-- only shown when a suggestion exists
- defer → I'll add this to `.review/deferred.md` (opt-in) so you
don't lose it. You handle it later.
- skip → move on; no action taken.
- discuss → surface the full body so you can copy it into a PR
comment reply. You handle the discussion in the UI.
- stop → end the walkthrough here; go to Step 7 summary.
──────────────────────────────────────────────────────────
6b. Handling apply
Only offered when the finding has a \``suggestion` block AND the
path + line range is reachable in the current working tree.
Read the target file. If
pathdoesn't exist in the working tree (e.g. renamed since the review), warn and skip: "<path>isn't in the current tree — the file may have been renamed. Falling back toskip."Verify the pre-image. A GitHub
\``suggestion```` block carries only the replacement text — not the lines it replaces. The apply step guards against silent-overwrite by first reconstructing what the reviewer actually saw, then comparing that to the current working tree.Primary source:
git show <marker-sha>:<path>. This streams the file's exact content at the reviewed commit to stdout (write-free — never touches the working tree). Slice linesstart–endfrom that stream (either read the whole stream and index into it, or pipe throughsed -n "${start},${end}p") — that is the expected pre-image. This works for every RIGHT-side comment (the defaultfindings_to_gh_inline_comments()posts, per the"side": f.side or "RIGHT"fallback inside that function inscripts/reviewer.py), which is the ~100% case for AI Diff Reviewer output: theline/startLinefields refer to post-image file positions, so slicing the reviewed-commit file at those line numbers gives the anchor's actual pre-image content.Then compare that slice line-for-line against the working tree's
<path>at linesstart–end. If they match, proceed to preview (step 3 below). If they diverge (developer's already edited nearby, or a different SHA than the review anchored on), present the three-option mismatch menu — and NEVER fall through to the happy-path preview automatically:"Lines
<start>–<end>in<path>no longer match the file at the reviewed commit. Applying anyway may produce incorrect output. Options:force/skip/discuss."skip— take no action; advance to the next finding in Step 6 (identical to the top-levelskipoption).discuss— surface the full finding body so the developer can copy it into a PR comment reply; advance to the next finding (identical to top-leveldiscuss).force— the escape hatch when the developer has read the divergence and wants to overwrite anyway (e.g. because their local edits are trivial reformatting and the suggestion still applies logically). Procedure:- Print a second banner making the risk explicit: "You're
about to overwrite lines
<start>–<end>in<path>even though the working tree differs from what the review saw. This may discard local edits. Typeyes forceto confirm, anything else to cancel." - Require the literal string
yes force(not justyes, noty, notapply) — the exact-string requirement is the second, deliberately-annoying confirmation the guardrail depends on. - On
yes force: skip the pre-image check, jump straight to preview step 3, then on the preview's ownyeswrite via theEdittool. On anything else: cancel and re-present the original three-option mismatch menu.
Once
forceis chosen and the write completes, log the fact in Step 7's summary ("1 apply-with-force" row) so the developer has an audit trail of which finding bypassed the safety check.forcenever becomes silent-default and never applies without both the mismatch banner AND theyes forceliteral.- Print a second banner making the risk explicit: "You're
about to overwrite lines
Secondary consistency signal:
diffHunk. When present, cross- check the sliced pre-image against thediffHunk's post-image side (+and context lines). If the slice does not appear inside the hunk's+/context region at all, the anchor is likely a rare LEFT-side (removed-line) comment, or the hunk header has drifted; refuse the apply and route toskip / discussrather than attempt to reconstruct the LEFT-side pre-image via@@header arithmetic (out of scope for this sub-skill).Failure modes and forbidden fallbacks. If
git show <marker-sha>:<path>fails (the marker SHA has been force-pushed away, the path was renamed in that commit, or the reviewed commit was garbage-collected), refuse the apply and route toskip / discuss— never silently overwrite. Do not usegit checkout <marker-sha> -- <path>as a workaround: that overwrites<path>in the working tree with the historical blo
…(truncated)