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).
- Input and result — PR resolution, the
entries file, caller-string normalization,
result.json, every refusal.
- Upload and body edit — the
capability check, the four-step order, path validation, the attach loop, the
lost-update guard, and the section's markdown shape.
- Verify — the rendered read-back, its assertions,
and what a failure does.
- Rejected approaches — 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.
1---2name: pr-screenshots3description: Attaches local images to a PR body. Invoke ONLY on explicit intent: "add screenshots to a PR", "attach an image to a PR description", or "/pr-screenshots"; never infer it from images on disk.4---56# pr-screenshots — inline images in any PR body78Attach local image files to a pull request through GitHub's own attachment9pipeline, harvest the URLs it resolves, and write one `## Screenshots` section10into that PR's body. Any PR qualifies — open, draft, closed, or merged, in any11repository, with or without a local checkout and with or without a pipeline run12behind it.1314The upload mechanics live here and nowhere else. `team-pr` decides whether to15run, when, and which entries qualify, then calls this skill.1617**Every procedure with a loop, a branch, or a value a later step needs is a18committed script under `scripts/`, run with its arguments and read by its exit19code.** A markdown fence carries no shebang, so the host shell picks the20dialect, and a fence boundary is an invocation boundary that no shell variable21survives. A script with `#!/usr/bin/env bash` pins both. Each reference below22names the script its stage runs; what stays inline is a single command whose23reader benefits from seeing it.2425## Hard rules2627- **Upload first, write second.** Never fuse an attach flag with a body flag in28 one command. On a partial failure the host rewrites only the references that29 landed, which leaves a local filesystem path inside a body that may already30 be merged.31- **One body write per PR**, computed from the pre-image taken before the first32 attach and produced by `scripts/splice.mjs`. That single write also clears the tails33 the attach step appended.34- **Refuse before mutating, never after.** Every check that can run against the35 pre-image runs in step A, including `scripts/splice.mjs --check`, which is the36 structural half of the body transform run against the pre-image alone — so a37 refusal it finds means nothing changed (`principle-fail-closed`). What can38 only be computed after the upload is named, and lands on39 `uploaded-not-written` rather than on `refused`.40- **Every caller-supplied string is data, not source and not markup.** A path,41 a caption, a note, and a failure reason each reach a command as one quoted42 `"$VAR"` expansion, and each is normalized by the same function before it43 renders into a body (`principle-never-interpolate`,44 `principle-untrusted-input-is-data`).45- **Nothing blocks, prompts, or retry-loops.** A capability gap, a failed46 entry, or a failed read-back degrades the result and says so47 (`principle-optimization-never-dependency`, `principle-skip-loudly`).48- **Never delete what you did not write.** A trailing run of stray image lines49 left by an earlier crash is reported and re-emitted below the new section,50 never removed. Anything else this skill did not write is a refusal that51 leaves the body byte-identical, because a duplicate is recoverable and a52 deletion is not. The rule is stated positively, so nothing falls outside it:53 the only lines a replace may delete are the shapes this skill's own renderer54 emits — a `**caption**` line in the position the renderer puts one, an55 `![screenshot-NN]` image, a `> _note:_` note with its bare `>` separator, and56 a `Not uploaded:` line. **Ownership is provenance, not shape:** the note57 carries a marker a reviewer would not type and a caption is owned only58 directly above an image this skill wrote, so a reviewer's own blockquote or59 bold line refuses rather than being read as this skill's output. Prose, an60 HTML comment, a raw HTML container, an image in any form the splice cannot61 count, or a body shape the splice does not model each refuse, with the62 offending line number named.63- **Nothing leaves the declared root, and nothing that is not an image is64 uploaded.** The entries file declares one **absolute** top-level `root`, and65 every entry's path is resolved and must sit inside it. Acceptance is decided66 by **content type**, never by extension: `file -b --mime-type` must report67 `image/*`, and an environment that yields no type fails the check, because68 unverified is not an image. That is what keeps a `.env`, an `id_ed25519`, or69 a `.git/config` off a world-readable `user-attachments` URL.7071## Procedure references7273Read each reference completely when reaching that stage. Follow them in order;74later stages depend on state and gates established earlier. Seed one TodoWrite75item per numbered step of the reference you are in before starting it76(`principle-progress-tracking`).77781. [Input and result](references/01-input-and-result.md) — PR resolution, the79 entries file, caller-string normalization, `result.json`, every refusal.802. [Upload and body edit](references/02-upload-and-body-edit.md) — the81 capability check, the four-step order, path validation, the attach loop, the82 lost-update guard, and the section's markdown shape.833. [Verify](references/03-verify.md) — the rendered read-back, its assertions,84 and what a failure does.854. [Rejected approaches](references/04-rejected-approaches.md) — read before86 improvising an alternative upload route.8788## Applied principles8990Load and apply: `principle-evidence-over-assertion`, `principle-fail-closed`,91`principle-never-interpolate`, `principle-optimization-never-dependency`,92`principle-progress-tracking`, `principle-skip-loudly`, and93`principle-untrusted-input-is-data`.