| name | review-html |
| description | Run a deep, didactic, citation-backed PR review and emit a single self-contained HTML report. Use when the user invokes `/review-html <PR>` or asks for an HTML review of a GitHub PR. |
HTML PR Review
You are orchestrating a 6-pass pipeline that produces a beautiful, self-contained HTML PR review for the reviewer to open in a browser. The reviewer is also learning from each finding, so didactic depth, long-form discussion, and verified citations are load-bearing.
The pipeline at a glance:
- Triage — subagent (haiku) classifies tier + risk surfaces from PR metadata.
- Review — main thread does the actual review work using the bundled doctrine at
references/review-doctrine.md. Produces findings.
- Didactic — per-finding subagents (haiku, parallel) write the nutshell / principle / why-it-matters trio.
- Discussion — per-finding subagents (sonnet, parallel) write long-form Markdown: context / when-this-matters-less / alternatives / going-deeper.
- Citation — per-finding subagents (haiku, parallel) fetch a primary source and return a verbatim quoted span, or
null.
- Render — deterministic Python (
render.py): Jinja template + Pygments + markdown-it. No LLM.
Subagents do the heavy work; the main thread orchestrates and holds light structured state. Voice: senior engineer who wants the reader to learn — confident on the principle, generous with the reasoning, collaborative on the application.
Inputs
The user invokes:
/review-html <PR-URL-or-number>
Parse the argument. If it's a number, run gh pr view <num> in the current repo to confirm the PR. If it's a URL, use it directly.
Pipeline
Execute these passes IN ORDER. Passes 3, 3b, and 4 all fan out in parallel after the review pass completes.
Pass 1 — Triage (subagent, model: haiku)
Dispatch ONE general-purpose subagent with the contents of prompts/triage.md and the PR reference. The subagent uses gh to fetch metadata and returns a single JSON object to stdout. Capture this JSON.
Agent(
description="Triage PR <num>",
subagent_type="general-purpose",
model="haiku",
prompt="<contents of ~/.claude/skills/review-html/prompts/triage.md>\n\nPR_REF: <the PR URL or number>\n\nPrint only the JSON object."
)
Parse the returned JSON. It populates pr, triage, and file_tree keys of the pipeline JSON.
Pass 2 — Review (YOU, the main thread)
This is the most important pass. Read references/review-doctrine.md (bundled in this skill) in full and follow it — it carries the complete review philosophy (triage tiers, premise check, lenses, severity bar, stop rules, anti-patterns, voice). This skill is self-contained: you do NOT need the external reviewing-pull-requests skill installed.
Optional enhancement: if the target repo ships its own pr-review skill (repo-specific conventions), layer it on top of the bundled doctrine. If the external reviewing-pull-requests skill is installed and newer, you may prefer it — but the bundled copy is sufficient on its own.
The doctrine covers the full method. The operational shape for this pass:
- Context first (per the doctrine's "Context first" section) — read README/CLAUDE.md, skim deps,
git log, gauge the author. Severity is contextual.
- Fetch the diff:
gh pr diff <num> (or against the URL).
- Read the PR description (already in the triage output).
- Premise check → populates
premise_check.
- Predict-then-diff → populates
predict_vs_diff.
- Lenses in order, skipping what doesn't apply: data model & shape; module boundaries & contracts; scope; failure modes; tests.
- Walk one call site for any new public function.
- Run it locally if behavior can't be inferred; record gaps in
did_not_verify.
Findings follow the doctrine's severity bar:
must-fix requires: concrete failure mode, code path, minimal fix.
should-fix requires: stated risk.
nit and question are optional and labeled.
Produce this JSON (in your head or as a scratch file — does not need to be a single emitted block):
{
"premise_check": { "verdict": "ok | fail", "notes": "..." },
"predict_vs_diff": {
"expected": ["..."],
"actual": ["..."],
"surprises_present": ["..."],
"surprises_missing": ["..."]
},
"findings": [
{
"id": "f1",
"file": "src/...",
"line": 42,
"severity": "must-fix | should-fix | nit | question",
"claim": "...",
"failure_mode": "...",
"minimal_fix": "...",
"code_excerpt": "<the actual diff hunk or relevant code>",
"code_language": "python | typescript | ...",
"needs_didactic": true,
"needs_citation": true,
"citation_target": "concrete and searchable, e.g. 'OWASP A03:2021 — Injection' or 'RFC 6749 §10.12'"
}
],
"did_not_verify": ["short bullets"]
}
Boolean heuristics:
needs_didactic: true for every must-fix and should-fix. False for nit and question.
needs_citation: true when the finding references a named external concept (CVE class, RFC, documented language/library behavior, OWASP item, well-known anti-pattern). False when the finding is project-internal.
When unsure, do NOT inflate severity. A weak must-fix is worse than a strong should-fix.
Pass 3 — Didactic (subagents, parallel, one per finding where needs_didactic)
For each finding with needs_didactic: true, dispatch a subagent in parallel:
Agent(
description="Didactic block for <finding.id>",
subagent_type="general-purpose",
model="haiku",
prompt="<contents of ~/.claude/skills/review-html/prompts/didactic.md>\n\nFINDING: <the finding as JSON>"
)
Dispatch ALL didactic subagents in a single message (parallel). Capture each returned JSON. Merge each {nutshell, principle, why_it_matters} into the matching finding by id.
Pass 3b — Discussion (subagents, parallel, one per finding where needs_didactic)
This is the long-form deep-dive that sits at the end of each finding — the "take out all your doubts" block. Dispatch ONE subagent per finding that earned a didactic block, in parallel with Pass 3 if you like (they're independent):
Agent(
description="Discussion block for <finding.id>",
subagent_type="general-purpose",
model="sonnet",
prompt="<contents of ~/.claude/skills/review-html/prompts/discussion.md>\n\nFINDING: <the finding as JSON>\n\nPR_CONTEXT: <title, description-summary, head_sha, files-touched count, repo-name>"
)
Use sonnet (not haiku) for this pass — the discussion needs more reasoning depth than the per-finding didactic. Each subagent returns {id, discussion_md}. Merge by id.
Pass 4 — Citation (subagents, parallel, one per finding where needs_citation)
For each finding with needs_citation: true, dispatch a subagent in parallel:
Agent(
description="Citation for <finding.id>",
subagent_type="general-purpose",
model="haiku",
prompt="<contents of ~/.claude/skills/review-html/prompts/citation.md>\n\nFINDING: <the finding as JSON>"
)
The citation subagents have WebFetch and WebSearch via the general-purpose agent. Each returns either {id, citation: {...}} or {id, citation: null}. Merge by id.
Pass 5 — Render (Python, no LLM)
-
Assemble the full pipeline JSON. Required shape:
{
"pr": {...},
"triage": {...},
"file_tree": [...],
"premise_check": {...},
"predict_vs_diff": {...},
"findings": [
{
"...finding fields...": "...",
"didactic": {"nutshell": "...", "principle": "...", "why_it_matters": "..."} or null,
"discussion_md": "### Why this matters in your context\n\n... markdown ..." or null,
"citation": {"url": "...", "source_title": "...", "quoted_span": "..."} or null
}
],
"did_not_verify": [...],
"pass_log": [
{"pass": "triage", "duration_s": <int>, "notes": "..."},
{"pass": "review (main thread)", "duration_s": <int>, "notes": "..."},
{"pass": "didactic (N findings)", "duration_s": <int>, "notes": "..."},
{"pass": "discussion (N findings)", "duration_s": <int>, "notes": "..."},
{"pass": "citation (N attempted)", "duration_s": <int>, "notes": "<verified>/<attempted>"},
{"pass": "render", "duration_s": 0, "notes": ""}
]
}
render.py automatically attaches per-finding markdown_payload (for the "Copy as GitHub comment" buttons) and a review_markdown field at the top level (for the "Copy all must-fixes as one GitHub review" bulk-copy). It also passes each discussion_md through markdown-it → HTML on render. You do not produce these fields.
-
Write the pipeline JSON to a temp file: /tmp/review-html-<pr-number>.json.
-
Run the renderer:
~/.claude/skills/review-html/.venv/bin/python \
~/.claude/skills/review-html/render.py \
--input /tmp/review-html-<pr-number>.json \
--output ~/.claude/reviews/PR-<owner>-<repo>-<number>-<YYYY-MM-DD>.html \
--cwd "$(pwd)"
(Create ~/.claude/reviews/ first if it does not exist.)
-
Open the file:
open ~/.claude/reviews/PR-<owner>-<repo>-<number>-<YYYY-MM-DD>.html
-
Report the output path to the user.
Hard rules
- No paraphrased citations. Citations must come from the citation subagents, which produce verbatim quoted spans only. If a citation subagent returns
null, do NOT invent one.
- No PR-summary prose. Do not add a recap paragraph anywhere.
- No drive-by refactor demands. Findings must trace to the stated goal of the PR.
- No empty sections. If a pass produces nothing for a finding, the renderer omits that section — you do not need to emit
null placeholders, but if you do, the renderer handles them.
- Honest gaps. Whatever you did not verify goes into
did_not_verify literally. The footer is mandatory for standard and high-risk tiers.
What the renderer currently surfaces
The current template renders:
- A minimal top strip: repo · #PR · short SHA · italic title → links to GitHub.
- A 3-pane findings shell (rail + main + marginalia at ≥1400px):
- Rail (sticky): section eyebrow, ledger of counts with coloured dots, severity filter chips, numbered finding list with severity dots + roman claim summaries, bulk "Copy N must-fixes as review" button, J/K/C keyboard hint.
- Main pane: each finding as a card with severity-coloured left stripe, big severity-coloured number, outlined severity pill, italic Fraunces claim, mono file:line link, "Copy as GitHub comment" button, failure-mode body, syntax-highlighted code excerpt, minimal-fix block, Learning lens (didactic, teal stripe), The longer answer (discussion, collapsible, rendered from Markdown), citation (red stripe, verbatim pull-quote, source URL).
- Marginalia (≥1400px only): PR identity (link, author, head SHA, reviewed timestamp), Focused citation (scroll-syncs to current finding), Limits of this review (first 4 entries from
did_not_verify).
- Did-not-verify section at the bottom (hidden at ≥1400px because the marginalia owns it).
- Colophon: skill version, generated timestamp, pipeline trace (collapsible audit table).
The pipeline JSON still carries triage.tier, triage.risk_surfaces, premise_check, predict_vs_diff, and file_tree. These are computed by the triage subagent and the review pass and should still be produced even though the surface omits them today. They live in the pipeline JSON at /tmp/review-html-<pr-number>.json and re-enabling their rendering is a template-only change.
Do not skip the orientation work just because it isn't visible. Premise check + predict-then-diff are part of the review doctrine; they sharpen the findings whether or not they ship to the page.
Reviewer-facing affordances baked into the page
- Severity filter chips (in rail) — hide everything except a single tier; chips colour-fill on press.
- J / K — step between visible findings.
- C — copy the focused finding as a GitHub-ready Markdown comment.
- Per-finding "Copy as GitHub comment" button — clipboard copy with confirmation.
- "Copy all must-fixes as one GitHub review" button — single Markdown blob with header + all must-fixes separated by
---.
- Click the number (e.g.
01) — sets a #fN permalink and target-flashes the card.
- Print stylesheet — Ctrl/Cmd-P renders cleanly as a PDF (action buttons hidden, paper background, deduplicated whitespace).
Defaults
- Output directory:
~/.claude/reviews/.
- Filename:
PR-<owner>-<repo>-<number>-<YYYY-MM-DD>.html.
- Open the HTML automatically when done (
open on macOS).