A pnpm **monorepo** of the public, client-installed packages for **UI Verify** (visual testing),
A pnpm monorepo of the public, client-installed packages for UI Verify (visual testing), published as standalone open-source packages that talk to the UI Verify service only over HTTP / MCP:
This large 1,329-word instruction set is purpose-built for testing & qa work in AI coding agents. Paste directly into your CLAUDE.md or AI agent settings.
uiverify (public client packages)A pnpm monorepo of the public, client-installed packages for UI Verify (visual testing),
published as standalone open-source packages that talk to the UI Verify service only over HTTP / MCP:
packages/cli (uiverify) — the CI uploader (uiverify upload).packages/playwright (@uiverify/playwright) — the Playwright capture SDK (swap @playwright/testtsup → ESM + d.ts.
packages/skills (@uiverify/skills, private) — agent skills: plain SKILL.md workflows a coding.claude-plugin/marketplace.json; the plugin manifest is packages/skills/.claude-plugin/plugin.json).The invariants below are written for the CLI, but the cross-cutting rule — dumb client, no server-side
product logic, publishable & safe to open-source — applies to EVERY package here. The CLI's "never fail the
consumer's CI" exit-code rule is CLI-specific.
The CLI talks to the UI Verify service only over HTTP. It must NOT gain a dependency on
server-side code or service internals:
src/client.ts) — request/response shapes mirror thedist/uiverify.js (esbuild inlinestar + zod), so the published artifact carries no external runtime deps.If you ever need a helper, write it here or pull a public npm package — never reach back into
server-side code.
Every flag, env var, exported symbol and skill name in this repo is published under a version
consumers pin (npx uiverify@<ver> upload …) and mirrored to a public repo. Adding one is cheap in
the moment and permanent afterwards — removing it later is a breaking change for someone else's CI.
So implement exactly the surface that was asked for, and nothing adjacent. Confirm with the
maintainer before adding any more of it, and especially before adding a second way to express the
same thing: one option, one spelling. No env-var "equivalent" for a flag, no --no-x twin, no alias
or shorthand, unless the ask named it.
The failure this exists to stop is not one bad flag, it's the accretion, where every step looks
locally reasonable: --only-changed was specified → an unasked-for UIVERIFY_ONLY_CHANGED was added
as a "convenience" → review then noticed a job-wide env had no per-step escape → --no-only-changed
was added to serve it. Three ways to set one boolean, two of them contradictory, for an option whose
whole job is "render less this run". When a review finding says an input needs an escape hatch, the
first question is whether that input should exist at all — deleting the env var deleted the negative
flag and the entire precedence module with it.
This is a judgment call, so no lint can check it. The guard is asking first.
softFail()uiverify upload has three non-zero exits. Two are about the run; the third is about the invocation,
and the distinction is what keeps --no-strict honest:
softFail() in bin/uiverify.ts — the upload itself notUIVERIFY_API_KEY, a missing/empty --static-dir, and any
network/timeout/5xx that survives withRetry. resolveStrict() decides whether it exits 1 (strict)
or logs [uiverify] ⚠ … and exits 0 (non-strict).
--strict/--no-strict
wins; else strict.
changed/failed → exit 1) on a normal run is the other, independentexitCodeFor() regardless of strict.
softFail(): an unknown or misspelledparseArgs' invalid), or a bad subcommand.
--no-strict says "don't fail my job if the upload breaks", which is not consent to skip the upload
entirely — honouring it here would exit 0 having uploaded nothing, the exact outcome the funnel exists
to prevent. The pre-allowlist parser silently ignored unknown flags and uploaded anyway, so swallowing
this would be strictly worse than what it replaced.When adding a new operational failure path, route it through softFail() — do not throw pastmain()'s catch or call process.exit(1) directly. When adding a new argv-validation path, add it toparseArgs' invalid so it joins the exit-2 funnel. The API key is redacted from every message.
storybook-visual-testing, vitest-visual-testing, and playwright-visual-testing teach the same job —
stop flaky visual diffs — to three capture surfaces, so most of what they say is universal and must
stay identical across them: the isUIVerify() escape hatch (charts, <canvas> / requestAnimationFrame
loops), freezing the clock, non-Math.random randomness (crypto/uuid/faker), honoringprefers-reduced-motion, and above all freezing live data with fixtures — the single highest-value,
headline determinism step wherever a component renders in isolation (Vitest and Storybook; the same
lever is recipe 2/6 on a real page).
So when you add or change a universal lever in one skill, port it to the other two in the same
change, and mark which parts are universal vs surface-specific. What is deliberately NOT shared:
SSR-time Math.random() + UI_VERIFY=1 on the app server, and the global-RNG-consumed-by-render-order
→ local-seed fix, are real-app / Playwright only (Vitest and Storybook have no SSR, and the capturer
seeds Math.random before app code); scroll-settling, flag/third-party stubbing, and loading-skeleton
waits are real-page only. The failure this prevents is silent and already happened once: a real-app
determinism port improved the Playwright skill's coverage of a shared concept (the <canvas> fix, the
data-mocking headline) and left the siblings behind, so an agent reading the Storybook skill would never
learn it. No lint can diff prose against prose — the guard is porting the learning while it is in your
hands. (Also note the parity of the record-time markers: the Playwright SDK sets both the UA marker
and the window global, but the Vitest SDK's browser provider owns the context, so it sets only the
global — isUIVerify() still matches via the or, but don't claim the UA branch works for Vitest.)
The same subjects are also taught on the public product docs (uiverify.ai/docs, maintained in the
service repo — not here): a universal determinism change owes those articles a matching pass too.
pnpm build — esbuild bundle → dist/uiverify.js (gitignored). prepublishOnly runs it.pnpm test — Vitest. pnpm typecheck — tsc --noEmit.examples/ pin published versions — bump them with every releaseThe standalone example projects under examples/ (storybook, vitest, playwright, react-native)
are meant to be copied out and run against the published npm packages, so their package.jsons pin
the published versions (uiverify, @uiverify/playwright, @uiverify/vitest) — NOT workspace:.
That means they don't move when you bump a package here: whenever you publish a new version of any
client package, bump the matching ^x.y.z in every examples//package.json that depends on it* in the
same change, or the examples silently install a stale SDK. grep -rl '@uiverify/\|"uiverify"' examples//package.json
finds them; the current published versions are what npm view <pkg> version reports.
Uiverify is a free testing & qa skill for AI coding agents. A pnpm **monorepo** of the public, client-installed packages for **UI Verify** (visual testing),. It provides a specialized system prompt that configures your agent with testing & qa expertise.
Copy the system prompt below and paste it into your CLAUDE.md file or Claude Code's custom instructions to activate Uiverify.
Uiverify is compatible with Claude Code, Cursor, GitHub Copilot, Windsurf, OpenClaw, Cline, and any AI agent that supports custom system prompts or .cursorrules files.
Yes, Uiverify is completely free and open source. The full source is available on GitHub at https://github.com/uiverify/uiverify/blob/main/CLAUDE.md. You only need a subscription to the AI agent you use it with.
Weekly roundup of top Claude Code skills, MCP servers, and AI coding tips.