The PR description is the home for proof-of-testing screenshots. They are hosted as raw GitHub blobs at a commit SHA that is force-pushed away after the URL is captured — the blob keeps serving until GitHub GC.
-
Capture comprehensively. Navigate to the URL with browser_navigate, then browser_take_screenshot with fullPage: true to capture the entire page — full-page is the default; only fall back to element-scoped (target: "<ref-from-snapshot>") when the page is impractically tall (infinite scroll, very long forms) or when the visible diff is genuinely a single component. Save under the workspace at .symphony/screenshots/<descriptive-name>.png. The Playwright sandbox blocks /tmp/... and any path outside the workspace + .playwright-mcp/ roots.
Capture every state that matters to a reviewer, not just the happy path. For a typical user-facing change that means multiple files — e.g. 01-default.png, 02-loading.png, 03-error.png, 04-mobile.png, 05-hover.png. Resize the viewport with browser_resize between shots when the change is responsive. Err on the side of more screenshots: the commit is force-pushed away so size doesn't matter, and a missing state is the most common reviewer ask. Number filenames so they sort and embed in a deterministic order.
-
Stage and commit all the screenshots together:
git add .symphony/screenshots/
git commit -m "chore: temporary screenshots for PR description (will be removed)" --no-verify
--no-verify is allowed here because this commit is throwaway and lint/format hooks would reject the binary paths. This is the only skill that bypasses hooks.
-
Push. If the remote is ahead, rebase the screenshot commit onto it first (git fetch && git rebase origin/<branch>); a merge commit pollutes the throwaway history.
git push origin "$(git branch --show-current)"
-
Build the raw URLs at the new commit SHA — one base, one URL per file.
sha=$(git log -1 --format=%H)
repo_url=$(gh repo view --json url -q .url)
for f in .symphony/screenshots/*.png; do
name=$(basename "$f")
echo "${repo_url}/raw/${sha}/.symphony/screenshots/${name}"
done
-
Update PR body. Read the current body, append (or replace) a ## Screenshots section, write back. Never clobber existing sections. Embed every captured screenshot — one image per state — with a short caption derived from the filename so reviewers can scan them.
pr=$(gh pr view --json number -q .number)
body=$(gh pr view --json body -q .body)
block=$(printf '\n\n## Screenshots\n\n')
for f in .symphony/screenshots/*.png; do
name=$(basename "$f" .png)
url="${repo_url}/raw/${sha}/.symphony/screenshots/${name}.png"
block+=$(printf '**%s**\n\n\n\n' "$name" "$name" "$url")
done
gh pr edit "$pr" --body "${body}${block}"
Use a heredoc or a built-up shell variable for the --body arg so newlines stay literal. Group related captures (e.g. mobile vs desktop) under sub-headings if it makes the PR easier to scan.
-
Verify the images render in the PR (visual confirmation by the operator, or a curl -I "$raw_url" returning 200 if running unattended). Do not proceed to step 7 without confirmation — once the commit is force-pushed away, the URLs still work but you can no longer regenerate them from history.
-
Reset and force-push to drop the screenshot commit from branch history:
git reset --hard HEAD~1
git push --force-with-lease origin "$(git branch --show-current)"
Always --force-with-lease, never --force. If the lease check fails, someone pushed in the meantime — fetch, re-rebase, and retry from step 3 (the new SHA invalidates the URLs captured in step 4, so re-do the PR-body update too).
-
Cleanup workspace artifacts: rm -rf .symphony/screenshots .playwright-mcp (those dirs should not appear in git status afterward).