explain-pr — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited explain-pr (Agent Skill) and scored it 96/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 1 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
You are tasked with helping the user understand a GitHub pull request, not review it. The goal is comprehension through visualisation and a structured, interactive tour of the diffs. You do not produce a review document. You do not score dimensions. You guide the user.
Verify gh is available with gh --version. If missing, tell the user this skill needs GitHub CLI and stop.
Detect the mode:
html (case-insensitive), set mode = html. The remaining arguments (if any) identify the PR.mode = interactive.Resolve the PR identifier from the remaining arguments:
gh pr view --json number,title,url to detect a PR from the current branch.Fetch metadata and diff:
gh pr view {number} --json number,title,url,body,author,headRefName,baseRefName,headRefOid,state,additions,deletions,changedFiles,files
gh pr diff {number}Build the permalink prefix for later use:
{owner}/{repo} from the PR URL (https://github.com/{owner}/{repo}/pull/{number}).{head_sha} from headRefOid. https://github.com/{owner}/{repo}/blob/{head_sha}/{file path}Use these permalinks everywhere a file path is mentioned to the user — scope listings, walkthrough plan stops, stop headings, HTML output — so the user can click straight to the pinned source.
In interactive mode: produce a single markdown message with these four sections, in order. Be ruthlessly concise.
In html mode: produce the same content but capture it for later substitution into the template (see Step 7). Do not print it to the user.
body field). If empty, say so.Output is rendered in a terminal, so use only monospace-friendly visualisations. Never any rendered diagram format. Pick whichever of the following help most. Aim for 1-3, not all of them.
(new) / (modified) / (deleted): controller/
├── OrderController.java (modified)
└── dto/CreateOrderRequest.java (new)
service/
└── OrderService.java (modified)──►, │, ▼). Strongly prefer this over any other visualisations unless it makes no sense at all. Annotate new or changed nodes inline: Client ──► OrderController ──► OrderService ──► OrderRepository ──► DB
│ │
▼ ▼
CreateOrderRequest DomainEvent (new)Keep every visual under ~20 lines. If a flow doesn't fit, split it or fall back to a table.
In interactive mode: follow the procedure below and confirm scoping with the user.
In html mode: follow the same procedure but do not ask the user. Make the call yourself and document it in the rendered output (the Scope section of the template).
Before designing walkthrough plans, decide whether the walk should cover every changed file or only the essence.
Most large PRs have a small essence — a few files that contain the real change — surrounded by mechanical churn (renames, generated code, mass formatting, repetitive call-site updates, lockfile changes, snapshot updates, etc.). The agent's job here is to make that distinction explicit.
Procedure:
This PR touches {N} files but the essence is in {M}. I'll focus the walk on those and skip the rest.
>
Excluded files ({count}):
>
-path/to/file.ext— generated -path/to/another.ext— repetitive call-site update - …
Show the full list of excluded files with a short reason for each. Wrap each path in a markdown link to its permalink. Do not truncate. Group by reason where it aids readability.
Only files in the essence bucket become stops in the walkthrough plans.
In interactive mode: present plans to the user and let them pick.
In html mode: design the same plans, then choose the best one yourself. The chosen plan drives the walkthrough; the others are still listed in the rendered output for reference.
Analyse the diff and design 1-3 walkthrough plans. Each plan is a linear ordered sequence of "stops" through the diff that tells a coherent story. Examples:
Each plan should:
One file does not always mean one stop. A file with a large or multi-themed change deserves several stops, each covering part of it, so the explanation tracks the change instead of collapsing hundreds of lines into a sentence or two.
For each essence file, decide whether to split it. Split when any of these hold:
When you split a file, divide its diff into segments. A segment is one or more consecutive @@ hunks that share a single theme. Group hunks by concern — read them and cluster related hunks rather than slicing mechanically by line count. Give each segment a short title (3-5 words) and order the segments to tell a coherent story (usually top-to-bottom through the file; reorder only if a later hunk explains an earlier one).
Each stop then covers exactly one of:
Do not mix whole-file and segment coverage in one stop, and never put segments from two different files in the same stop. A split file therefore contributes as many stops as it has segments, each with its own title, framing, and diff.
Present plans to the user:
AskUserQuestion tool with each plan as an option. Include the stop list in each option's description.Call this menu the walkthrough plan in all user-facing text.
This step runs in interactive mode only. In html mode, skip to Step 7.
Once the user picks a plan, walk through its stops in order. For each stop:
## Stop {n}/{total}: {short stop title} (for a segment stop, use the segment title; the file recurs across its segments, so the title is what distinguishes them).Files: [src/Foo.java]({permalink}), [src/Bar.java]({permalink})). For a segment stop, name the file once and note which part it is (e.g. File: [src/Foo.java]({permalink}) — validation hunks).diff block. Slice it from the full diff fetched in Step 1 — split on lines matching ^diff --git a/. b/. and pick the section(s) for the file(s) at this stop. If the stop covers a **segment** of a split file (see Step 4), slice that file's section further to only the segment's hunks: a hunk runs from a @@ … @@ line up to (but not including) the next @@ line or the end of the file's section, and the @@` header stays with its hunk. Do not paraphrase the diff.Choices to offer after every stop (always in this order):
Presentation rules:
AskUserQuestion with these four labelled options.1) … 2) … 3) … 4) …) and wait for a number.After 1) or 2), when the user is done, re-present the same four choices.
If the user picks 4), jump back to Step 4 and re-offer the plans (you may add a new plan if you have learned something during the walk).
When the user picks 3) on the final stop, proceed to Step 6.
This step runs in interactive mode only. In html mode, skip to Step 7.
Print a short recap:
## Recap headingThen offer to run the formal review:
Want me to run /review-pr {number} for a structured red-amber-green review?If they accept, invoke the review-pr skill with the PR number. If they decline, stop.
This step runs in html mode only.
By the end of Step 4 you have: PR metadata, the four intro sections (description+goal, plain-language summary, context, visualisations), the scope decision (essence vs excluded files), and the set of walkthrough plans with one marked as chosen.
For every stop in the chosen plan, slice the file's diff from the full PR diff fetched in Step 1 — split on lines matching ^diff --git a/.* b/.* and pick the section(s) for the file(s) at this stop.
Then load the template at {skill dir}/templates/explain-pr.html.tmpl and substitute the placeholders below. Output to:
{repo root}/.ai/review/{yyyy-mm-dd} {pr-number} {pr-title-abbreviated}.htmlWhere {pr-title-abbreviated} is the PR title in kebab-case, truncated to a max of 50 characters at a word boundary. Create .ai/review/ if it doesn't exist. If a file with that name already exists, overwrite it.
| Placeholder | Value |
|---|---|
{{NUMBER}} | PR number |
{{TITLE}} | PR title (HTML-escaped) |
{{URL}} | PR URL |
{{AUTHOR}} | PR author login |
{{HEAD_BRANCH}} | Source branch |
{{BASE_BRANCH}} | Target branch |
{{GENERATED_AT}} | Local time, format YYYY-MM-DD HH:MM TZ |
{{PR_BODY_HTML}} | PR body rendered to HTML (paragraphs, lists, code). HTML-escape any raw text. |
{{GOAL}} | One-sentence goal in your own words (HTML-escaped) |
{{SUMMARY_HTML}} | The plain-language summary as one or more <p> elements |
{{CONTEXT_HTML}} | Context bullets as a <ul> |
{{VISUALISATIONS_HTML}} | Visualisations as <table>, <pre>, etc. (see below) |
{{SCOPE_SECTION_HTML}} | Scope section markup, or empty string if mechanical bucket was empty (see below) |
{{PLANS_HTML}} | Markup for plans block (see below) |
{{WALK_HTML}} | Markup for the chosen plan's stops with diffs (see below) |
{{RECAP_HTML}} | 3-6 recap bullets as a <ul> |
Use the same visualisation types as Step 2.4 but emit native HTML:
<table><thead>…</thead><tbody>…</tbody></table><pre>…</pre> (preserve whitespace exactly)<h4> per type with <ul> of fields and relationshipsNo Mermaid or other rendered diagram formats — <pre> blocks render correctly in any browser.
If the mechanical bucket from Step 3 is empty, set {{SCOPE_SECTION_HTML}} to an empty string.
Otherwise emit (wrap every <code>{path}</code> in an <a href="{permalink}">…</a>):
<section id="scope">
<h2>Scope</h2>
<p>This PR touches {N} files but the essence is in {M}. The walk focuses on those and skips the rest.</p>
<details>
<summary>Excluded files ({count})</summary>
<ul>
<li><a href="{permalink}"><code>{path}</code></a> — {reason}</li>
...
</ul>
</details>
</section><p>Chosen for this document: <span class="badge chosen">{chosen plan name}</span> — {chosen plan description}</p>
<details>
<summary>All candidate plans ({count})</summary>
<ul>
<li><strong>{plan name}</strong> — {description}
<ol>
<li>{stop title} — <a href="{permalink}"><code>{file path}</code></a></li>
...
</ol>
</li>
...
</ul>
</details>If only one plan was designed, omit the <details> block and just show the chosen-plan badge line.
One <section class="stop"> per stop in the chosen plan. Inside each stop, emit one `<details class="diff-wrapper">` per file for a whole-file stop, or one `<details>` for the segment for a segment stop (see Step 4). Never put more than one file — or more than one segment — in a single <details>. This gives natural visual separation, lets the user expand only what they care about, and avoids confusion about where one diff ends and the next begins.
Because a split file contributes several stops, each segment lands in its own <section class="stop"> with its own heading, framing, and diff — exactly like any other stop. The file's permalink recurs across those sections; the segment title is what tells them apart.
Every <details> stays closed by default so large PRs feel less intimidating.
The <summary> shows the file as a permalink plus an additions/deletions count. For a segment stop, add a <span class="diff-seg"> with the segment title so the user can tell which part of the file it is:
<!-- whole-file stop -->
<section class="stop" id="stop-{n}">
<h3>Stop {n}/{total}: {stop title}</h3>
<p class="stop-frame">{one-sentence framing}</p>
<details class="diff-wrapper">
<summary><a href="{permalink}"><code>{file path}</code></a> <span class="diff-stats">+{additions} −{deletions}</span></summary>
<pre class="diff" data-lang="{lang}"><code>{wrapped diff lines for this file}</code></pre>
</details>
...
</section>
<!-- segment stop (one of several for the same split file) -->
<section class="stop" id="stop-{n}">
<h3>Stop {n}/{total}: {segment title}</h3>
<p class="stop-frame">{one-sentence framing of this segment}</p>
<details class="diff-wrapper">
<summary><a href="{permalink}"><code>{file path}</code></a> <span class="diff-seg">{segment title}</span> <span class="diff-stats">+{additions} −{deletions}</span></summary>
<pre class="diff" data-lang="{lang}"><code>{wrapped diff lines for this segment's hunks only}</code></pre>
</details>
</section>Slice a segment's hunks exactly as in Step 5: a hunk runs from a @@ … @@ line up to (but not including) the next @@ line or the end of the file's section, and the @@ header stays with its hunk. As a refinement, point the segment's permalink at the changed lines by appending a line anchor for the new-file side — #L{start} (or #L{start}-L{end}), where {start} is the new-file start line from the segment's first @@ -a,b +start,len @@ header and {end} is start + len − 1 of its last hunk.
Count {additions} and {deletions} over only the lines in that `<details>` — the file's slice for a whole-file stop, or the segment's hunks for a segment stop: additions are lines starting with + (excluding +++), deletions are lines starting with - (excluding ---).
First, drop every file-header line from the slice — any line starting with diff --git, index , --- , or +++ . These only repeat the file path, which the <summary> already shows as a clickable permalink, so they add noise and nothing else. The rendered diff starts at the first @@ hunk header.
For each remaining line of the per-file diff slice, emit a <span class="line ..."> element that carries two line-number gutters (old file, new file) followed by the line content — the same gutter layout GitHub shows. Concatenate all line spans inside the <pre class="diff"><code>...</code></pre> with no line breaks between them — each has display: block via CSS.
Each line span has this internal shape:
<span class="line {class}"><span class="ln ln-old">{old}</span><span class="ln ln-new">{new}</span><span class="lc">{escaped content}</span></span>{escaped content} is the verbatim line (leading +/-/space kept) with & → &, < → <, > → >.user-select: none in CSS, so copying the diff yields only the code, never the line numbers.Computing the gutter numbers. Track two counters, old and new. Reset them at every hunk header: a header @@ -{oldStart},{oldLen} +{newStart},{newLen} @@ sets old = oldStart and new = newStart. Then per line:
| Leading characters | Class | ln-old | ln-new | After emitting |
|---|---|---|---|---|
@@ | line hunk | empty | empty | reset old/new from header |
| Any other (context) | line | old | new | old++, new++ |
+ (not +++) | line add | empty | new | new++ |
- (not ---) | line del | old | empty | old++ |
Always emit both <span class="ln …"> elements even when empty, so columns stay aligned.
Example:
<span class="line hunk"><span class="ln ln-old"></span><span class="ln ln-new"></span><span class="lc">@@ -10,7 +10,9 @@ public class OrderController {</span></span><span class="line"><span class="ln ln-old">10</span><span class="ln ln-new">10</span><span class="lc"> public ResponseEntity<Order> create() {</span></span><span class="line del"><span class="ln ln-old">11</span><span class="ln ln-new"></span><span class="lc">- return ResponseEntity.ok(service.createDefault());</span></span><span class="line add"><span class="ln ln-old"></span><span class="ln ln-new">11</span><span class="lc">+ return ResponseEntity.ok(service.create(request));</span></span>The template bundles highlight.js inline (no network, single file). It highlights at view time, so you do not emit any token markup — keep emitting lines exactly as in "Diff line wrapping" above. Your only job is to set the data-lang attribute on each <pre class="diff" data-lang="{lang}">.
{lang} is a highlight.js language id derived from the file's extension. Map common ones:
| Extension(s) | {lang} |
|---|---|
.java | java |
.kt, .kts | kotlin |
.js, .mjs, .cjs | javascript |
.ts, .tsx | typescript |
.py | python |
.go | go |
.rs | rust |
.rb | ruby |
.sql | sql |
.sh, .bash | bash |
.yml, .yaml | yaml |
.json | json |
.xml, .html | xml |
.css | css |
.md | markdown |
For an extension not listed, use the matching highlight.js id if you know it, otherwise omit `data-lang` entirely — the embedded script skips any <pre> without it (and any unknown id), leaving the diff colouring intact. The script highlights only .lc content on non-hunk, non-meta lines, and never touches the gutters or the leading +/-/space marker.
Print one line to the user with the absolute path of the file you created. Do not offer /review-pr and do not print any of the document content.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.