On this page
Your coding agent says the order form works. This tutorial builds the thing that checks: a small verification harness around a real app, using Reticle. By the end you have one recorded flow, a command that replays it and exits 0 or 1, a deliberate bug it catches, and a gate that blocks the merge. Every command below ran on Reticle 3.8.2, and every output is pasted from that run.
If the term is new, a test harness is the code that drives the thing under test and judges the result. This page is the hands-on half of that testing harness explainer: less theory, more terminal.
You need Node 20.11 or newer and a web app with a dev server. We used a fresh Vite React app.
1. What you will build
The app is a one-screen mug shop. You pick a quantity, press Place order, the client posts to /api/order, and the page shows Order 1002 confirmed: 3 x mug. The harness proves that journey from four angles, saves it, and replays it on every change.
The app's whole state is one component. The only Reticle-specific line is useReticleStore, which lets the harness read what the app believes, not just what it painted:
const [qty, setQty] = useState(1)
const [order, setOrder] = useState<Order | null>(null)
if (import.meta.env.DEV) useReticleStore('cart', { qty, order })
Keep the DEV guard. Without it, our vite build of this app rendered a blank page in 3.8.2: the production stub of the hook threw Cannot read properties of undefined (reading 'store'). We reported it; the guard is safe because the condition never changes within a build.
2. Install
One global install puts the reticle command on your path:
npm install -g @reticlehq/server
changed 106 packages in 3s
That says "changed" because our machine already had 3.8.2; a first install says "added". Then wire the app, from the folder with its package.json:
reticle init
Show the output (trimmed: the MCP registration lines, the prompt-capture notice and the next-step hints)
added 46 packages in 4s reticle init [✓] Agent verification rule → CLAUDE.md [✓] Agent verification rule → AGENTS.md [✓] Full agent rules → RETICLE.md [✓] The /reticle command → .claude/commands/reticle.md [✓] Install dependencies → package.json [✓] Reticle config → .reticle.json [✓] Vite plugin → vite.config.ts [✓] Capabilities + store → src/reticle-dev.ts starting: npm run dev ✓ Connected to http://localhost:5173 (setup started the dev server). Onboarding is done; nothing is verified yet.
The run also printed an instrumentation score of 87%, missing stable test ids. init found no data-testid attributes, so we added data-testid="qty" and data-testid="place-order" before recording. reticle init --dry-run shows the plan without writing anything, and --no-mcp skips the agent files.
3. Record your first flow
Open your coding agent in the app folder and ask it to prove one flow. init prints a prompt for this. The agent starts a recording, fills the quantity, and clicks with the outcome declared up front:
{
"target": { "testid": "place-order" },
"action": "click",
"until": { "kind": "allOf", "predicates": [
{ "kind": "net", "method": "POST", "urlContains": "/api/order", "status": 200 },
{ "kind": "text", "contains": "confirmed: 3 x mug" },
{ "kind": "state", "store": "cart", "path": "order.qty", "equals": 3 },
{ "kind": "console", "level": "error", "absent": true }
]}
}
The first attempt was refused: potentially destructive action blocked; retry with args.confirmDangerous=true. Reticle reads "Place order" as a consequential label and wants it confirmed. Fair, for a button that spends money.
Saving the recording writes .reticle/flows/<project>/place-order.json: two steps, each bound to a test id rather than a CSS path, with the four checks on the click. Commit it. Session journals in .reticle/ hold request bodies, and the .reticle/.gitignore that init writes keeps them out of git.
Open the saved file once. In 3.8.2 the saver rewrote our literal text check into a pattern (confirmed: \d[\d,.]* x mug), and that pattern failed on replay against a page that plainly showed the text. We put the literal back and reported it. Edit the file like any other test.
4. Read the verdict
The agent's click came back "verified": "yes", with one piece of evidence per signal (trimmed):
"evidence": [
{ "method": "POST", "url": "/api/order", "status": 200, "initiator": "fetch" },
[{ "role": "status", "name": "Order 1002 confirmed: 3 x mug", "source": "src/App.tsx:28" }],
{ "store": "cart", "path": "order.qty", "value": 3 },
{ "absent": true }
]
That is the network result, the DOM outcome, the application state and a clean console, in that order. Now replay it from a terminal, with no agent and no model:
reticle verify http://localhost:5173 --confirm-dangerous
status: pass
**Reticle result: proved** · 1 of 1 check held
- Covered: place-order
- Metrics: 1 flow replayed · 1.2s
exit=0
The summary line, trimmed here, adds two caveats. Only one of the two steps declares a consequence (the fill is driven, not checked). And the flow is "not replayable as saved" because it creates real orders, so a red on it could be the data rather than the app. Without --confirm-dangerous the replay stops at the same guard and fails.
5. Break something on purpose
This is the bug agents write: a value hard-coded during a refactor. Change one line in placeOrder:
- body: JSON.stringify({ sku: 'mug', qty }),
+ body: JSON.stringify({ sku: 'mug', qty: 1 }),
The page still looks fine. The field says 3, the server answers 200, and the confirmation renders. It just says the wrong number:

