Evidence-Driven Testing
Record annotated proof of behavior, then attach it to the PR and tracker issue.
The recording is the capture of you testing the app via computer use: start the
recorder, then drive the app yourself — click, type, navigate — through each
test target. Every action in the video is the test being performed live; the
recording has no value as evidence unless it shows that interactive session.
If the harness has no computer-use tools but a GUI exists, drive the app with
cua-driver instead (see below) — it is still your live session.
The bundled recorder, scripts/evidence.py, captures the display with FFmpeg
on Linux, macOS, and Windows, timestamps each annotation you add while
testing, burns them into the video on stop, and verifies the result with
ffprobe. It writes evidence.mp4, report.md, and manifest.json into the
session folder.
Inputs
- Test targets (required): The behaviors/flows to verify, phrased as testable statements.
- PR / issue (optional): Where to post the evidence. If omitted, deliver to the requester only.
The recorder
EVIDENCE below means the path to scripts/evidence.py inside this skill's
folder (wherever the skill is installed, e.g.
~/.claude/skills/evidence-driven-testing/scripts/evidence.py). It needs only
Python 3 and FFmpeg.
Check first: python3 $EVIDENCE doctor — verifies ffmpeg, ffprobe,
libx264, and the ass filter (ready), then which screen-capture source
works on this machine (capture_ready and the source auto will pick).
Exits non-zero only when the toolchain is missing; read capture_ready
before recording.
Platforms (--source auto picks the first available):
| OS |
Source |
Needs |
| Linux X11 / XWayland |
x11 (x11grab) |
DISPLAY set |
| Linux Wayland |
wayland (wf-recorder) |
WAYLAND_DISPLAY set, wf-recorder on PATH, and a compositor confirmed to support wlr-screencopy — either a known wlroots one (Sway, Hyprland, river, Wayfire, labwc, dwl, niri) or verified via wayland-info. GNOME and KDE Wayland are not capturable this way; doctor says so. Use x11 through XWayland for X11 apps, or a fallback recorder. --source wayland still forces it |
| macOS |
avfoundation |
Screen Recording permission granted to the terminal / agent host app; doctor lists screen indexes for --screen-index |
| Windows |
gdigrab |
any standard ffmpeg build; powershell for process checks |
Capture is the full screen by default; --geometry WxH and --offset X,Y
crop a region on x11, wayland, and gdigrab. XWayland only sees X11 windows,
so prefer the wayland source when the app under test is Wayland-native.
Crash-safe raw capture: the raw recording is MPEG-TS (raw.ts), so if
the recorder is killed hard or crashes, what was captured still probes and
renders. stop remuxes or re-encodes it into a standard evidence.mp4.
How stopping works: on Linux the recorder is signalled through a pidfd.
On macOS and Windows start launches a small supervisor process that owns
the ffmpeg child, and stop asks it (via stop.request in the session
folder) to interrupt, then terminate, then kill; it writes
recorder-exit.json when done. Nothing ever signals a bare PID, so a
recycled PID can't be hit. If the supervisor dies while the recorder is
still running, stop refuses and tells you which PID to stop by hand. If
it died before it could even record which process it started, stop stays
blocked until you have checked for a stray recorder yourself and rerun it
with --accept-untracked-recorder; the report then carries that caveat.
Fallback recorders when doctor reports no capture source (for
example Wayland without wf-recorder): cua-driver recording start <dir> /
stop (see the cua-driver section), or the OS recorder (macOS:
screencapture -v out.mov). On these paths there is no annotation overlay,
so keep the annotation protocol as files — an assertions.md listing each
setup / test_start / assertion with its result and the approximate
video timestamp, exactly as in the headless path.
Never present --source test (the synthetic pattern generator) as UI
evidence. It exists to smoke-test the toolchain; the repo's
tests/test_evidence.py exercises it.
Instructions
1. Prepare the screen
- Maximize the browser/app window; close popups, notifications, and extra panels.
- Navigate to the starting state (logged in, correct page) BEFORE recording, unless setup itself is under test.
- Note the exact revision under test:
git rev-parse HEAD and
git branch --show-current (or the deployment URL) — the recorder stamps
them into the report.
2. Start recording
Begin the screen recording before the first meaningful action:
python3 $EVIDENCE start \
--output .artifacts/<task-name> \
--title "<what is being verified>" \
--commit "$(git rev-parse HEAD)" --branch "$(git branch --show-current)" \
--environment "<OS / browser / display / deployment>"
It prints JSON with a session path and the chosen source; keep the
path (SESSION=...) for every later command. The source is auto-detected
and the whole screen is captured; pass --source, --geometry,
--offset, --display/--xauthority (X11), --screen-index (macOS), or
--output-name (Wayland) only when doctor or the situation calls for it.
Add a setup annotation describing the starting context:
python3 $EVIDENCE annotate "$SESSION" --type setup \
--message "Logged in, navigating to connectors page"
3. Test via computer use, annotating as you go
Perform every interaction through computer use on the live app — the
recording captures your session, so the testing and the evidence are the
same act. Work at a watchable pace: let the UI settle after each action so
state changes are visible on video.
At each named test's start, add a test_start annotation in Jest style:
python3 $EVIDENCE annotate "$SESSION" --type test_start \
--message "It should execute the tool directly when permission is 'always'"
After each check, add an assertion annotation with --result passed,
failed, or untested:
python3 $EVIDENCE annotate "$SESSION" --type assertion --result passed \
--message "Tool ran without a permission prompt"
Rules for assertions:
- One assertion per meaningful state change — consolidate, don't annotate per UI label.
- Use "Precondition: ..." assertions to establish starting state.
- Keep under 80 characters, high-signal (the recorder rejects longer messages).
- If a test cannot run (missing prerequisite, expired auth window), mark it
untested with the reason — never skip silently.
- The timestamp records when you asserted, not whether it was true — look at
the screen before choosing
passed.
4. Stop and review
Stop recording after the final assertion:
python3 $EVIDENCE stop "$SESSION"
This stops the capture (gracefully, so the recorder flushes; escalating
only if it ignores the request), burns the annotations into evidence.mp4,
probes the result, and writes report.md and manifest.json next to it.
It prints "verified": true on success; if rendering fails the session is
marked finalization_failed — fix the reported cause and run stop again
(a retry does not signal the recorder twice). If the recorder process can no
longer be signalled safely (it died, or its PID now belongs to another
process), the session is marked recorder_lost. Running stop again
finalizes whatever video was captured, but only once that recorder process
is confirmed gone — if it is still alive, stop it first, or the video would
be rendered while still being written.
Confirm the recording captured the key moments before sharing: extract a
frame at each assertion timestamp (ffmpeg -ss <t> -i evidence.mp4 -frames:v 1 frame.png) and check the state and the label are visible.
Fill in the Caveats section of report.md; never leave the placeholder.
5. Post the evidence
report.md is the report: what was tested, environment + exact commit,
pass/fail per test, caveats. Extend it rather than rewriting from scratch.
- Post the video + summary as a PR comment (embed in the PR description if
it's your PR).
gh pr comment cannot attach a local video — upload
evidence.mp4 through the PR's comment box in an authenticated browser, or
upload it to a host and link it (for example the before-and-after upload
adapters). Reopen the comment and confirm the video plays before claiming it
is posted.
- Attach the same video to the tracker issue (Linear/Jira) with a one-line result.
- Send the report + recording to the requester.
Guardrails
- The video must show the actual test session being driven live. Never present
scripted playback, stitched clips, or synthetic footage as a recording; if
the harness lacks computer-use tools but a GUI exists, drive via
cua-driver; with no GUI at all, use the headless path instead.
- Never record a half-covered or tiled window — maximize first.
- Never record a screen showing secrets, tokens, customer data, or payment
details; if a flow requires them, mark it
untested and say why.
- When verifying a fix, show or reference the old failure alongside the new success.
- Always state the exact commit/branch/deployment tested against.
No computer-use tools? Drive with cua-driver (GUI available)
When a display exists but the agent has no built-in computer-use capability,
use cua-driver (macOS / Windows / Linux) as
the actuator. It is still you testing the app live — the recording rule holds
unchanged; only the input mechanism differs.
- Verify the setup with
cua-driver doctor before recording. If a
cua-driver skill is installed, read it and follow its protocol — the
snapshot-before-action invariant is mandatory.
- Loop per interaction:
launch_app → get_window_state (accessibility tree
- screenshot) → act via
element_token (click, type_text, press_key)
→ verify_state for the expected postcondition. Each verify_state check
maps 1:1 onto an assertion annotation.
- Wherever
doctor reports capture_ready: yes, keep using the bundled
recorder above for the video and the annotations; cua-driver only supplies
the input.
- Otherwise,
cua-driver recording start <output-dir> / cua-driver recording stop is the recorder (the output directory is required, and the daemon
must be running: cua-driver serve). Video capture is on by default and is
finalized to <output-dir>/recording.mp4 on stop — but on Windows/Linux it
shells out to ffmpeg, so a missing ffmpeg or display yields only the
per-turn trajectory folders (before/after screenshots, action.json,
click.png), no video. After stopping, verify recording.mp4 exists
before citing it; if it is absent, fix the recorder or present the
per-turn before/after screenshots as numbered captures per the headless
protocol.
- If no annotation overlay is available on this path, keep the protocol as
files: an
assertions.md listing each test_start / assertion with its
result, exactly as in the headless path.
Headless path (no GUI available)
When the agent has no desktop to record, keep the same assertion discipline;
swap the recorder for scripted capture:
Save everything to .artifacts/<task-name>/ (gitignore it — evidence gets
uploaded, never committed). Keep the capture script beside the captures so
the run is repeatable.
Screenshots: the before-and-after CLI (@vercel/before-and-after)
captures URLs or elements and its pairs feed PR embeds directly. In
containers/VMs where Chrome fails with "No usable sandbox", set
AGENT_BROWSER_ARGS="--no-sandbox".
Video / multi-step flows: a one-off Playwright script, run without
adding playwright to the project's dependencies:
npx --yes --package=playwright node record.mjs
(Plain npx playwright node record.mjs fails — node is not a Playwright
CLI command; --package=playwright is what puts the module on the path.)
Minimal record.mjs:
import { chromium } from "playwright";
const browser = await chromium.launch();
const context = await browser.newContext({
recordVideo: { dir: ".artifacts/<task-name>/" },
});
const page = await context.newPage();
await page.goto("http://localhost:3000/path-under-test");
// ...drive the flow, one meaningful state change per step...
await context.close(); // finalizes the .webm
await browser.close();
Trim or compress with ffmpeg if the file is large.
The annotation protocol becomes files: number captures in test order
with the assertion in the name — 01-precondition-signed-in.png,
02-it-saves-on-blur-passed.png — and keep an assertions.md in the
artifacts folder listing each test_start / assertion with its result
(passed / failed / untested + reason).
Non-UI changes still need evidence
- API / performance: a scripted probe with measured numbers — request
counts per phase, latency before/after — captured to
probe-output.txt.
- Rendering / canvas / shader: rendered frames plus pixel assertions
(diff values), reviewed by eye and saved as PNGs.
- Agent behavior: the relevant transcript excerpt showing the tool call
and response.
- Bug fixes: reproduce and capture the failure before writing the
fix — that capture is the "before" half of a before/after pair.
Capture hygiene
- Confirm the server you're probing is running your code (right port,
right process), especially when multiple agents share a machine:
lsof -i :<port> — or where lsof isn't installed,
ss -ltnp "sport = :<port>" to find the listener's PID, then
ps -p <pid> -o args= to confirm it's yours.
- Evidence complements the repo's checks (typecheck/build/tests); it never
replaces them.
- Hand before/after media pairs to a before/after tool for the PR embed
(e.g.
before-and-after before.png after.png --markdown).
1---2name: evidence-driven-testing3description: Records visual proof while testing UI behavior — the agent tests the app hands-on via computer use while a screen recording with structured test/assertion annotations captures the session — then posts the video and a results summary to the PR and tracker issue. Use whenever a change needs verifiable evidence that it works, instead of prose claims — including headless environments (scripted screenshots and probes) and non-UI changes (measured numbers, output pairs).4---56# Evidence-Driven Testing78Record annotated proof of behavior, then attach it to the PR and tracker issue.910The recording is the capture of you testing the app via computer use: start the11recorder, then drive the app yourself — click, type, navigate — through each12test target. Every action in the video is the test being performed live; the13recording has no value as evidence unless it shows that interactive session.14If the harness has no computer-use tools but a GUI exists, drive the app with15`cua-driver` instead (see below) — it is still your live session.1617The bundled recorder, `scripts/evidence.py`, captures the display with FFmpeg18on Linux, macOS, and Windows, timestamps each annotation you add while19testing, burns them into the video on stop, and verifies the result with20ffprobe. It writes `evidence.mp4`, `report.md`, and `manifest.json` into the21session folder.2223## Inputs2425- **Test targets** (required): The behaviors/flows to verify, phrased as testable statements.26- **PR / issue** (optional): Where to post the evidence. If omitted, deliver to the requester only.2728## The recorder2930`EVIDENCE` below means the path to `scripts/evidence.py` inside this skill's31folder (wherever the skill is installed, e.g.32`~/.claude/skills/evidence-driven-testing/scripts/evidence.py`). It needs only33Python 3 and FFmpeg.3435- **Check first**: `python3 $EVIDENCE doctor` — verifies `ffmpeg`, `ffprobe`,36 `libx264`, and the `ass` filter (`ready`), then which screen-capture source37 works on this machine (`capture_ready` and the source `auto` will pick).38 Exits non-zero only when the toolchain is missing; read `capture_ready`39 before recording.40- **Platforms** (`--source auto` picks the first available):4142 | OS | Source | Needs |43 |---|---|---|44 | Linux X11 / XWayland | `x11` (x11grab) | `DISPLAY` set |45 | Linux Wayland | `wayland` (wf-recorder) | `WAYLAND_DISPLAY` set, `wf-recorder` on PATH, and a compositor confirmed to support wlr-screencopy — either a known wlroots one (Sway, Hyprland, river, Wayfire, labwc, dwl, niri) or verified via `wayland-info`. GNOME and KDE Wayland are not capturable this way; `doctor` says so. Use `x11` through XWayland for X11 apps, or a fallback recorder. `--source wayland` still forces it |46 | macOS | `avfoundation` | Screen Recording permission granted to the terminal / agent host app; `doctor` lists screen indexes for `--screen-index` |47 | Windows | `gdigrab` | any standard ffmpeg build; `powershell` for process checks |4849 Capture is the full screen by default; `--geometry WxH` and `--offset X,Y`50 crop a region on x11, wayland, and gdigrab. XWayland only sees X11 windows,51 so prefer the `wayland` source when the app under test is Wayland-native.52- **Crash-safe raw capture**: the raw recording is MPEG-TS (`raw.ts`), so if53 the recorder is killed hard or crashes, what was captured still probes and54 renders. `stop` remuxes or re-encodes it into a standard `evidence.mp4`.55- **How stopping works**: on Linux the recorder is signalled through a pidfd.56 On macOS and Windows `start` launches a small supervisor process that owns57 the ffmpeg child, and `stop` asks it (via `stop.request` in the session58 folder) to interrupt, then terminate, then kill; it writes59 `recorder-exit.json` when done. Nothing ever signals a bare PID, so a60 recycled PID can't be hit. If the supervisor dies while the recorder is61 still running, `stop` refuses and tells you which PID to stop by hand. If62 it died before it could even record which process it started, `stop` stays63 blocked until you have checked for a stray recorder yourself and rerun it64 with `--accept-untracked-recorder`; the report then carries that caveat.65- **Fallback recorders** when `doctor` reports no capture source (for66 example Wayland without wf-recorder): `cua-driver recording start <dir>` /67 `stop` (see the cua-driver section), or the OS recorder (macOS:68 `screencapture -v out.mov`). On these paths there is no annotation overlay,69 so keep the annotation protocol as files — an `assertions.md` listing each70 `setup` / `test_start` / `assertion` with its result and the approximate71 video timestamp, exactly as in the headless path.72- **Never** present `--source test` (the synthetic pattern generator) as UI73 evidence. It exists to smoke-test the toolchain; the repo's74 `tests/test_evidence.py` exercises it.7576## Instructions7778### 1. Prepare the screen7980- Maximize the browser/app window; close popups, notifications, and extra panels.81- Navigate to the starting state (logged in, correct page) BEFORE recording, unless setup itself is under test.82- Note the exact revision under test: `git rev-parse HEAD` and83 `git branch --show-current` (or the deployment URL) — the recorder stamps84 them into the report.8586### 2. Start recording8788- Begin the screen recording before the first meaningful action:8990 ```bash91 python3 $EVIDENCE start \92 --output .artifacts/<task-name> \93 --title "<what is being verified>" \94 --commit "$(git rev-parse HEAD)" --branch "$(git branch --show-current)" \95 --environment "<OS / browser / display / deployment>"96 ```9798 It prints JSON with a `session` path and the chosen `source`; keep the99 path (`SESSION=...`) for every later command. The source is auto-detected100 and the whole screen is captured; pass `--source`, `--geometry`,101 `--offset`, `--display`/`--xauthority` (X11), `--screen-index` (macOS), or102 `--output-name` (Wayland) only when `doctor` or the situation calls for it.103- Add a `setup` annotation describing the starting context:104105 ```bash106 python3 $EVIDENCE annotate "$SESSION" --type setup \107 --message "Logged in, navigating to connectors page"108 ```109110### 3. Test via computer use, annotating as you go111112- Perform every interaction through computer use on the live app — the113 recording captures your session, so the testing and the evidence are the114 same act. Work at a watchable pace: let the UI settle after each action so115 state changes are visible on video.116- At each named test's start, add a `test_start` annotation in Jest style:117118 ```bash119 python3 $EVIDENCE annotate "$SESSION" --type test_start \120 --message "It should execute the tool directly when permission is 'always'"121 ```122123- After each check, add an `assertion` annotation with `--result passed`,124 `failed`, or `untested`:125126 ```bash127 python3 $EVIDENCE annotate "$SESSION" --type assertion --result passed \128 --message "Tool ran without a permission prompt"129 ```130131- Rules for assertions:132 - One assertion per meaningful state change — consolidate, don't annotate per UI label.133 - Use "Precondition: ..." assertions to establish starting state.134 - Keep under 80 characters, high-signal (the recorder rejects longer messages).135 - If a test cannot run (missing prerequisite, expired auth window), mark it `untested` with the reason — never skip silently.136 - The timestamp records when you asserted, not whether it was true — look at137 the screen before choosing `passed`.138139### 4. Stop and review140141- Stop recording after the final assertion:142143 ```bash144 python3 $EVIDENCE stop "$SESSION"145 ```146147 This stops the capture (gracefully, so the recorder flushes; escalating148 only if it ignores the request), burns the annotations into `evidence.mp4`,149 probes the result, and writes `report.md` and `manifest.json` next to it.150 It prints `"verified": true` on success; if rendering fails the session is151 marked `finalization_failed` — fix the reported cause and run `stop` again152 (a retry does not signal the recorder twice). If the recorder process can no153 longer be signalled safely (it died, or its PID now belongs to another154 process), the session is marked `recorder_lost`. Running `stop` again155 finalizes whatever video was captured, but only once that recorder process156 is confirmed gone — if it is still alive, stop it first, or the video would157 be rendered while still being written.158- Confirm the recording captured the key moments before sharing: extract a159 frame at each assertion timestamp (`ffmpeg -ss <t> -i evidence.mp4160 -frames:v 1 frame.png`) and check the state and the label are visible.161- Fill in the Caveats section of `report.md`; never leave the placeholder.162163### 5. Post the evidence164165- `report.md` is the report: what was tested, environment + exact commit,166 pass/fail per test, caveats. Extend it rather than rewriting from scratch.167- Post the video + summary as a PR comment (embed in the PR description if168 it's your PR). `gh pr comment` cannot attach a local video — upload169 `evidence.mp4` through the PR's comment box in an authenticated browser, or170 upload it to a host and link it (for example the `before-and-after` upload171 adapters). Reopen the comment and confirm the video plays before claiming it172 is posted.173- Attach the same video to the tracker issue (Linear/Jira) with a one-line result.174- Send the report + recording to the requester.175176## Guardrails177178- The video must show the actual test session being driven live. Never present179 scripted playback, stitched clips, or synthetic footage as a recording; if180 the harness lacks computer-use tools but a GUI exists, drive via181 `cua-driver`; with no GUI at all, use the headless path instead.182- Never record a half-covered or tiled window — maximize first.183- Never record a screen showing secrets, tokens, customer data, or payment184 details; if a flow requires them, mark it `untested` and say why.185- When verifying a fix, show or reference the old failure alongside the new success.186- Always state the exact commit/branch/deployment tested against.187188## No computer-use tools? Drive with cua-driver (GUI available)189190When a display exists but the agent has no built-in computer-use capability,191use [cua-driver](https://github.com/trycua/cua) (macOS / Windows / Linux) as192the actuator. It is still you testing the app live — the recording rule holds193unchanged; only the input mechanism differs.194195- Verify the setup with `cua-driver doctor` before recording. If a196 `cua-driver` skill is installed, read it and follow its protocol — the197 snapshot-before-action invariant is mandatory.198- Loop per interaction: `launch_app` → `get_window_state` (accessibility tree199 + screenshot) → act via `element_token` (`click`, `type_text`, `press_key`)200 → `verify_state` for the expected postcondition. Each `verify_state` check201 maps 1:1 onto an `assertion` annotation.202- Wherever `doctor` reports `capture_ready: yes`, keep using the bundled203 recorder above for the video and the annotations; cua-driver only supplies204 the input.205- Otherwise, `cua-driver recording start <output-dir>` / `cua-driver recording206 stop` is the recorder (the output directory is required, and the daemon207 must be running: `cua-driver serve`). Video capture is on by default and is208 finalized to `<output-dir>/recording.mp4` on stop — but on Windows/Linux it209 shells out to ffmpeg, so a missing ffmpeg or display yields only the210 per-turn trajectory folders (before/after screenshots, `action.json`,211 `click.png`), no video. After stopping, verify `recording.mp4` exists212 before citing it; if it is absent, fix the recorder or present the213 per-turn before/after screenshots as numbered captures per the headless214 protocol.215- If no annotation overlay is available on this path, keep the protocol as216 files: an `assertions.md` listing each `test_start` / `assertion` with its217 result, exactly as in the headless path.218219## Headless path (no GUI available)220221When the agent has no desktop to record, keep the same assertion discipline;222swap the recorder for scripted capture:223224- Save everything to `.artifacts/<task-name>/` (gitignore it — evidence gets225 uploaded, never committed). Keep the capture script beside the captures so226 the run is repeatable.227- **Screenshots**: the `before-and-after` CLI (`@vercel/before-and-after`)228 captures URLs or elements and its pairs feed PR embeds directly. In229 containers/VMs where Chrome fails with "No usable sandbox", set230 `AGENT_BROWSER_ARGS="--no-sandbox"`.231- **Video / multi-step flows**: a one-off Playwright script, run without232 adding playwright to the project's dependencies:233234 ```bash235 npx --yes --package=playwright node record.mjs236 ```237238 (Plain `npx playwright node record.mjs` fails — `node` is not a Playwright239 CLI command; `--package=playwright` is what puts the module on the path.)240 Minimal `record.mjs`:241242 ```js243 import { chromium } from "playwright";244 const browser = await chromium.launch();245 const context = await browser.newContext({246 recordVideo: { dir: ".artifacts/<task-name>/" },247 });248 const page = await context.newPage();249 await page.goto("http://localhost:3000/path-under-test");250 // ...drive the flow, one meaningful state change per step...251 await context.close(); // finalizes the .webm252 await browser.close();253 ```254255 Trim or compress with ffmpeg if the file is large.256- **The annotation protocol becomes files**: number captures in test order257 with the assertion in the name — `01-precondition-signed-in.png`,258 `02-it-saves-on-blur-passed.png` — and keep an `assertions.md` in the259 artifacts folder listing each `test_start` / `assertion` with its result260 (`passed` / `failed` / `untested` + reason).261262## Non-UI changes still need evidence263264- **API / performance**: a scripted probe with measured numbers — request265 counts per phase, latency before/after — captured to `probe-output.txt`.266- **Rendering / canvas / shader**: rendered frames plus pixel assertions267 (diff values), reviewed by eye and saved as PNGs.268- **Agent behavior**: the relevant transcript excerpt showing the tool call269 and response.270- **Bug fixes**: reproduce and capture the failure **before** writing the271 fix — that capture is the "before" half of a before/after pair.272273## Capture hygiene274275- Confirm the server you're probing is running *your* code (right port,276 right process), especially when multiple agents share a machine:277 `lsof -i :<port>` — or where `lsof` isn't installed,278 `ss -ltnp "sport = :<port>"` to find the listener's PID, then279 `ps -p <pid> -o args=` to confirm it's yours.280- Evidence complements the repo's checks (typecheck/build/tests); it never281 replaces them.282- Hand before/after media pairs to a before/after tool for the PR embed283 (e.g. `before-and-after before.png after.png --markdown`).