From pr-brief
Interactive PR review. Pulls a PR via gh, groups files into features (markdown descriptions), computes a suggested review sequence, and launches a local HTML UI with GitHub-style stacked diffs, inline commenting (click-and-drag multi-line), realtime posting via gh, and an AI **narrative layer** of "explain pills" — purple ✨ callouts that tell the story of the PR alongside the code, connecting each block to the broader change. Triggers on: review this pr, brief pr, pr review ui, narrate pr, annotate pr, interactive pr review.
How this skill is triggered — by the user, by Claude, or both
Slash command
/pr-brief:pr-briefThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Launch an interactive, browser-based review workflow for a pull request.
Launch an interactive, browser-based review workflow for a pull request.
What the user sees: a local webpage with an ordered review sequence in the sidebar, and — when a feature is clicked — every file in that feature stacked top-to-bottom (GitHub "Files Changed"-style) with syntax-highlighted diffs, per-line inline commenting, and a parallel narrative layer of AI-authored "explain pills" (purple ✨ callouts) on the right that tell the story of the change. Each saved comment is posted to GitHub immediately (realtime — no batch submit step).
Pipeline:
ghdata.json (PR meta + features + sequence + per-file unified diff with pre-computed GitHub position values + resolved explain-pill positions)gh api~/.claude/pr-review/pr-<num>/ — never inside the repo being reviewed.The user passes a PR identifier. Accept any of:
123 — PR number, current repo (resolve via gh repo view --json nameWithOwner -q .nameWithOwner)owner/repo#123https://github.com/owner/repo/pull/123If absent, use the current branch's open PR: gh pr view --json number,headRepository.
gh auth status || echo "NOT_AUTHED"
If not authenticated, stop and tell the user: "Run gh auth login in a terminal, then re-invoke this skill."
mkdir -p ~/.claude/pr-review/pr-<num>
gh pr view <num> --repo <owner>/<repo> \
--json number,title,body,author,headRefOid,headRefName,baseRefName,url,additions,deletions,changedFiles \
> ~/.claude/pr-review/pr-<num>/pr.json
gh pr diff <num> --repo <owner>/<repo> > ~/.claude/pr-review/pr-<num>/pr.diff
gh pr diff <num> --repo <owner>/<repo> --name-only > ~/.claude/pr-review/pr-<num>/files.txt
Capture headRefOid — this is the sha you'll pass to the server (every comment posted needs it).
Launch one opus agent with:
files.txt)pr.diff) if <500 KB, else the first 1500 lines~/.claude/pr-review/pr-<num>/features.jsonInstruct the agent to:
Group every file into a single feature bucket. A feature is a kebab-case change theme (1-3 words): ownership-guards, asgi-middleware, group-conversations, migrations, documentation, etc. Group by change theme, not directory. A file with multiple themes joins them with + (e.g. sse-stability + inbox-ux).
For each feature, produce:
tldr — one-line plain-text what + why (≤120 chars; no markdown, the UI renders this as plain text)full_description — MARKDOWN-formatted, 2-4 short paragraphs or a bulleted list. The frontend renders this through marked (GitHub-flavored markdown). Use:
**bold** for key terms (file paths, function names, important nouns)- item bullet points for lists of changes (preferred over long sentences when there are 3+ discrete points)`inline code` for symbols, file paths, flagsblast_radius — high (schema/auth/migrations/breaking), medium (behavioral/endpoint changes/SSE), or low (docs/tests/refactors/lockfiles)why_first — one-line plain-text rationale for sequence positionOrder features into a sequence list. Heuristic:
Emit edges — pairs ["fromFeature", "toFeature"] for "understanding A helps read B". Examples: ["migrations","ai-enabled-flag"], ["ownership-guards","controllers-using-ownership"]. ~5-12 edges total.
Per-file briefs (required for non-trivial files). For every file, produce:
tldr — one-line plain-text what + why for this file specifically (≤120 chars; no markdown). Distinct from the feature-level tldr: that one summarizes the whole feature; this one summarizes the file's role within it. Example feature tldr: "Schema updates for group conversations". Example file tldr for V0079_*.py: "Adds is_group and group_jid columns to conversation_assignments".description — markdown, 1-3 short sentences or a small bullet list. Mini-PR-description for the file: what changed + why this file in particular needs to change. Use `inline code` for symbols, **bold** for the key noun. No headings. The UI renders this through marked and shows it in a header card directly above each file's diff (so a reviewer sees the file's purpose before scanning the diff itself).Skip both fields (or set to empty strings) only for: lockfiles, pure renames with no content changes, deleted-file shells, or auto-generated artifacts. For these, the feature-level brief is sufficient.
Per-file tldr + description are distinct from explain pills: the brief sits at the top of the file as a header; pills annotate specific line ranges inline. The brief answers "why am I about to look at this file?" — pills answer "why is this block written this way?".
Explain pills — the narrative layer. This is the most important output of the agent after the feature grouping. Explain pills are AI-authored callouts that tell the story of the PR. They render as floating purple ✨ boxes next to the diff, and a reviewer reading them in sequence order should leave understanding the whole change, not just isolated snippets.
Treat the pill set as one connected narrative arc, not a pile of independent annotations.
Step 6a — Plan the narrative (do this BEFORE writing any pill).
Walk through the sequence you produced. For each feature, ask:
Step 6b — Per file, produce 1–3 pills.
if/try block, a config dict, a SQL statement, a state machine. Avoid trivial 1-line spans unless that one line is the point.Step 6c — Each pill must do at least three of these four jobs.
path:symbol. Examples: "Called by inbox_service.get_conversations", "Reads the ai_enabled column written in V0077_*.py", "Consumed by the frontend store at src/stores/inbox.ts:streamInbox()". This is the connective tissue that makes pills feel like a story.tldr. Examples: "This is the producer side of the group-conversations seam", "Closes the SSE retry loop the middleware enabled".Step 6d — Voice and form.
`inline code` for symbols, files, flags, env vars.**bold** for the key noun (the function, the column, the concept).Step 6e — Connect across the sequence.
ownership-guards").inbox-controller next").Fields:
title — 4–8 words, plain text.body — markdown per Step 6d.start_line — first line number of the range, in the new file (post-change line numbers from the diff).end_line — last line number of the range (== start_line for single-line).side — "RIGHT" for new/added/context lines (default); "LEFT" only when the explanation is specifically about removed code.position here — build_data.py resolves it from (start_line, end_line, side) against the parsed diff.Anti-patterns to avoid:
Examples of good pills (showing connectivity):
{
"title": "ai_enabled column lands first",
"body": "Adds the **`ai_enabled`** boolean to `conversation_assignments`. This is the schema seam the rest of `ai-enabled-flag` builds on — `session_management.py` reads it via Redis cache (next pill), and the controller flips it through `PATCH /inbox/.../ai`.\n\nIdempotent because Cloud Run may rerun the migration on cold start."
}
{
"title": "Cache layer in front of Postgres",
"body": "Reads **`ai_enabled`** from Redis with a TTL fallback to the column added in the prior migration. Cuts the per-message DB hit on the hot WhatsApp path.\n\nWritten by the inbox controller's toggle endpoint; cache key is `ai:{phone_group_key}` to share state across group sessions."
}
{
"title": "Pure ASGI to unblock SSE",
"body": "Switched from Starlette's `BaseHTTPMiddleware` to raw ASGI because the former buffers `StreamingResponse` bodies through an asyncio queue.\n\n**Why now:** `inbox_service.stream_conversations` (next file) emits SSE — the buffer would have collapsed all events into a single chunk, which is the bug `sse-stability` fixes."
}
Output: Write ~/.claude/pr-review/pr-<num>/features.json matching exactly this schema:
{
"features": {
"<feature-name>": {
"tldr": "Plain-text one-liner",
"full_description": "**Markdown** with `code` and:\n- bullet one\n- bullet two\n\nA closing paragraph.",
"blast_radius": "high|medium|low",
"why_first": "Plain-text rationale",
"files": [
{
"path": "path/to/file.ext",
"tldr": "Plain-text one-liner about THIS file",
"description": "**Markdown** mini-brief. 1-3 short sentences or a small bullet list — what changed in this file and why it had to.",
"explanations": [
{
"title": "Short title (plain text, 4-8 words)",
"body": "**Markdown** body. 1-3 short sentences or a small bullet list explaining *why* this code is here.",
"start_line": 42,
"end_line": 56,
"side": "RIGHT"
}
]
}
]
}
},
"sequence": ["<feature-name>", ...],
"edges": [["<from>", "<to>"], ...]
}
Hard rules:
files.txt appears in exactly one feature. No duplicates, no orphans.sequence covers every feature.full_description is markdown; tldr and why_first are plain text.json.load).data.jsonThis step combines pr.json, features.json, and pr.diff into the single file the HTML reads. Do this yourself (not via agent) — it's mechanical. Use a small Python script with stdlib only:
For each file, extract its unified diff from pr.diff (split on ^diff --git a/). Parse the diff into an array of line objects with pre-computed GitHub position values.
Position rules (load-bearing — GitHub's API is strict):
position is 1-indexed. The first line after the file's header (i.e. the first @@ hunk header) is position 1.+, -) increments position by 1.\ No newline at end of file markers do NOT increment position; skip them.Per-line objects:
{ "position": 1, "type": "hunk", "content": "@@ -1,5 +1,10 @@" }
{ "position": 2, "type": "context", "content": " foo", "old_line": 1, "new_line": 1 }
{ "position": 3, "type": "add", "content": "+bar", "new_line": 2 }
{ "position": 4, "type": "del", "content": "-baz", "old_line": 2 }
Track old_line / new_line per file: context increments both; + increments new only; - increments old only. Hunk header resets both via @@ -A,B +C,D @@.
Binary / rename-only files: lines: [].
Resolve explanations: for each explanation in a file's explanations array, look up its start_position and end_position from the parsed lines:
side: "RIGHT": find the line where (type == "context" or type == "add") and new_line == target_line — its position is what you want.side: "LEFT": find the line where (type == "context" or type == "del") and old_line == target_line.explanations array through to the file entry in data.json.tldr and description (from features.json) straight through to the file entry in data.json — the frontend renders them as a header card above each file's diff.Final data.json shape:
{
"pr": {
"number": 969,
"title": "...",
"url": "https://github.com/.../pull/969",
"head_sha": "0468e2ed...",
"base": "main",
"author": "alice",
"additions": 7220,
"deletions": 120
},
"sequence": ["migrations", "ownership-guards", ...],
"edges": [["migrations", "ownership-guards"], ...],
"features": {
"migrations": {
"tldr": "Schema updates for group conversations and ai_enabled column",
"full_description": "Adds two new columns:\n- **`is_group`** ...\n- **`group_jid`** ...",
"blast_radius": "high",
"why_first": "Schema lands before code that reads the new columns.",
"files": [
{
"path": "backend-python/src/migrations/versions/V0079_....py",
"tldr": "Adds `is_group` and `group_jid` columns to `conversation_assignments`.",
"description": "Schema migration that introduces the two columns the rest of `group-conversations` depends on. Idempotent via `migration_tracking` so a Cloud Run cold start can replay it safely.",
"additions": 45,
"deletions": 2,
"lines": [ ... ],
"explanations": [
{
"title": "Idempotent migration guard",
"body": "Checks `migration_tracking` before writing — makes the upgrade safe to re-run on Cloud Run cold starts.",
"start_line": 22,
"end_line": 35,
"side": "RIGHT",
"start_position": 5,
"end_position": 18
}
]
}
]
}
}
}
Write to ~/.claude/pr-review/pr-<num>/data.json.
The skill ships index.html and server.py under its own templates/ directory. The skill's location depends on how it was installed — standalone (~/.claude/skills/pr-brief/) or as a plugin (~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/skills/pr-brief/). Resolve the templates directory at runtime with find, then copy the files into the PR output dir each run (so improvements to the templates propagate):
TEMPLATES_DIR=$(find ~/.claude -type d -path "*/skills/pr-brief/templates" 2>/dev/null | head -1)
[ -z "$TEMPLATES_DIR" ] && { echo "pr-brief templates not found under ~/.claude"; exit 1; }
cp "$TEMPLATES_DIR/index.html" ~/.claude/pr-review/pr-<num>/index.html
cp "$TEMPLATES_DIR/server.py" ~/.claude/pr-review/pr-<num>/server.py
Pick a free port. Default 7681; if lsof -i:7681 is busy, try 7682, 7683, ... up to 7690.
cd ~/.claude/pr-review/pr-<num>
python3 server.py --port <port> --pr <num> --repo <owner>/<repo> --sha <head_sha> &
Background the server, then open the browser:
open "http://localhost:<port>"
Output a compact summary to the terminal:
🟢 Review UI ready → http://localhost:7681
Suggested order (N features, M files):
1. migrations (6 files, high blast) — schema lands first
2. ownership-guards (13 files, high blast) — dep for controller changes
3. ...
In the UI:
• Click a feature → all its files render stacked, scroll through them top-to-bottom
• Click "+" in the gutter → inline editor; click-and-drag the "+" or shift-click for multi-line
• Save → posted to GitHub immediately (realtime); the badge flips Pending → Posted ✓ with link
• "Viewed" checkbox per file (sticky, persists per PR)
• If a post fails (network/auth), the comment stays local; "Retry N unposted" in the sidebar resends
Stop the server: lsof -ti:<port> | xargs kill
These behaviors are part of the bundled index.html and server.py. Do not regress when modifying templates:
flex: 1; min-height: 0; overflow-y: auto on .feature-list; flex-shrink: 0 on .pending-box.)localStorage keyed by pr-brief-viewed-<repo>-<num>. Marking a file viewed dims and collapses it.highlight.js 11.9 with the github-dark stylesheet. Line prefix ( / + / -) is colored separately so add/del row tints stay correct.full_description. Frontend uses marked (CDN). Bullet points, bold, inline code, and short paragraphs render as expected. The agent producing features.json MUST emit markdown for this field.file.explanations, a purple "✨" callout floats anchored to the start row of its range (data-start-position), measured via getBoundingClientRect() against the track. Pills auto-stack (sorted by start position) to avoid overlap. After layout, track.style.minHeight is set to lastBottom + 16px so pills near EOF are not clipped. Pills are collapsible (toggle button), the body is markdown-rendered through marked and resizable (CSS resize: vertical). Rows in the explanation's range get a left-border accent (box-shadow: inset 3px 0 0 #bb80ff). Pill content is narrative-driven — see step 6 of the agent task for the storytelling rules.+ in the gutter → click expands an editor row directly below. Cmd/Ctrl+Enter saves, Esc cancels.+ across lines (GitHub-native UX). Live blue band highlights the range.+ after a previous click./api/post-comment immediately, server shells gh api repos/.../pulls/<num>/comments. Switching to Batch makes Save just queue locally; clicking the sidebar submit button POSTs /api/submit-review once with all queued comments (single API call, sidesteps GitHub's secondary rate limit). Mode persists in localStorage per PR (pr-brief-mode-<repo>-<num>)./api/post-comment, /api/submit-review, /api/post-briefs) gate behind a 1.5s minimum gap (per GitHub's "≥1s between writes" guidance) via a single threading lock — even with concurrent saves, the actual gh calls are serialized.gh returns "secondary rate limit" output, the server replies HTTP 429 with {ok:false, rate_limited:true, retry_after_seconds:60, hint}. The frontend detects this, shows a "Rate limited — switching to Batch" toast, and auto-flips MODE to batch.Retry N unposted / All posted; Batch → Submit N as one review / All posted.Stdlib-only Python http.server:
GET / and /index.html → staticGET /data.json → staticGET /api/context → {pr, repo, sha} for the UIPOST /api/auth-status → runs gh auth status, returns {ok, message}POST /api/post-comment (realtime path) → body {path, body, line, side, start_line?, start_side?} → throttle 1.5s → gh api repos/<repo>/pulls/<pr>/comments → returns {ok, url, id}. On secondary rate limit returns 429 with {ok:false, rate_limited:true, retry_after_seconds, hint}.POST /api/submit-review (batch path) → body {comments: [...], summary} → throttle 1.5s → gh api repos/<repo>/pulls/<pr>/reviews (event=COMMENT) → returns {ok, url, id, count}. Used by Batch-mode submit and "Publish briefs". Same 429 contract on rate limit.POST /api/post-briefs → posts feature briefs as position: 1 comments per file via the reviews endpointAll POST endpoints expect/emit JSON.
~/.claude/pr-review/pr-<num>/.full_description must be markdown. tldr and why_first are plain text.uv.lock, package-lock.json, yarn.lock, poetry.lock, Pipfile.lock) collapse into a build or deps feature; tldr = "Auto-generated lockfile update — no manual review needed", lines: [].gh api returns 422 with stale SHA — tell the user to re-run the skill.TEMPLATES_DIR via find ~/.claude -type d -path "*/skills/pr-brief/templates", then cp "$TEMPLATES_DIR"/* …) so future template improvements propagate to existing per-PR dirs.~/.claude/pr-review/pr-<num>/ lets multiple PRs coexist.localStorage keyed per PR).gh CLI inherits the user's system auth — server just shells out, no token in the HTML.highlight.js + github-dark (syntax), marked (description / explanation markdown). No build step.Provides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.
npx claudepluginhub lucastononro/pr-brief --plugin pr-brief