Drive a URL once, replay every saved flow, and exit zero only on a real pass.
reticle verify is the CI command: no model in the loop, no MCP. It boots the engine in drive mode, waits for the in-page SDK to dial back, replays every saved flow, renders the verdict, and exits.
It uses the same runner and the same verdict as the MCP and HTTP paths, so a platform agent that can only run a shell command gets a byte-identical artifact.
The rendered run report on stdout. On failure it names the cause on stderr instead, and there are two refusals worth knowing:
No app connected: Reticle drove the URL but no @reticlehq/browser session dialed back. Make sure the SDK is in the build and reticle.connect() runs on the preview page (for a non-localhost preview: allowNonLocalhost + a pairing token).
No saved flows to verify (.reticle/flows is empty), so refusing to report a pass for verifying nothing. Flows are recorded interactively by an agent (reticle_record{action:"start"} → act → reticle_flow_save via the MCP tools), then committed to .reticle/flows/. In CI, check those files in and re-run `reticle verify`.
Both of those are honesty guards, and both exit 1. Verifying nothing is never a pass.
A URL that does not answer is not one of those two. It surfaces as the underlying Playwright error, prefixed with verify failed:, and also exits 1. Real capture:
verify failed: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:59999/Call log: - navigating to "http://localhost:59999/", waiting until "domcontentloaded"
verify binds the bridge port itself and has no --port flag: passing one is rejected as
unknown argument '--port'. If a daemon is already on the port, verify does not start a second
one: it prints three ways out (ask the running daemon via reticle_run, stop it, or move with
RETICLE_PORT) rather than a raw listen error. This bites most often on a developer machine,
where an agent has usually left a daemon running; in CI the port is normally free.