Use when starting a PR review and the session file needs to be created or refreshed. Not user-facing — invoked by the orchestrator only.
Scanned 5/27/2026
Install via CLI
openskills install ahardin13/review-assistant---
name: reading-pr-context
description: Use when starting a PR review and the session file needs to be created or refreshed. Not user-facing — invoked by the orchestrator only.
---
# Reading PR Context
Gather all context for a PR review: metadata, diff, linked issues, REVIEW.md guidelines, and code-review analysis. Write everything to a session file. Do not present anything to the user — the calling skill handles that.
**File I/O:** Use `Bash` with heredocs or `>>` for session/temp files — not Write, Read, or Edit tools.
## Inputs
- `pr_number`: required
- `repo`: owner/repo
- `threshold`: confidence threshold (default: 50)
## Step 1: Clean up old sessions
Data dir: `$HOME/.local/state/review-assistant` (kept OUT of `~/.claude/` so "always allow" permission rules persist — paths under `~/.claude/` are treated as sensitive by Claude Code and re-prompt every session).
```bash
mkdir -p $HOME/.local/state/review-assistant/sessions && find $HOME/.local/state/review-assistant/sessions -name "*.md" -mtime +7 -delete
find $HOME/.local/state/review-assistant -maxdepth 1 -name "pr-*-recreate-*.json" -mtime +7 -delete 2>/dev/null
```
The second `find` prunes the recreate-backup payloads written by `scripts/post-pending-review.py` before each destructive DELETE; they live one level above `sessions/` so the first `find` doesn't reach them.
## Step 2: Check PR eligibility
```bash
gh pr view <PR_NUMBER> --repo <REPO> --json state,isDraft,title
```
- **Closed/merged:** Use `AskUserQuestion`: "PR #N is closed. Proceed anyway?" with options "Yes, review it anyway" / "No, exit". Stop if no.
- **Draft:** Proceed normally, note "This is a draft PR."
- **Already reviewed:** Check for existing session file(s) matching `pr-<NUMBER>-*.md` in `$HOME/.local/state/review-assistant/sessions/`. If found, use the most recent one (by filename timestamp). Use `AskUserQuestion`:
> "You've reviewed this PR before (session: <date>). How would you like to proceed?"
- "Full re-review" — start fresh, delete old session file
- "Incremental" — review only changes since last review (reads `last_reviewed_sha` from session file, uses `git diff <last_reviewed_sha>..HEAD`)
- "Update session" — re-analyze full diff, keep `## User Context` from previous session
## Step 3: Fetch PR metadata and diff
```bash
gh pr view <PR_NUMBER> --repo <REPO> --json title,body,commits,labels,milestone,closingIssuesReferences,headRefOid
```
Record `headRefOid` as `review_sha`.
Fetch linked GitHub issues from `closingIssuesReferences`:
```bash
gh issue view <ISSUE_NUMBER> --repo <REPO> --json title,body
```
Fetch the full diff and cache it for reuse by downstream skills and the analyzer subagent. Writing to our data dir (rather than `/tmp/`) keeps the path inside the pre-approved allowlist so subagents don't get a per-session permission prompt:
```bash
mkdir -p $HOME/.local/state/review-assistant
gh pr diff <PR_NUMBER> --repo <REPO> > $HOME/.local/state/review-assistant/pr-<PR_NUMBER>-diff.txt
```
Refer to this path as `<DIFF_PATH>` in later steps.
## Step 4: Produce the "why" summary
From the PR title, body, commit messages, and linked issues, write a 2-3 sentence summary of what changed and why.
**If the why is unclear** (empty body, unhelpful commits, no linked issues): ask the user ONE question:
> "I couldn't determine why this change was made. Can you give me a brief summary of the intent?"
## Step 5: Load REVIEW.md
Load in order (both if present):
1. `~/.claude/REVIEW.md`
2. `REVIEW.md` at the project root
If neither exists: note "No REVIEW.md found — proceeding without review guidelines."
Concatenate contents (per-repo appends to/overrides global).
## Step 6: Write initial session file
```bash
cat <<'EOF' > $HOME/.local/state/review-assistant/sessions/pr-<NUMBER>-<YYYYMMDD-HHMMSS>.md
# Review Session: PR #<NUMBER> — <title>
review_sha: <headRefOid>
## Why
<2-3 sentence summary>
## Findings
_Populated by analysis_
## Queued Comments
_Populated during interactive review_
## User Context
_Populated if user provides corrections_
EOF
```
## Step 7: Run code-review analysis
### 7a: Check code-review plugin availability
Attempt to invoke the `code-review:code-review` skill. If not available, stop:
> "The `code-review` plugin is required but not installed. Install with:
> ```
> claude plugin install code-review@claude-plugins-official
> ```
> Then re-run `/review-assistant`."
### 7b: Identify inconsequential files
From the diff, identify files to skip (pure renames, generated files, mass reformats, bulk deletions). Mark as `skip: true`. **Exception:** If REVIEW.md guidelines call for reviewing a category of these files, include them.
### 7c: Dispatch code-review-analyzer agent
Dispatch the `code-review-analyzer` agent (Agent tool with `subagent_type="review-assistant:code-review-analyzer"`) with the following task. Substitute `{pr_number}`, `{repo}`, `{diff_path}` (the `<DIFF_PATH>` from Step 3), and `{review_md_content}`:
> Review PR #{pr_number} in {repo}.
>
> - The PR diff has already been fetched and cached at `{diff_path}`. Use that file instead of re-running `gh pr diff`. Do NOT write scratch diffs to `/tmp/` — that path triggers a user permission prompt every session. If the `code-review` skill accepts a diff path, pass `{diff_path}` to it; otherwise read from `{diff_path}` directly.
> - Use a confidence threshold of 0 — return EVERY finding the code-review skill produces, regardless of confidence. The caller filters later; do not filter here.
> - In addition to any CLAUDE.md files, also check the diff against these review guidelines:
>
> ---
> {review_md_content}
> ---
>
> If no review guidelines were provided above (the section between the --- markers is empty), skip the review guidelines compliance check.
### 7d: Parse and deduplicate findings
Parse the subagent's response. The agent's output format is a metadata head row followed by an indented description continuation block; the block ends at the next `- file:` line (or end of output). Inside the block, blank lines are paragraph breaks — preserve them. Each finding needs: `file`, `line`, `severity`, `confidence`, `source`, `description`. Skip malformed entries.
**Backwards compat:** if the head row has a trailing `description: <text>` (legacy single-line format), use that as the description. Treat it as a single paragraph.
Merge findings at the same `file` + `line`. Keep the highest confidence score. When merging descriptions, keep the longer/more-paragraph-shaped one rather than concatenating.
Serialize the merged list as JSON to `$HOME/.local/state/review-assistant/pr-<NUMBER>-findings.json` so Step 7e can read it.
### 7e: Anchor findings to the diff
Before writing findings to the session file, attach a line-text anchor (`code:` field) so downstream consumers can verify that comments land on the right lines.
**Do NOT write a standalone `/tmp/anchor-*.py` script for this step.** The authoritative anchorer is `scripts/classify-and-verify.py`, which runs at POST time inside `auto-draft-review` / `interactive-diff-review` and includes ±3-line text-match re-anchoring as a fallback. Step 7e's only job is to populate the `code:` text — a freelanced script duplicates the diff parser already in the plugin and adds drift risk for no benefit.
Use a one-shot inline `python3` invocation (no separate `.py` file). Read the cached diff once, build an in-memory `(file, right_line) -> text` / `(file, left_line) -> text` index, look up each finding's `(file, line)`, and emit decorated findings to stdout. ~25 lines is enough — example shape:
```bash
DIR="$HOME/.local/state/review-assistant"
python3 - "$DIR/pr-<NUMBER>-diff.txt" "$DIR/pr-<NUMBER>-findings.json" <<'PY' > "$DIR/pr-<NUMBER>-findings.anchored.jsonl"
import json, re, sys
right, left, cur, rl, ll = {}, {}, None, 0, 0
DIFF = re.compile(r"^diff --git a/(.+?) b/(.+)$")
HUNK = re.compile(r"^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@")
for raw in open(sys.argv[1]):
raw = raw.rstrip("\n")
if (m := DIFF.match(raw)): cur = m.group(2); right.setdefault(cur, {}); left.setdefault(cur, {}); continue
if (m := HUNK.match(raw)): ll, rl = int(m.group(1)), int(m.group(2)); continue
if cur is None or raw[:3] in ("+++", "---"): continue
if raw[:1] == "+": right[cur][rl] = raw[1:]; rl += 1
elif raw[:1] == "-": left[cur][ll] = raw[1:]; ll += 1
elif raw[:1] == " ": right[cur][rl] = left[cur][ll] = raw[1:]; rl += 1; ll += 1
for f in json.load(open(sys.argv[2])):
p = f["file"] # strip repo-absolute prefix if any
cand = next((k for k in right if p == k or p.endswith("/" + k)), p)
f["file"] = cand
f["code"] = right.get(cand, {}).get(f["line"]) or left.get(cand, {}).get(f["line"])
f["in_diff"] = cand in right
print(json.dumps(f))
PY
```
Findings come in via `sys.argv[2]` rather than stdin because `python3 -` consumes stdin for the heredoc script body — `< findings.json` would be silently dropped.
For each finding:
- If `code` is populated: render `code: \`<exact line text>\`` and `in_diff: true` in Step 7f.
- If the file isn't in the diff, or the line isn't in any hunk: set `in_diff: false`. **Do NOT drop the finding** — auto mode still reports it, and the post-time classifier may re-anchor it by text match.
### 7f: Write findings to session file
Record the active threshold for later consumers:
```
threshold: <T>
```
Partition findings by confidence. Write those with `confidence >= threshold` under `## Findings`, and those below threshold under `## Below Threshold` (same format, minus `skip`). Both sections use this shape:
```
## Findings
- file: src/foo.ts, line: 42, severity: high, confidence: 87, source: bug-scan, skip: false, in_diff: true
code: `const id = user.id;`
**Possible null dereference.** When `getUser()` returns null this line throws a TypeError. The pattern appears in three callers, only one of which handles the rejected promise.
**Suggestion:** wrap the call in a guard, or change `getUser` to throw a typed error so the caller has to acknowledge the failure mode.
- file: generated/types.ts, line: 1, severity: info, confidence: 100, source: claude-md, skip: true, in_diff: true
code: `export type Foo = ...`
Generated file — skipping.
## Below Threshold
- file: src/bar.ts, line: 15, severity: low, confidence: 30, source: code-comments, in_diff: true
code: `setTimeout(fn, 86400);`
Magic number 86400 could be a named constant.
```
Description-block rules:
- Each continuation line MUST start with at least two spaces. Indented Markdown nesting (lists, fenced code blocks at four-space indent) past the prefix is preserved when the body is rendered on GitHub.
- **Blank lines between paragraphs MUST be bare blank lines** — not two-space-indented empty lines. The classifier's parser treats truly empty raw lines as paragraph breaks (`\n\n`); a blank line that contains stray whitespace breaks differently.
- Single-paragraph descriptions stay on a single continuation line. Don't manufacture paragraph breaks that aren't in the source finding.
Interactive consumers read only `## Findings`. Auto-mode reads both so it can report what got filtered.
The `code:` line is the anchor that `classify-and-verify.py` uses to confirm the finding lands on the right diff line (with a ±3 line scan on mismatch). Keep backticks around the text literally — they're stripped by the parser.
## Step 8: Hand off silently
Do NOT print a closing summary, "session file ready" message, or "returning to the orchestrator" line — any user-facing text here reads as a turn boundary and causes the orchestrator to stop before dispatching the walkthrough skill.
Hold the session file path in working memory. The orchestrator's next step (Step 4 of `review-assistant.md`) will pass it to either `auto-draft-review` or `interactive-diff-review`. Proceed directly to that dispatch with no intervening output.
No comments yet. Be the first to comment!