Skip to content
Back to skills

Verify Live

ASecurity

Drive proofcut's web UI in a real headless browser over CDP — clicks with dwell and hit-testing, drags, viewport overflow probes, console capture, canvas readback. Use whenever a UI change has to be verified the way docs/plans/STUDIO.md requires, rather than through DOM stubs or the HTTP tests.

  • 4 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 19, 2026
ai-agentspythonrustgoshellnodeawstestingdebugging

Works with

  • cli

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned September 23, 2026

npx -y skills add tydude001/proofcut --skill verify-live --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Verify Live?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Verify Live
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tydude001-verify-live/badge)](https://www.skillsdirectory.com/skills/tydude001-verify-live)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: verify-live
description: Drive proofcut's web UI in a real headless browser over CDP — clicks with dwell and hit-testing, drags, viewport overflow probes, console capture, canvas readback. Use whenever a UI change has to be verified the way docs/plans/STUDIO.md requires, rather than through DOM stubs or the HTTP tests.
---

# verify-live

`tests/test_webui_http.py` speaks HTTP to a real socket and proves routing.
It cannot prove a gesture works. docs/plans/STUDIO.md's bar for every UI item is the
real project in a real browser, every click driven at **0ms and ~120ms**
dwell — a rule that exists because a fix once read green at 0ms and was dead
in the hand (HISTORY.md § The dwell-timing lesson).

`cdp.mjs` is that harness, kept here because it has been rebuilt from scratch
in more than one session. Node 24's global `WebSocket`, no dependencies.

## Run it

```sh
# 1. serve a project (a COPY of anything real — check it first with film-check)
#    Check the port first: a server another session left there answers the
#    goto below with ITS project, and the new one's bind error is only in its log.
ss -ltnp | grep :8793 && echo "8793 taken, pick another port"
uv run proofcut -C /path/to/proj web --port 8793

# 2. a browser to drive, left running between calls
~/.cache/ms-playwright/chromium_headless_shell-*/chrome-headless-shell-linux64/chrome-headless-shell \
  --remote-debugging-port=9444 --headless --disable-gpu --no-sandbox \
  --window-size=1400,900 about:blank &

# 3. drive it — each call attaches to the same page, so state persists
node cdp.mjs goto http://127.0.0.1:8793/
node cdp.mjs eval '(() => document.querySelector("#truth-strip").textContent)()'
node cdp.mjs click "#finish-render" 120      # dwell in ms; 0 and ~120 both
node cdp.mjs dragxy 81 327 145 326 120       # press, move in steps, release
node cdp.mjs viewport 700 900                # resize; two sweeps (see below)
node cdp.mjs key "?" - 120                   # a real key press; `-` = no focus target
node cdp.mjs key Escape "#agent-prompt" 0    # ...or press it with focus in a field
node cdp.mjs console 3000                    # console errors for N ms, each with its url
node cdp.mjs shot out.png
```

`CDP_PORT` overrides 9444.

## Capturing the README screenshots

**Do not drive this by hand — run `scripts/capture_screenshots.py`.** It has
drifted three times, and the cause was the same each time: the theme seed, the
viewport and the demo project's state were re-derived from memory and one of
them got forgotten. The script is this section's prose turned into code, and
it ends with the check that would have caught every one of those rounds.

```sh
uv run python scripts/capture_screenshots.py          # build, serve, render, shoot
uv run python scripts/capture_screenshots.py --reuse  # keep the demo project
uv run python scripts/capture_screenshots.py --keep   # leave the page up to poke at
```

What it encodes, and what to keep true if you change it:

- **The theme is seeded before the page loads, never toggled after it.**
  `theme.js` is the one classic script in `<head>` and applies `data-theme`
  before paint, so a toggle afterwards is a repaint the two canvases only
  follow via its `proofcut:theme` event. Seeding means `localStorage`, which
  needs the origin, so it is goto, set, goto — `seed_dark`, which then
  asserts on `data-theme` rather than trusting the write.
- **Confirm the seed took, and then confirm the set agrees.** `#theme`
  reading `☾` says the attribute is set; only mean luma across the set says
  they read as *one theme*, which is `check_set` and is the check a recapture
  twice did not run. The shots are mostly dark b-roll on a mostly dark page,
  so a wide spread is a light capture wearing dark footage — HISTORY.md § The
  screenshots went back to dark.
- **Render once from Finish** (`#finish-render`). That one SSE stream fills
  both Finish's stage report *and* the Edit agent pane's completion card — the
  card is `handleRenderEvent`, not an agent run, so no `claude -p` is needed.
  It does not survive a reload, so the theme is seeded **before** rendering.
- **Frame is shot at its own content height, not at 900.** `fit_viewport`
  measures `#frame-view` and resizes twice, because the first resize reflows
  what it measured. Captured flat at 1400x900 the pane came out 44% dead
  black, which beside a full Edit shot reads as a different, emptier product
  — HISTORY.md § The screenshots stopped being captured by hand. A scroll
  container is measured by its `scrollHeight`: `#frame-rows` is one, so its
  box says nothing about how much is in it.
- **`check_timeline_width` is a regression test, not a capture step.** The
  repo has no JS test harness, so the assertion that the ruler matches
  `#track-lanes` is the only thing holding down HISTORY.md § The timeline
  re-measures when Edit is looked at — a render landing while Edit is hidden
  used to lay every lane out at `computePxPerSec`'s 800px fallback, and the
  render this script drives from Finish is exactly such an event. If it ever
  fails again, `timeline.js`'s `mode` listener is what regressed.
- **Edit's highlighted word is `.w.playing`, the playhead's, not `.w.sel`** —
  clicking a word to seek raises the `.selection-toolbar` over the transcript
  and Escape does not lower it (`refreshToolbar` hides on a null selection,
  and the only gesture that nulls one is a mousedown outside any `.w`). The
  script seeks the `<video>` directly, which leaves the playhead highlight and
  no toolbar.

Finish mode is no longer in the README — the Edit shot's agent pane already
draws the same Export-complete card off the same stream — so the script does
not shoot it. It still *drives* the render from Finish, because that is what
fills the Edit card. If it comes back, the two things the old capture had to
relearn: the player is not in the page until `Watch` is clicked, and
`#finish-view` is its own scroll container, so the report sits above the
picture at its own `scrollTop`, not the window's.

## What it will not do for you

- **`click` refuses a target it cannot hit.** It asserts
  `document.elementFromPoint` at the point it presses, because `el.click()`
  skips hit-testing and reads green on a control nobody can reach.
- **Whether a screenshot contains `<video>` is not settled** — black on
  2026-08-09, composited on 2026-08-19 and 2026-08-24, same binary. So a black
  capture proves nothing and a good-looking one proves nothing: `drawImage`
  into a canvas and read the pixels, and only after
  `!seeking && readyState >= 2`.
- **`viewport` reports two sweeps and you need both.** `overflowing` walks the
  page and skips anything inside a scroll container — without that skip every
  ruler tick and clip block is a finding, which is a probe that gets ignored.
  `scrollers` is each scroll container against its *own* `clientWidth`, which
  is the only way a pane that clips a value mid-word ever shows up. In Edit
  mode expect exactly one, `#track-lanes`.
- **`key` sends virtual key codes, and that is not cosmetic** — a code-less
  Escape reaches a JS listener but not Chrome's `<dialog>` close watcher, and
  reads as a bug in the page. Measurement in wiki `tooling.md` § Headless
  browser. It also takes a key NAME, so the transport is `key "Space"`; a
  literal `key " "` is silently a no-op, which reads as space-to-play being
  broken.
- **A lazy image is not a broken image.** Measure `naturalWidth` only after
  scrolling the element's *real* scroll parent; through the wrong one, 30
  perfectly good tiles read exactly like a route that 404s.

Every mechanic and its measurement: wiki `tooling.md` § Headless browser.

Files in this skill

  • SKILL.md7.3 KB
  • cdp.mjs13.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…