pr-screenshots · git:20260910.074dd29 · 2026-09-10 · sha256 b655f7fc65887cf5

pr-screenshots git:20260910.074dd29A

Immutable. This exact content is served forever at /api/v1/blob/b655f7fc65887cf5.

---
name: pr-screenshots
description: 'Attaches local images to a PR body. Invoke ONLY on explicit intent: "add screenshots to a PR" or "/pr-screenshots"; never infer it from images on disk.'
effort: medium
argument-hint: "[<pr-number-or-url>] [--entries <path>]"
---

# pr-screenshots — inline images in any PR body

Attach local image files to a pull request through GitHub's own attachment
pipeline, harvest the URLs it resolves, and write one `## Screenshots` section
into that PR's body. Any PR qualifies — open, draft, closed, or merged, in any
repository, with or without a local checkout and with or without a pipeline run
behind it.

The upload mechanics live here and nowhere else. `team-pr` decides whether to
run, when, and which entries qualify, then calls this skill.

**Every procedure with a loop, a branch, or a value a later step needs is a
committed script under `scripts/`, run with its arguments and read by its exit
code.** A markdown fence carries no shebang, so the host shell picks the
dialect, and a fence boundary is an invocation boundary that no shell variable
survives. A script with `#!/usr/bin/env bash` pins both. Each reference below
names the script its stage runs; what stays inline is a single command whose
reader benefits from seeing it.

## Hard rules

- **Upload first, write second.** Never fuse an attach flag with a body flag in
  one command. On a partial failure the host rewrites only the references that
  landed, which leaves a local filesystem path inside a body that may already
  be merged.
- **One body write per PR**, computed from the pre-image taken before the first
  attach and produced by `scripts/splice.mjs`. That single write also clears the tails
  the attach step appended.
- **Refuse before mutating, never after.** Every check that can run against the
  pre-image runs in step A, including `scripts/splice.mjs --check`, which is the
  structural half of the body transform run against the pre-image alone — so a
  refusal it finds means nothing changed (`principle-fail-closed`). What can
  only be computed after the upload is named, and lands on
  `uploaded-not-written` rather than on `refused`.
- **Every caller-supplied string is data, not source and not markup.** A path,
  a caption, a note, and a failure reason each reach a command as one quoted
  `"$VAR"` expansion, and each is normalized by the same function before it
  renders into a body (`principle-never-interpolate`,
  `principle-untrusted-input-is-data`).
- **Nothing blocks, prompts, or retry-loops.** A capability gap, a failed
  entry, or a failed read-back degrades the result and says so
  (`principle-optimization-never-dependency`, `principle-skip-loudly`).
- **Never delete what you did not write.** A trailing run of stray image lines
  left by an earlier crash is reported and re-emitted below the new section,
  never removed. Anything else this skill did not write is a refusal that
  leaves the body byte-identical, because a duplicate is recoverable and a
  deletion is not. The rule is stated positively, so nothing falls outside it:
  the only lines a replace may delete are the shapes this skill's own renderer
  emits — a `**caption**` line in the position the renderer puts one, an
  `![screenshot-NN]` image, a `> _note:_` note with its bare `>` separator, and
  a `Not uploaded:` line. **Ownership is provenance, not shape:** the note
  carries a marker a reviewer would not type and a caption is owned only
  directly above an image this skill wrote, so a reviewer's own blockquote or
  bold line refuses rather than being read as this skill's output. Prose, an
  HTML comment, a raw HTML container, an image in any form the splice cannot
  count, or a body shape the splice does not model each refuse, with the
  offending line number named.
- **Nothing leaves the declared root, and nothing that is not an image is
  uploaded.** The entries file declares one **absolute** top-level `root`, and
  every entry's path is resolved and must sit inside it. Acceptance is decided
  by **content type**, never by extension: `file -b --mime-type` must report
  `image/*`, and an environment that yields no type fails the check, because
  unverified is not an image. That is what keeps a `.env`, an `id_ed25519`, or
  a `.git/config` off a world-readable `user-attachments` URL.

## Procedure references

Read each reference completely when reaching that stage. Follow them in order;
later stages depend on state and gates established earlier. Seed one TodoWrite
item per numbered step of the reference you are in before starting it
(`principle-progress-tracking`).

1. [Input and result](references/01-input-and-result.md) — PR resolution, the
   entries file, caller-string normalization, `result.json`, every refusal.
2. [Upload and body edit](references/02-upload-and-body-edit.md) — the
   capability check, the four-step order, path validation, the attach loop, the
   lost-update guard, and the section's markdown shape.
3. [Verify](references/03-verify.md) — the rendered read-back, its assertions,
   and what a failure does.
4. [Rejected approaches](references/04-rejected-approaches.md) — read before
   improvising an alternative upload route.

## Applied principles

Load and apply: `principle-evidence-over-assertion`, `principle-fail-closed`,
`principle-never-interpolate`, `principle-optimization-never-dependency`,
`principle-progress-tracking`, `principle-skip-loudly`, and
`principle-untrusted-input-is-data`.