Run the same command:
status: fail
**Reticle result: failed** · 1 of 1 check did not hold
- Covered: place-order
- Did not hold: place-order
...
"whatChanged": "no element matched {\"text\":\"confirmed: 3 x mug\",\"matchCase\":true}",
exit=1
Exit 1 is the whole point. Harness tests that only checked status codes would have passed this build.
6. The repair packet
The replay tells you what changed. To get the line, the agent drives the same click live and reads the full verdict:
"verified": "no",
"source": "src/App.tsx:27",
"failureReason": "no element matched {\"text\":\"confirmed: 3 x mug\",\"matchCase\":true}",
{ "failureReason": "state 'order.qty' is 1, expected 3", "observed": "order.qty = 1", "expected": "order.qty = 3" },
"initiatorStack": "at placeOrder (http://localhost:5173/src/App.tsx?t=1793187444920:15:21)"
src/App.tsx:27 is the button. Its onClick is placeOrder, the function the request came from, and the store says the order went out with 1 instead of 3.
An agent with that packet does not guess: it opens placeOrder, finds qty: 1, restores qty, and replays. Ours went back to status: pass and exit=0.
Reticle also saved the failure under .reticle/capsules/, so the same packet survives the agent's context window.
7. Run it in CI
verify is the test. reticle gate is the merge rule: it fails when you changed code a saved flow covers and no passing run covers the change. Here it is three times, against the broken build, after the fix but before re-verifying, and after verify passed:
$ reticle gate
{"event":"reticle_gate","pass":false,"uncovered":["place-order"],"quarantined":[],"coverage":{"covered":0,"total":1,"pct":0}}
exit=1
$ reticle gate
{"event":"reticle_gate","pass":false,"uncovered":["place-order"],"quarantined":[],"coverage":{"covered":0,"total":1,"pct":0}}
exit=1
$ reticle verify http://localhost:5173 --confirm-dangerous
status: pass
exit=0
$ reticle gate
{"event":"reticle_gate","pass":true,"uncovered":[],"quarantined":[],"coverage":{"covered":1,"total":1,"pct":100}}
exit=0
The timestamps are cut from those lines. In a pull request workflow the same two commands run after your app starts. This is the pattern from the gate docs; we ran the commands locally, not inside GitHub Actions:
- run: npx @reticlehq/server verify http://localhost:4173 --confirm-dangerous
- run: npx @reticlehq/server gate --since ${{ github.event.pull_request.base.sha }}
One catch we hit: after our early runs, which mixed passes and failures, Reticle's flake ledger quarantined the flow, and quarantined flows never block. The gate passed at 0% coverage with "quarantined":["place-order"]. The list prints on every run, so read it.
8. Move to the platform (optional)
Everything so far ran on one laptop with no account, and the results stayed there. Every run said so: "Kept on this machine only. The Reticle platform adds run history, your team and Reticle Harness". The CLI does send anonymous usage counts by default, and reticle telemetry disable turns them off.
Those are the three reasons to move: a history of runs to compare against, results your team can see, and checks that run off your laptop. The GitHub Action that posts one check per pull request needs a workspace API key, which is the platform.
Start a 14-day trial if that is where you are. If you want to stay local, the open-source SDK is everything you used above.
For what a check like this should prove before you trust an agent's "done", see AI verification. For the bugs worth recording flows against first, see testing AI-generated code.
Fun fact
HTTP has a status code for a shop like this one. RFC 2324, published on 1 April 1998, defines the Hyper Text Coffee Pot Control Protocol and says any attempt to brew coffee with a teapot "should result in the error code "418 I'm a teapot"". It adds that the response body "MAY be short and stout" (RFC 2324).
Conclusion
You now have a verification harness in its smallest useful form: one flow that checks network, DOM, state and console, a replay that exits 1 when the app lies, and a gate that will not let an unverified change merge. The broken build passed a status-code check and failed this one.
Next, record the flow your users would notice first if it broke, and commit it today.
FAQ
What is a verification harness?
A verification harness is a test harness that judges outcomes, not steps. It drives the running app, then checks what actually happened across the network, the page, the app's state and the console. It returns pass, fail or cannot tell, with the evidence attached, so a person or an agent can act on the result.
How do I build a test harness for a web app?
Build a test harness for a web app by choosing one real journey, driving it in the running app, and asserting the outcome you expect. Save it so it replays without manual steps, run it on every change, and make CI fail when it fails. The tutorial above does this in eight steps with Reticle.
Do I need an account to run Reticle?
You do not need an account to run Reticle locally. In this tutorial, install, init, recording, replay and the gate all ran with results kept on the machine, as every run reported. An account and the platform add run history, team visibility and checks that run off your laptop, including the GitHub Action.
Why did the replay pass when the page showed the wrong quantity?
It did not pass: the replay failed with exit 1, which is why the flow checks more than the status code. The server answered 200 and the console stayed clean, so those two checks held. The confirmation text and the cart state both showed 1 instead of 3, and either one was enough to fail it.


