ui-video-evidence
Record captioned browser/UI evidence videos and link sanitized artifacts for authorized review
- License 许可
- MIT
- 最近更新
- 2026-11-14
- Stars
- ★ 6
- Forks
- 0
01 前言 Front Matter
- name
- ui-video-evidence
- description
- Record captioned browser/UI evidence videos and link sanitized artifacts for authorized review
03 SKILL.md 正文
UI Video Evidence for Visual Verification
Purpose
Prove user-visible behavior in a way reviewers and agents can verify quickly from the PR conversation.
Before producing or reviewing any video, apply the canonical evidence-standards continuous-video rule. Screenshot sequences and slideshow MP4s never satisfy video evidence.
When UI Video Is Required
Apply the narrow proportional exceptions in pr-lifecycle first. Source-only harmless display-copy may justify media N/A; a bounded static-copy rendering claim may use an actual screenshot. Other UI/browser video is mandatory for user-facing behavior, including:
- Visual/UI layout or styling changes
- Click flows, form submit flows, navigation, or modal behavior
- New/changed browser interactions
- Any claim of visual correctness or lack of visual regression
Outside those explicit exceptions, missing applicable UI video is an evidence failure. Never waive requested interaction, streaming or native proof by calling it a string change.
Required Outputs
Each video run must provide the following; the simple-static screenshot exception retains the canonical paired-capture, provenance, and visual-review requirements:
- UI video (
.mp4preferred) - UI fallback media (
.gifrecommended when player embed is not required) - Captions (burned-in preferred;
.vtt/.srtacceptable) - Git linkage (
git rev-parse HEADvisible in recording context) - Media links accessible to the intended reviewer at an authorized destination
- A browser-viewable artifact (
.gif, commit-pinned raw asset, release asset, or native attachment) - A downloadable high-fidelity artifact (
.mp4,.mp4.zip, or release asset) - Matching metadata and caption artifacts in the reviewed evidence receipt
Scoped Browser Ownership (resolve FIRST, before picking a capture method)
Before choosing how to capture evidence, check whether the target repo has its
own browser-execution owner (e.g. testing_ui/CLAUDE.md,
../browser-testing/SKILL.md). If a scoped owner exists and
forbids headed/manual capture or Claude-in-Chrome, its policy wins — use its
designated headless tool (Aside CLI/aside-mcp, or headless Playwright). Do
not escalate to a native-chrome/manual-capture requirement, and do not ask for
Screen Recording permission, when a scoped headless path is available and not
documented as blocked.
Mandatory Frames
Capture this journey on each compared implementation. The pre-action frame below is not by itself the pre-change-code baseline; preserve both revisions and their original capture provenance under the canonical BEFORE/AFTER rule.
| # | Frame | Must show |
|---|---|---|
| 1 | URL + Page Load | Full native browser URL bar with route under test, OR — when the scoped owner mandates headless capture — a genuine route/URL provenance marker (printed navigation URL/log line, on-page route text, or window title) captured from the same automated session and tied to the frame timestamp |
| 2 | Pre-action State | Initial state before action on this revision |
| 3 | Action | Click/input/navigation action |
| 4 | Post-action State | Resulting state after action on this revision |
| 5 | Git Linkage | Terminal split, devtools log, or on-page SHA marker |
Recording Options
Option 1: Scoped headless driver (Aside / Playwright) — DEFAULT
Follow ../browser-testing/SKILL.md: drive the browser headlessly
via Aside CLI/aside-mcp or headless Playwright, log the navigated URL/route to
the terminal or an overlay, and record with the scoped repo's own recorder
(e.g. testing_ui/streaming/base.py UIVideoRecorder) or ffmpeg (Option 3).
Never use mcp__claude-in-chrome__* or any headed browser for this path.
Option 2: Manual desktop capture (Kap) — LAST RESORT ONLY
Only when the current thread explicitly requests visible capture, the browser is independently verified as task-owned, and the repo permits it. Absence of a repo prohibition is not authorization. Follow the shared browser-control owner; never use personal/default Chrome, its profiles or its cookies.
brew install --cask kap
Record browser window including address bar.
Option 3: ffmpeg (headless/CI)
ffmpeg -video_size 1280x720 -framerate 10 -f x11grab -i :99 -t 30 "/tmp/${WORK_NAME:-work}.mp4"
Caption Requirements (MANDATORY)
Both tmux and UI videos must always have captions.
Accepted forms:
- Burned-in captions in the video (preferred)
- Sidecar caption file (
.vtt/.srt) linked alongside the reviewed media artifacts
Use ../video-caption/SKILL.md for reliable burned-in captions.
Evidence access and authorized publication
Follow ../evidence-standards/SKILL.md for publication authority, audience, and destination. A PR, checked-in document, access-controlled receipt/store, or authorized gist may link the evidence. For PRs governed by pr-lifecycle, its review receipts belong in the PR description; opted-in repositories also link every stage-exit receipt through its required sanitized gist URL (except a pr-lifecycle one-liner fast-path receipt, which lives in the PR body). Applicable inline viewable media and original media URLs are required there. Other authorized artifact storage does not remove that description/publication requirement. Preserve real media, captions, exact source provenance, and reviewer access; publish only within the current authorization.
The following GitHub release example applies only when that destination is authorized. Verify the intended PR exists via gh pr view before creating any release or upload. Caller must provide non-empty VIDEO_FILE and PREVIEW_FILE artifacts, an explicit non-empty RUN_ID, an explicit non-empty CAPTURED_SHA matching the PR head SHA, and declare an explicit caption choice: set CAPTION_FILE to the actual .vtt or .srt sidecar path, or set CAPTION_MODE=burned when captions are burned into the video. All media and subtitle formats are verified with ffprobe prior to archive creation and publication. Packaging creates a new archive in a private temporary directory so existing user archives are never overwritten or updated with stale entries. Each run publishes to an immutable unique release tag combining the PR number, commit SHA, and run identifier without overwriting or clobbering existing releases.
This example publishes a current-candidate capture only. Retain baseline media at its actual source SHA through an authorized artifact route; never relabel it as the PR head to satisfy this example's head-equality guard.
(
set -euo pipefail
repo="${REPO:-}"
url_regex='^https://github\.com/([^/]+/[^/]+)/pull/([1-9][0-9]*)/?$'
if [[ -n "${PR_NUMBER_OR_URL:-}" ]]; then
if [[ "$PR_NUMBER_OR_URL" =~ $url_regex ]]; then
url_repo="${BASH_REMATCH[1]}"
url_pr="${BASH_REMATCH[2]}"
if [[ -n "$repo" && "$repo" != "$url_repo" ]]; then
echo "Error: Conflicting repository target '$repo' vs PR URL '$url_repo'" >&2
exit 1
fi
repo="${repo:-$url_repo}"
if [[ -n "${PR_NUMBER:-}" && "${PR_NUMBER}" != "$url_pr" ]]; then
echo "Error: Conflicting PR number '${PR_NUMBER}' vs PR URL '$url_pr'" >&2
exit 1
fi
PR_NUMBER="$url_pr"
elif [[ "$PR_NUMBER_OR_URL" =~ ^[1-9][0-9]*$ ]]; then
if [[ -n "${PR_NUMBER:-}" && "${PR_NUMBER}" != "$PR_NUMBER_OR_URL" ]]; then
echo "Error: Conflicting PR number '${PR_NUMBER}' vs PR_NUMBER_OR_URL '$PR_NUMBER_OR_URL'" >&2
exit 1
fi
PR_NUMBER="${PR_NUMBER:-$PR_NUMBER_OR_URL}"
else
echo "Error: PR_NUMBER_OR_URL must be a valid GitHub PR URL or positive PR number" >&2
exit 1
fi
fi
if [[ -n "${PR_NUMBER:-}" && "$PR_NUMBER" =~ $url_regex ]]; then
url_repo="${BASH_REMATCH[1]}"
url_pr="${BASH_REMATCH[2]}"
if [[ -n "$repo" && "$repo" != "$url_repo" ]]; then
echo "Error: Conflicting repository target '$repo' vs PR URL '$url_repo'" >&2
exit 1
fi
repo="${repo:-$url_repo}"
PR_NUMBER="$url_pr"
fi
if ! command -v ffprobe >/dev/null 2>&1; then
echo "Error: ffprobe is required for media validation but not found in PATH" >&2
exit 1
fi
if [[ -z "${PR_NUMBER:-}" || "$PR_NUMBER" == *"<"* || ! "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then
echo "Error: PR_NUMBER must be set to a valid PR number before publication" >&2
exit 1
fi
if [[ -z "${repo:-}" || "$repo" == *"<"* ]]; then
echo "Error: REPO must be set to a valid owner/repo before publication" >&2
exit 1
fi
if [[ ! "$repo" =~ ^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$ ]]; then
echo "Error: REPO must be in the format 'owner/repo'" >&2
exit 1
fi
if [[ -z "${RUN_ID:-}" || "$RUN_ID" == *"<"* ]]; then
echo "Error: RUN_ID must be set to an explicit nonempty identifier before publication" >&2
exit 1
fi
if [[ -z "${CAPTURED_SHA:-}" || "$CAPTURED_SHA" == *"<"* ]]; then
echo "Error: CAPTURED_SHA must be set to the explicit commit SHA captured in the evidence" >&2
exit 1
fi
if [[ -z "${VIDEO_FILE:-}" || "$VIDEO_FILE" == *"<"* ]]; then
echo "Error: VIDEO_FILE must be set to a valid path before publication" >&2
exit 1
fi
if [[ -z "${PREVIEW_FILE:-}" || "$PREVIEW_FILE" == *"<"* ]]; then
echo "Error: PREVIEW_FILE must be set to a valid path before publication" >&2
exit 1
fi
video_file="$VIDEO_FILE"
preview_file="$PREVIEW_FILE"
if [ ! -f "$video_file" ] || [ ! -s "$video_file" ]; then
echo "Error: Video file '$video_file' not found or is empty" >&2
exit 1
fi
if [ ! -f "$preview_file" ] || [ ! -s "$preview_file" ]; then
echo "Error: Preview file '$preview_file' not found or is empty" >&2
exit 1
fi
video_codec="$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_type -of default=noprint_wrappers=1:nokey=1 "$video_file" 2>/dev/null || true)"
if [[ "$video_codec" != "video" ]]; then
echo "Error: Video file '$video_file' does not contain a valid video stream" >&2
exit 1
fi
preview_codec="$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_type -of default=noprint_wrappers=1:nokey=1 "$preview_file" 2>/dev/null || true)"
if [[ "$preview_codec" != "video" ]]; then
echo "Error: Preview file '$preview_file' does not contain a valid video/image stream" >&2
exit 1
fi
if [[ -n "${CAPTION_MODE:-}" && "${CAPTION_MODE:-}" != "burned" ]]; then
echo "Error: Unsupported CAPTION_MODE '${CAPTION_MODE}': only CAPTION_MODE=burned is supported" >&2
exit 1
fi
if [[ -n "${CAPTION_FILE:-}" && "${CAPTION_MODE:-}" == "burned" ]]; then
echo "Error: Cannot specify both CAPTION_FILE and CAPTION_MODE=burned: choose exactly one caption option" >&2
exit 1
fi
if [[ "${CAPTION_MODE:-}" == "burned" ]]; then
caption_file=""
elif [[ -n "${CAPTION_FILE:-}" && "${CAPTION_FILE:-}" != *"<"* ]]; then
caption_file="$CAPTION_FILE"
if [ ! -f "$caption_file" ] || [ ! -s "$caption_file" ]; then
echo "Error: Caption sidecar '$caption_file' specified but not found or is empty" >&2
exit 1
fi
caption_codec="$(ffprobe -v error -show_entries stream=codec_type -of default=noprint_wrappers=1:nokey=1 "$caption_file" 2>/dev/null || true)"
if [[ "$caption_codec" != "subtitle" ]]; then
echo "Error: Caption file '$caption_file' does not contain a valid subtitle stream" >&2
exit 1
fi
else
echo "Error: Explicit caption choice required: specify CAPTION_FILE (.vtt/.srt) or CAPTION_MODE=burned" >&2
exit 1
fi
# Verify exact intended PR before any release creation or mutation
pr_view_json="$(gh pr view "$PR_NUMBER" --repo "$repo" --json number,headRefOid,url)"
pr_head_sha="$(printf '%s' "$pr_view_json" | jq -r '.headRefOid // empty')"
pr_number_resolved="$(printf '%s' "$pr_view_json" | jq -r '.number // empty')"
if [[ -n "$pr_number_resolved" && "$pr_number_resolved" != "$PR_NUMBER" ]]; then
echo "Error: Verified PR number mismatch ($pr_number_resolved vs $PR_NUMBER)" >&2
exit 1
fi
if [[ -z "$pr_head_sha" ]]; then
echo "Error: Unable to resolve head SHA for PR #$PR_NUMBER in $repo" >&2
exit 1
fi
if [[ "$CAPTURED_SHA" != "$pr_head_sha" ]]; then
echo "Error: Declared CAPTURED_SHA '$CAPTURED_SHA' does not match verified PR head '$pr_head_sha'" >&2
exit 1
fi
sha_short="${CAPTURED_SHA:0:12}"
tag="evidence-pr-${PR_NUMBER}-${sha_short}-${RUN_ID}"
archive_scratch="$("${CLAUDE_HOME:-$HOME/.claude}/skills/scratch-space/scripts/scratch-dir.sh" tmp)"
if [ -z "$archive_scratch" ]; then
echo "Scratch resolver returned an empty path; refusing archive allocation" >&2
exit 75
fi
archive_dir="$(mktemp -d "$archive_scratch/evidence_archive.XXXXXX")"
zip_file="$archive_dir/$(basename "$video_file").zip"
trap 'rm -f -- "$zip_file"; rmdir -- "$archive_dir"' EXIT
zip -j "$zip_file" "$video_file"
assets=("$zip_file" "$preview_file")
if [ -n "$caption_file" ]; then
assets+=("$caption_file")
fi
if ! gh release create "$tag" --repo "$repo" --target "$CAPTURED_SHA" --draft --title "PR #${PR_NUMBER} Evidence" --notes ""; then
echo "Error: Release '$tag' could not be created or already exists; stopping without modifying releases" >&2
exit 1
fi
release_json="$(gh release view "$tag" --repo "$repo" --json assets,targetCommitish,url)"
release_target="$(printf '%s' "$release_json" | jq -r '.targetCommitish // empty')"
if [[ -z "$release_target" || ! "$release_target" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "Error: Unable to resolve valid target commit for release '$tag' in $repo" >&2
exit 1
fi
if [[ "$release_target" != "$CAPTURED_SHA" ]]; then
echo "Error: Verified release target commit '$release_target' does not match CAPTURED_SHA '$CAPTURED_SHA'" >&2
exit 1
fi
gh release upload "$tag" --repo "$repo" "${assets[@]}"
release_json="$(gh release view "$tag" --repo "$repo" --json assets,targetCommitish,url)"
release_target="$(printf '%s' "$release_json" | jq -r '.targetCommitish // empty')"
if [[ -z "$release_target" || ! "$release_target" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "Error: Unable to resolve valid target commit for release '$tag' in $repo" >&2
exit 1
fi
if [[ "$release_target" != "$CAPTURED_SHA" ]]; then
echo "Error: Verified release target commit '$release_target' does not match CAPTURED_SHA '$CAPTURED_SHA'" >&2
exit 1
fi
for expected_asset in "${assets[@]}"; do
name="$(basename "$expected_asset")"
matches="$(printf '%s' "$release_json" | jq --arg n "$name" '[.assets[] | select(.name == $n)]')"
count="$(printf '%s' "$matches" | jq 'length')"
if [[ "$count" -ne 1 ]]; then
echo "Error: Expected asset '$name' present $count time(s) in release readback (must be exactly 1)" >&2
exit 1
fi
if [[ -z "$(printf '%s' "$matches" | jq -r '.[0].url // empty')" ]]; then
echo "Error: Expected asset '$name' has no usable URL in release readback" >&2
exit 1
fi
done
printf '%s\n' "$release_json"
)
From the JSON output returned by gh release view, extract the uploaded asset URLs and construct the canonical Scratch comment body. Do not guess draft asset download URLs or invent placeholder URLs.
Once the canonical Scratch comment body is constructed, post the comment to the authorized PR:
(
set -euo pipefail
repo="${REPO:-}"
url_regex='^https://github\.com/([^/]+/[^/]+)/pull/([1-9][0-9]*)/?$'
if [[ -n "${PR_NUMBER_OR_URL:-}" ]]; then
if [[ "$PR_NUMBER_OR_URL" =~ $url_regex ]]; then
url_repo="${BASH_REMATCH[1]}"
url_pr="${BASH_REMATCH[2]}"
if [[ -n "$repo" && "$repo" != "$url_repo" ]]; then
echo "Error: Conflicting repository target '$repo' vs PR URL '$url_repo'" >&2
exit 1
fi
repo="${repo:-$url_repo}"
if [[ -n "${PR_NUMBER:-}" && "${PR_NUMBER}" != "$url_pr" ]]; then
echo "Error: Conflicting PR number '${PR_NUMBER}' vs PR URL '$url_pr'" >&2
exit 1
fi
PR_NUMBER="$url_pr"
elif [[ "$PR_NUMBER_OR_URL" =~ ^[1-9][0-9]*$ ]]; then
if [[ -n "${PR_NUMBER:-}" && "${PR_NUMBER}" != "$PR_NUMBER_OR_URL" ]]; then
echo "Error: Conflicting PR number '${PR_NUMBER}' vs PR_NUMBER_OR_URL '$PR_NUMBER_OR_URL'" >&2
exit 1
fi
PR_NUMBER="${PR_NUMBER:-$PR_NUMBER_OR_URL}"
else
echo "Error: PR_NUMBER_OR_URL must be a valid GitHub PR URL or positive PR number" >&2
exit 1
fi
fi
if [[ -n "${PR_NUMBER:-}" && "$PR_NUMBER" =~ $url_regex ]]; then
url_repo="${BASH_REMATCH[1]}"
url_pr="${BASH_REMATCH[2]}"
if [[ -n "$repo" && "$repo" != "$url_repo" ]]; then
echo "Error: Conflicting repository target '$repo' vs PR URL '$url_repo'" >&2
exit 1
fi
repo="${repo:-$url_repo}"
PR_NUMBER="$url_pr"
fi
if [[ -z "${PR_NUMBER:-}" || "$PR_NUMBER" == *"<"* || ! "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then
echo "Error: PR_NUMBER must be set to a valid PR number before publication" >&2
exit 1
fi
if [[ -z "${repo:-}" || "$repo" == *"<"* ]]; then
echo "Error: REPO must be set to a valid owner/repo before publication" >&2
exit 1
fi
if [[ ! "$repo" =~ ^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$ ]]; then
echo "Error: REPO must be in the format 'owner/repo'" >&2
exit 1
fi
if [[ -z "${CAPTURED_SHA:-}" || "$CAPTURED_SHA" == *"<"* || ! "$CAPTURED_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "Error: CAPTURED_SHA must be set to a valid 40-character commit SHA" >&2
exit 1
fi
comment_scratch="$("${CLAUDE_HOME:-$HOME/.claude}/skills/scratch-space/scripts/scratch-dir.sh" tmp)"
comment_file="${COMMENT_FILE:-$comment_scratch/evidence_comment.md}"
if [ ! -s "$comment_file" ]; then
echo "Error: Comment body file '$comment_file' does not exist or is empty" >&2
exit 1
fi
# Verify exact intended PR before posting comment
pr_view_json="$(gh pr view "$PR_NUMBER" --repo "$repo" --json number,headRefOid,url)"
pr_number_resolved="$(printf '%s' "$pr_view_json" | jq -r '.number // empty')"
if [[ -n "$pr_number_resolved" && "$pr_number_resolved" != "$PR_NUMBER" ]]; then
echo "Error: Verified PR number mismatch ($pr_number_resolved vs $PR_NUMBER)" >&2
exit 1
fi
pr_head_sha="$(printf '%s' "$pr_view_json" | jq -r '.headRefOid // empty')"
if [[ -z "$pr_head_sha" || ! "$pr_head_sha" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "Error: Unable to resolve valid head SHA for PR #$PR_NUMBER in $repo" >&2
exit 1
fi
if [[ "$CAPTURED_SHA" != "$pr_head_sha" ]]; then
echo "Error: Declared CAPTURED_SHA '$CAPTURED_SHA' does not match verified PR head '$pr_head_sha'" >&2
exit 1
fi
target_pr="$PR_NUMBER"
gh pr comment "$target_pr" --repo "$repo" --body-file "$comment_file"
)
Optional path:
$HOME/.claude/scripts/github_pr_media_upload.pymay still be used when nativeuser-attachmentsURLs are specifically desired and a valid GitHub web session cookie is available
Behavior:
- Keeps publication fully zero-touch via
gh - Produces durable GitHub-hosted URLs that can be pasted into the PR description or comment
- Avoids dependence on a browser session cookie
PR Snippet Template
## UI Evidence
- GIF: `<reviewer-accessible location at the authorized destination>`
- MP4 ZIP: `<reviewer-accessible location at the authorized destination>`
- Captions: burned-in (or link to the matching caption artifact)
- Route: `/path/under/test`
- Commit: `<sha>`
- Claim: <what this proves>
Anti-Patterns
Reject these:
- URL bar cropped out
- Success-only clip with no applicable before→after transition or, when a user action exists, no before/action flow
- No git SHA linkage
- Missing captions
- Screenshot-only evidence for flow claims
- Manual drag-drop as the only publication path
- Media inaccessible to the intended reviewer
Authorization vs Capability (MANDATORY)
Before choosing a capture method, distinguish three separate things:
- Task authorization — you already have authorization to do routine, in-scope evidence capture; a failed OS-level capture probe does NOT mean the user withheld authorization.
- OS capability — whether one process/tool can capture the screen natively (e.g. a CoreGraphics/Screen Recording permission check).
- Evidence sufficiency — whether the required frames and provenance can be produced through an authorized, already-available alternative (e.g. a scoped headless browser driver, see "Scoped Browser Ownership" above).
Rules:
- Never run a TCC/permission reset as a diagnostic probe (e.g.
tccutil reset ScreenCapture, or any command that mutates OS privacy grants) to test whether capture works. This destroys unrelated apps' existing grants and is not reversible from inside the agent. - Exhaust all authorized, scoped, headless capture alternatives (Option 1 and Option 3 above) before concluding that human/OS-level consent is required.
- If genuine OS consent is required after alternatives are exhausted, report the exact capability gap and the alternatives you tried — do not claim the user withheld authorization and do not declare the whole goal blocked.
- Do not fabricate a browser address bar or native chrome to satisfy the Mandatory Frames table when actual capture is headless.
Reviewer Checklist
- Video is linked from the review receipt at an authorized, reviewer-accessible destination
- Any publication follows the authorized destination and applicable automation workflow
- Captions are present
- When a user action exists, the pre-action/action/post-action flow is visible on both compared implementations; otherwise the applicable before→after transition is visible
- URL and route match claim
- Actual baseline and candidate source linkage is verified, not inferred from captions
- The reviewed evidence receipt includes matching metadata/caption artifacts
- mpdecimate retained-frame count reported as kept/nominal (see Frozen-frame detection); a low count triggers provenance/timeline review, while a constructed slideshow always fails under the canonical rule
- When a user action exists, each clip contains the action itself plus the before state; otherwise each clip contains the applicable before→after transition
- Before/after provenance is fresh runs of each build, or labeled
derivedwith source /erand second-opinion (adviceskill) verdicts come from a different agent/lane than the evidence producer, and name that lane- An independent AI reviewer (not the author or orchestrator) judged frames extracted from the
published video against each acceptance step, with PASS/FAIL and timestamps;
privacy, playback, and upload checks are separate and do not substitute
(
evidence-reviewcheck 4)
Frozen-frame and slideshow detection (MANDATORY)
The canonical continuous-video prohibition is in evidence-standards. It is absolute: the motion probe below is diagnostic evidence, never permission to accept a screenshot sequence, and captions or named action frames never waive the prohibition.
Report the mpdecimate retained-frame count (kept) and the nominal frame count for every clip. The probe must fail closed: a failed decode or empty count is an error, never "zero frames".
set -o pipefail
kept=$(ffmpeg -nostdin -v error -i v.mp4 -vf mpdecimate -f null -progress pipe:1 - \
| grep -o '^frame=[0-9]*' | tail -1 | cut -d= -f2) || { echo "probe FAILED"; exit 1; }
nominal=$(ffprobe -v error -count_frames -select_streams v:0 \
-show_entries stream=nb_read_frames -of csv=p=0 v.mp4) || { echo "ffprobe FAILED"; exit 1; }
[ -n "$kept" ] && [ -n "$nominal" ] || { echo "count unavailable: FAILED"; exit 1; }
echo "kept=$kept nominal=$nominal"
A low retained-frame count triggers provenance and timeline review; it is not standalone proof of a slideshow and is not an automatic rejection of legitimate continuous footage with an idle interval. A constructed still-image sequence always fails under the canonical rule regardless of its retained-frame count, caption, or successful decode.
Action must be in the window (MANDATORY)
When a user action exists, each clip must include the user action under test (for example the Send tap) and the before state that precedes it. A capture window anchored only to completion (e.g. trimmed to the DONE state) that omits the action fails.
When the change's observable has no user action (first paint, an error state rendered on load, a theme token applied at mount), record this rule as N/A with the reason and instead provide a state-transition clip showing the before-state frame and the after-state frame.
Provenance of before/after clips (MANDATORY)
Before and after clips must come from fresh runs of each build (baseline and
candidate). Any re-cut, trimmed, or reused baseline clip must be labeled
derived in its caption and receipt, with its source (original artifact URL,
build SHA, and run). Unlabeled derived clips are INSUFFICIENT.
Reviewer independence
/er and second-opinion (advice skill) verdicts on UI video evidence must come from an agent other
than the one that produced the evidence. Self-review does not satisfy either
gate.
First-frame verification (MANDATORY)
Moved here from ~/.claude/CLAUDE.md on 2026-07-25. Applies to AGY CLI and any browser-capture pipeline.
-
Wait for page readiness before capture. Replace fixed-delay sleeps with
page.wait_for_load_state("networkidle")ANDpage.wait_for_selector(<expected-ui-element>, state="visible"). A blank/white first frame is invalid evidence — it means the page had not rendered. -
Verify the first frame visually before pushing. Extract frame 1 from the MP4 and confirm it shows the expected starting state (dashboard, modal opener, etc.), not a loading screen:
ffmpeg -i video.mp4 -vf "select=eq(n\,0)" -vframes 1 frame1.png -
Reject evidence with a white/blank first frame. Re-capture with stronger wait conditions. Do not publish a blank capture and label it as evidence.
-
Evidence Gate caveat: the
evidence-gateCI check validatesmetadata.jsonfreshness, NOT visual content. Visual verification is the responsibility of the agent presenting the evidence — a green evidence-gate says nothing about whether the video shows anything.
Capture script standard (your-project.com-specific)
Every testing_ui/capture_*.py script must use the shared browser_test_helpers.py helpers — wait_for_page_ready() and verify_first_frame_not_blank(). New scripts that re-implement waits inline are a regression.
Slack Distribution — Native Attachments Only
When distributing video or visual evidence to Slack (channels, incident threads, or DMs):
- NEVER use text-only Slack MCP tools (
conversations_add_message) to send bare paths or GitHub URLs. Text-only message tools do not render media inline. - ALWAYS use the dedicated uploader script:
python3 ../slack-media-upload/scripts/slack_upload.py [--dm | --channel <id>] --file <path> [--title <title>...] [--thread-ts <ts>] [--comment <text>] - This wraps Slack's two-step
files.getUploadURLExternal→files.completeUploadExternalAPI so videos and GIFs render directly inline in Slack.
04 同仓库更多 Skills
instinct
Use when the user invokes /instinct or /in, or asks to "message Instinct", "ask Instinct", "tell Instinct", "check Instinct", or read replies from the configured peer assistant. iMessage first, WhatsApp Web as fallback.
hr
Compatibility alias for history-resume; produce the same fresh-state, provenance-aware resume briefing.
history-resume
Resume briefing for the current repository branch or worktree using fresh state and admitted history sources.
grok
Alias for the canonical grokbot browser conversation workflow; /grok talks to the Grok bot the same way /dot talks to Dot.
codexa-bench
Compare `codexa` and normal `codex` on a fair split of comparable work by default; use `--strict` for duplicate runs of the exact same task.
frontend-design
Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.