prepare-review — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited prepare-review (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
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.
Draft a description whose job is to convey why the change exists and how it addresses the current problem. The proposed code stands on its own — the diff shows what changed; the description supplies the reason. The rest routes reviewer attention in order of criticality so they get maximum value from whatever time they can spend.
Audience assumption — ELI5 / assume unfamiliarity. Write for a competent developer who has never seen this system. Explain what it does today and why this change exists in plain language; spare a sentence or two to establish the business/system context up front — that investment is almost always worth the words. Skip the parts the diff already speaks to (which loop does what, which file moved where).
Default to terse on everything else. Justifications, hedges, asides, and "we used to / now we" framing add bytes without adding signal. Trim aggressively on the first pass; reviewers will ask for more if they want it. The shape to aim for: a Context section that earns its 30-60 seconds, then a tight Review guide. Recency-polish bullets, decisions no one was going to question, and author-todo lists all belong somewhere else — see Step 3 "What to avoid".
CR = change request: a pull request on GitHub, a merge request on GitLab. Pick the forge tool by the origin remote.
%%{ init: { 'look': 'handDrawn' } }%%
flowchart TD
Start(["/prepare-review"]) --> CR{Open CR?}
subgraph "Step 1: Gather the changeset"
CR -->|No| MakeCR["Open draft CR"]
CR -->|Yes| Behind
MakeCR --> Tack["Link to tack (optional)"]
Tack --> Behind{Behind main?}
Behind -->|Yes| DoRebase["Rebase + force-with-lease"]
Behind -->|No| StateCheck
DoRebase --> StateCheck["Sanity-check vs CR head"]
end
subgraph "Step 2: Resolve questions"
StateCheck --> Why["Ask the WHY + open decisions"]
end
subgraph "Step 3-4: Draft & write"
Why --> Recency["Anti-recency check"]
Recency --> Draft["Draft Context + Review guide"]
Draft --> Out{Disposition?}
Out -->|Write| Forge(["Push to CR"])
Out -->|Copy| CopyOnly(["Print for paste"])
Out -->|Edit| EditCh{moor?}
EditCh -->|Yes| Moor["One-off moor review"]
EditCh -->|No| Recency
Moor --> Recency
endThe reviewer reviews A → B — the net change from base to final state — not the path you took to get there. The minor pivots, dead ends, and intermediate iterations of this session are development process, not the change under review. This skill's recurring failure is narrating that process — to the user, or into the description — instead of just producing the artifact that describes A → B.
Step 1's recon and Step 4's diff/moor review each fold into one call precisely so there is nothing to narrate between them: run the call, read the result, move on.
The entire visible output of a run is:
BEHIND, a STATE mismatch, a CR_CREATE_ERROR);Everything else is internal — keep it out of the visible output:
Reserve prose for the steps that need your judgment or the user's input — the prompts in Step 2, drafting in Step 3, presenting options in Step 4. Those produce the artifact; narration about producing it does not.
Run the gather script once. It performs Step 1's deterministic recon and the safe default-path setup — detect the forge, resolve or auto-open the draft CR, count the gap to the default branch, capture the current description as the Step 4 diff baseline, check local state against the CR head, read the project template and anchor.* config — then prints one KEY=value block on stdout:
bash "${CLAUDE_PLUGIN_ROOT}/scripts/prepare-review.sh"Read the block and act only on what it surfaces; don't re-run the individual probes. The keys:
| Key | What to do with it |
|---|---|
FORGE | github / gitlab picks the CLI for the rest of the skill; none → the URL-free skip-deep-links path |
DEFAULT_BRANCH | substitute for main in the diff/log commands below |
ON_DEFAULT_BRANCH=1 | HEAD is the default branch — no CR; the no-commits-ahead check below stops the run |
AHEAD=0 | nothing ahead of the default branch — say so and stop |
BEHIND=<n> | >0 → run the rebase dialog below |
CR_URL / CR_IID | the resolved or freshly-opened draft — deep-link target and write target (empty on skip-deep-links) |
CR_DRAFT | gates the post-rebase force-push (see below) |
STATE | match → proceed; anything else → surface and stop (see "Act on STATE") |
CURRENT_DESC_PATH | baseline the Step 4 diff reads (empty on skip-deep-links) |
TEMPLATE_PATH | project CR template to compose into (Step 3) |
ANCHOR_CONFIG | anchor.* keys to apply (Step 3), as JSON |
FILE_ANCHORS | precomputed sha1(path) per changed file for GitLab deep links (Step 3), as JSON |
If the block carries a CR_CREATE_ERROR=… line, the draft-open hit an auth or push failure — surface it and ask the user to refresh credentials; do not fall back to the URL-free path (the fail-fast-on-auth rule).
Why auto-open is the default. A draft CR is cheap and reversible: it requests no review, the push already triggered any branch-level CI, and self-assign notifies only you. The deep links are the load-bearing part of the description, and a placeholder-only draft is broken on arrival — opening the real CR first is what makes the description useful. The script does not sniff for a "merges direct to main, never opens CRs" convention, because there's no reliable signal for it. Two cases give way to the skip-deep-links path:
main without CRs, or the CLI's default forge instance is wrong for this repo. Re-run with --no-open to proceed URL-free; or, if they'd rather open the draft themselves, pause until they confirm one is open, then re-run so the script resolves its URL.tack tracks work-in-progress as routes and tacks. The integration is optional — if tack isn't on PATH, skip this section silently. When it is present and a route resolves (a pinned .tack file, or tack find <CR_URL> --json matches), record the CR as the tack's deliverable so the WIP tracker links to the review:
tack deliverable <slug> <tack-id> "MR" "<CR_URL>" # "PR" on GitHubWhen the CR represents the completion of that unit of work, offer to mark the tack done:
Mark tack<slug>/<tack-id>done?[yes / no]
On yes, run tack done <slug> <tack-id>. Leave it open if more work on the tack is expected after review.
BEHIND > 0BEHIND=0 → skip this section. Otherwise the branch needs origin/<default> before it can merge — every conflict with intervening commits has to be resolved before the CR can land, and doing it now (while the change is fresh) is cheaper than after review when context has gone cold. Secondary: deep links anchor to lines in the current diff, so a behind-default branch points at content that won't compose cleanly at merge time. Ask:
Branch is<BEHIND>commits behindorigin/<default>. Rebase now?[yes / skip]
yes — run git rebase origin/<default>. On conflict, resolve in place: read both sides of each conflicted region, pick the resolution that preserves the intent of both changes (not just one side), git add the resolved files, then git rebase --continue. Loop until the rebase completes. Surface to the user when intent is genuinely ambiguous — two competing changes to the same logic, semantic conflicts the textual markers don't show, a rename colliding with an edit. Don't guess in those cases; show the conflict and ask. If a hook fails mid-rebase, surface the failure rather than retrying with --no-verify.skip — proceed with the current branch state. Note that deep links may render against lines that have shifted by merge time.A rebase rewrites history, so the push that follows is a force-push. Gate it on CR_DRAFT — the author's declared review state, which is reliable in a way that inferred engagement signals (note counts, reviewer lists) are not:
`CR_DRAFT=true` — mutable history is the norm (anchor opens CRs as drafts for exactly this reason). Rebase and force-push with lease without further ceremony.
`CR_DRAFT=false` (ready) — a reviewer may already be looking, and there's no reliable signal for whether they have. Force-pushing over commits they've seen destroys their "changes since you last looked" diff and marks inline threads outdated. Engagement signals are advisory context for the prompt (reviewers / discussion count via glab api projects/:fullpath/merge_requests/<CR_IID> | jq '{reviewers, user_notes_count}' or gh pr view --json reviews,reviewRequests,comments), but the decision is the user's — ask before proceeding:
This CR is marked ready. Rebasing now force-pushes over commits a reviewer may have seen, which resets their incremental diff. Rebase anyway? [yes / skip]After a successful rebase (and the review-activity check above), force-push with lease so the open CR updates to the rewritten history:
git push --force-with-lease--force-with-lease rejects the push if anyone else has pushed to the branch since you last fetched — that's the safety against clobbering a coworker's commit. If it rejects, fetch, inspect, and ask the user before escalating. If the rebase itself aborts (uncommitted changes blocking it, a rebase already in progress, missing remote), surface the error and stop.
Substitute DEFAULT_BRANCH from the block for main:
git log main..HEAD --onelinegit diff main...HEAD --statgit diff main...HEAD(AHEAD=0 already told you to stop if nothing is ahead.)
STATEThe deep links you'll generate point at specific lines of the current CR diff, so drafting against stale state ships a description that renders against content the reviewer can't see. STATE=match → proceed. Otherwise stop and surface — do not draft:
LOCAL_HEAD_SHA vs CR_HEAD_SHA) and ask.State drift between conversation belief and repo reality is silent and expensive. The script's read-only state check catches it; missing it ships a broken description.
The description needs the author's motivation and any decisions still in flux. If something would otherwise come out of the description as a hedge or an open offer ("happy to bump version if you'd like", "open to adding a test", "could split this into two CRs"), it's a question — ask it now, then draft. The CR description is not a place to negotiate. Any ambiguity is a reason to defer drafting, not park inside it.
Before drafting, scan for these common ambiguities and ask the user about each one that applies:
Wait for answers to all of them before drafting. A description shipped with parked questions is worse than one shipped a turn later.
If the only open item is the WHY, ask:
What problem does this solve, and why does it matter?
>
The diff shows what changed — I need you to tell me why. A sentence or two is enough. For example: - "Users were getting 500 errors when their session expired mid-checkout" - "We need to support the new billing API before the March deadline" - "The old approach couldn't scale past 10k concurrent connections"
TEMPLATE_PATH from Step 1's block names the project's CR template (.gitlab/merge_request_templates/*.md or .github/pull_request_template.md); empty means none. When set, it's the team's required scaffolding — compose into it, don't replace it. Fill the sections it defines, preserve its checklists and headings verbatim, and supply anchor's prose where it leaves prose to the author; on a structure conflict the team template wins. The composition rules are documented in the "Honoring a project's forge template" section of templates/cr-description.md.
anchor.* configANCHOR_CONFIG from Step 1's block holds the project + global anchor.* keys as JSON ({} when none). The keys come back lowercased (anchor.reviewbudgetmins); match them case-insensitively. Apply the keys relevant to a CR description; absent keys keep anchor's defaults — never invent a value:
<base-uri><id> from a bare id. No mention, no link — don't scrape the branch or prompt.origin remote: use mrRules / prRules when set, else fall back to crRules.See guides/configuring.md for the full key set.
Recency bias is the dominant failure mode here: detail you spent the last hour polishing carries disproportionate weight in working memory and anchors the Context section even when the CR is about something much larger. *The headline is what the branch was for — usually the first commit, not the last.* Mechanical fix:
Run this check internally — it shapes what you draft, but the disposition list itself is session-internal scratch, not output. Do not print the "Centerpiece / Footnote / Cut" rundown to the user; the only thing they see from this step is the resulting draft. (See Execute quietly at the top.)
A concise imperative phrase (under 72 characters) that captures the change. Same rules as a good commit subject line.
Draft the description following the section template in templates/cr-description.md: Context, Review guide, Approach & trade-offs (rare), Testing (rare), and Validation (shared components only). The template owns the shape — which sections, in what order, and what each is for; the guidance below owns the technique for realizing it.
Deep-link construction (Review guide). Always deep-link to the actual line, not just the file — reviewers should be one click away from the hunk you're pointing them at. Construction differs by forge:
<CR_URL>/diffs#<file-anchor>_<old-line>_<new-line> where <file-anchor> is sha1(<repo-relative-file-path>) — already computed per changed file in FILE_ANCHORS from Step 1's block. (You still pick the line numbers; only the path-hash is precomputed.) For a file-level link (no specific line), just use <CR_URL>/diffs#<file-anchor>. For pure additions, use the new line number for both <old-line> and <new-line> — the link still resolves.<CR_URL>/files#diff-<file-anchor>R<new-line> (or L<new-line> for the left/old side). The <file-anchor> for GitHub is sha256(<file-path>) — gh doesn't expose it directly, so fall back to opening the CR's "Files changed" tab and copying the link from the line-number gutter when in doubt.Validation — ask, don't guess (shared components). The Validation section applies when the change is to a shared component consumed by other repos. Detect it from these signals: shared-component repo path (terraform-modules/, libraries/, base-configs/), no direct deploy pipeline of its own, version exposed as a git ref or semver tag pinned by other repos. Skip when the change is to a deployable service or UI app — it ships its own production validation.
When the signals fire, ask the author what validation looks like rather than emitting a guessed checklist row:
What does validation look like for this change? Which downstream consumer (or representative sandbox) did you exercise it against, and what did you observe?
Record their answer as the evidence row shown in the template's Validation section. The diff shows what the change says; the rendered artifact shows what it renders; the author's validation evidence tells the reviewer what the change does in composition with the consumers that actually exercise it.
Conversational and informal. Reviewers are colleagues, not stakeholders — write like you'd talk through the change at a desk, not like a status report. Sentence fragments are fine. Mid-thought asides in parens are fine. Don't sweat capitalization on tier labels and short bullets, and don't sweat trailing punctuation on fragments — core change, lives here reads as well as Core change, lives here. and a closing period on a one-line bullet adds nothing. Save the more formal register for the Why paragraph where context actually matters; everywhere else, default low-friction.
Presentation is a primary concern, not a finishing pass. Before drafting, ask: what shape is this data, and what visualization fits it? The right choice makes information land in seconds; the wrong one buries it in prose that reviewers skim past. Pick deliberately — a diagram that doesn't match the data shape is worse than no diagram, and a flat markdown table for tree-shaped data is worse than a brief HTML table.
The menu, ordered by data shape:
| Data shape | Use this | When |
|---|---|---|
| Linear story | Paragraphs | Motivation → consequence → next step. The default for Context. |
| Flat tabular | Markdown table | Lists with consistent columns: changed flags with old/new values, verification rows with Run / Status columns, affected services with owners. |
| Process / shape / interaction | Mermaid diagram | State machine (stateDiagram-v2), service sequence (sequenceDiagram), decision/data flow (flowchart TD), type relationships (classDiagram). |
| Tree-shaped / hierarchical | Inline HTML table with `rowspan` / `colspan` | A parent record with multiple child records sharing parent attributes; a per-environment matrix where one row spans envs. Markdown tables can't represent 2D nesting — drop into raw HTML (<table><tr><td rowspan="2">…). Markdown renderers accept raw HTML; use it where it's the right tool. |
| Structural change | Before/after with highlights | Added input, swapped algorithm, new flow. Two mermaid blocks under explicit ### Before / ### After headings, with classDef highlighting the changed nodes. |
| Visual / UX change | Annotated screenshots (preferred) or Before/After markdown table | Component, CSS, template, or layout changes — anything where the rendered output is what the reviewer needs to evaluate. |
Pick once and commit. If two visualizations would each carry the data, prefer the more compact one — reviewers stop reading when they run out of time, not when you run out of content.
After visualization choice, lean into markdown for the surrounding prose:
**Why:**, **Note:**).Backticks for every code identifier, file path, env var, branch name, package, or CLI flag. Anything a reader might grep for or paste into a terminal earns backticks. Inline backticks are cheap and pay back hugely in skim-readability.!148, #42), commit SHAs (a553528), and user @mentions (@chris) when they appear as bare text. Wrapping them in backticks turns them into inert code spans and kills the link. Write !148, not ` !148 ; write a553528, not a553528 `.##, ###) to chunk the description so reviewers can jump straight to "Approach" or "Testing".Collapsible sections — fold heavy detail away to keep the outline brief. Brevity is the default, but some changes genuinely carry detail a reviewer occasionally wants and shouldn't have to scroll past to skip: a full terraform plan, a long log excerpt, an exhaustive flag-by-flag table, a verbose config sample. Wrap that detail in a <details><summary>…</summary> block. The summary line keeps the outline scannable; the detail is one click away when a reviewer wants it. This is the escape valve that lets the description stay terse and complete — reach for it instead of either inlining a wall of output or dropping the detail entirely. Both GitHub and GitLab render the HTML; GitLab's markdown reference documents the syntax. The one gotcha: markdown inside the block renders only with a blank line after </summary> and before </details> — without it, tables and code fences show as raw text.
<details>
<summary><strong>Full <code>terraform plan</code> output</strong></summary>
</details>Mermaid diagrams. A small mermaid diagram earns its keep when the change has shape that prose hides — a state machine, a sequence between services, a before/after architecture, a decision flow. A picture lands faster than two paragraphs of prose. Pick the type that matches the content:
stateDiagram-v2 for state transitionssequenceDiagram for service or actor interactionsflowchart TD for decision/data flowclassDiagram for type relationshipsKeep diagrams tight (5–10 nodes — a diagram you have to scroll is worse than no diagram). Follow these mermaid conventions: hand-drawn look (%%{ init: { 'look': 'handDrawn' } }%%), no \n or <br> in node labels (use separate nodes or shorter labels instead).
Before/after with highlights. When the change has a structural shape — added input, swapped algorithm, new flow — a before/after diagram is worth the extra lines. Use two separate fenced mermaid blocks under explicit `### Before` and `### After` markdown subheadings — not two subgraph blocks inside a single mermaid block. Mermaid renders subgraphs in non-deterministic order, so a single-block "Before / After" can render with the After above the Before on GitLab. Explicit subheadings guarantee ordering. Highlight the new or changed nodes with a classDef so the reader's eye lands on the difference:
### Before
%%{ init: { 'look': 'handDrawn' } }%% flowchart LR A["input"] --> X["process"] X --> O["output"]
### After
%%{ init: { 'look': 'handDrawn' } }%% flowchart LR A["input"] --> X["process"]:::changed New["new input"]:::changed --> X X --> O["output (now scoped)"] classDef changed fill:#fff59d,stroke:#f57c00,stroke-width:3px,color:#000
Use ### when Before/After fits under an existing ## section; use ## when it stands alone. Keep both diagrams tight — if the After grows several new inputs, collapse them into a single labeled node when their individual identity isn't load-bearing (e.g. salt = env|app instead of three separate nodes for env, app, salt).
Visual / UX changes — screenshots. When the diff touches rendered output, prose can't show the reviewer what changed. Capture screenshots before drafting Context — for UX-driven CRs they're usually the centerpiece, not a footnote.
.css/.scss, component files (.tsx/.jsx/.vue/.svelte), templates (.html/.erb/.razor), design tokens, or asset files (icons, images, fonts).browser_navigate, browser_take_screenshot). For an isolated component the app can't easily mount, a frontend-design skill can compose a representative view to screenshot.<style> element via browser_evaluate with !important opacity / display rules, take the shot, then navigate away to reset (the <style> element doesn't survive route changes). The same technique scales to focused crops: hide non-target siblings with a temporary display: none !important class so the screenshot bounds tightly around the area of interest.git worktree add at main), screenshot the same views, restore your branch. Match viewport size and route so the only delta is the change under review. | Before | After |
|--------|-------|
|  |  |Screenshots are local files; the markdown references them by path. After pasting the description into the forge web UI, drag and drop each PNG into the editor — GitHub/GitLab uploads and rewrites the path to a hosted URL automatically.
Categories of cruft. If something fits one of these, it doesn't belong in the description.
now-deprecated, previously, formerly, the old, we used to, used to be); past-or-future speculation ("this was originally X", "will eventually become Y", "could one day be extended to Z"); invented incidents, audiences, or current state. Every factual claim about prior workflow or current state needs a citable source — something the user said, the diff shows, or a doc establishes. (Exception: a deprecation CR whose entire purpose is announcing the deprecation — there, naming the deprecated thing is the point.)templates/cr-description.md).guides/description-vs-docs.md).The single exception to "no verification content in the description body" is the Validation evidence row for shared components — see the Validation section in templates/cr-description.md. That row records evidence, not a todo.
Write the drafted description to a temp file (mktemp -u /tmp/cr-desc-draft.XXXXXX.md) — both the diff presentation here and the moor edit loop below read it.
Present the change. When CURRENT_DESC_PATH from Step 1's block is non-empty (any CR exists — including a freshly-opened draft, whose --fill baseline makes the draft render as all-additions), show what changed rather than the whole body — diff the draft against that baseline and present it in a fenced diff block:
git --no-pager diff --no-index <CURRENT_DESC_PATH> <draft-path>When CURRENT_DESC_PATH is empty (the skip-deep-links path, where no CR exists), display the full description in a fenced code block instead. Use markdown formatting appropriate for the platform (GitHub, GitLab, etc.). After presenting, offer to write the description back to the forge — see the final prompt below.
The description gets pasted into a markdown renderer, so rendering bugs are user-visible. Walk this list:
\\\text fences inside the description will close a 3-backtick wrapper early. Use ``` (4 backticks) for the outer fence whenever the inner content has triple-backtick fences — otherwise the second half of the description renders as raw text.%%{ init }%% line outside `mermaid shows up as raw text and the diagram loses its theme. The block must look like `mermaid → %%{ init: { 'look': 'handDrawn' } }%% → diagram → ` .<details>/<summary> wrapper renders the markdown inside it only with a blank line after </summary> and before </details> — otherwise tables, code fences, and lists inside show as raw text.$FAMILY, $CI_PIPELINE_CREATED_AT), config keywords (extends:, needs:, on_success, manual, allow_failure), job/product/feature suffixes that match identifiers in the diff, CLI flags, file paths. The "if a reader might paste it into a terminal" test is more permissive than "code identifier only" — err generous. But scan separately for CR/issue refs (!148, #42), commit SHAs, and user @mentions — these must be bare text to autolink; backticks render them as inert code spans.$...$ as inline KaTeX/math mode and silently swallow the content between the delimiters — a description that mentions two shell variables in the same paragraph (e.g. $FAMILY and $REGION) renders as a blank gap. Inside fenced code blocks and inline backticks, $ is safe; outside them, use \$ for the literal sign. The same applies to bare _underscores_ around identifiers (read as italics) and *asterisks* inside flag names — backtick them or escape them.Then ask the user how to proceed using the AskUserQuestion tool — an arrow-key-selectable prompt beats a free-text [write / edit / copy] prompt the user has to type out. Use header Disposition and these three options (in order, so the default lands on Yes):
Map the user's selection to the actions below:
<draft-path> is the temp file you wrote at the top of Step 4.gh pr edit --body-file <draft-path>.glab api -X PUT projects/:fullpath/merge_requests/<CR_IID> -F "description=@<draft-path>" — glab mr update -d doesn't accept a file. See the forge cookbook for the full canonical invocation. command -v moorIf `moor` is present, open a one-off review of the current description vs. the draft so the user can comment on specific lines with a reason — directed, line-anchored feedback instead of a prose back-and-forth. Launch via the wrapper (not moor directly — the wrapper writes the MOOR_CONTEXT input header and prints the verdict on its stdout). The viewer blocks until closed, so launch as a background Bash call (run_in_background: true); a foreground call holds the turn open until the Bash timeout:
bash "${CLAUDE_PLUGIN_ROOT}/scripts/review-diff.sh" --files \
<CURRENT_DESC_PATH> <draft-path> \
--title 'CR description — proposed edits' \
--detail branch=<BRANCH> --detail CR=<CR_URL>When the background command completes, read its stdout with the BashOutput tool — not tail / $(...), which trips the command-substitution gate. The last lines carry the verdict: REVIEW_VERDICT (0/1/2/3/absent) and REVIEW_OUTPUT (compact JSON, with .comments when the verdict is 1). Don't read silence as success — only REVIEW_VERDICT 0 is approval; every other outcome either carries feedback to fold in or means the review never happened, so never write the description off the back of one.
REVIEW_OUTPUT's .comments is {body, action, file?, startLine?, endLine?}, where body is the user's inline feedback and action is fix-now (blocker), fix-later, or consider. Fold every fix-now comment into a revised draft — along with any advisory fix-later / consider comments worth incorporating — then loop back to Present the change.diff from Present the change and ask what to change (the moor-absent path below). If the user has a non-moor difftool configured, offer to open the two files in it (git difftool --no-index <CURRENT_DESC_PATH> <draft-path>) as an alternative; don't silently retry moor, since the same failure will recur.The wrapper leaves the context file in place; moor recycles it.
If `moor` isn't on PATH, fall back to chat: ask what to change, revise the draft, and re-present (loop back to Present the change).
One web-UI step remains regardless of choice: screenshots embedded in the description must be dragged into the forge editor (gh/glabdon't expose a clean upload path). After Yes (write) lands the body, open the CR in the browser, drop each PNG, and re-save — the forge rewrites the local paths to hosted URLs.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.