Skip to main content
Reticle works with zero instrumentation. You get DOM, network, console and routing out of the box, and that is already more than a screenshot can tell you. But there is a ceiling, and this page is about raising it. Instrumentation is the difference between a verdict that says something appeared and one that says the application declared success.

The grade ladder

Every verdict carries an honesty.grade. How strong the evidence actually was.

presence

An element appeared, or text showed up. True, and weak: a mock renders exactly the same.

state

A registered store changed. Now you know the app accepted it, not just drew it.

signal

The app fired a named signal. The app itself declaring success. Nothing beats this.
Here is the same login, verified two ways. First with a network predicate and no instrumentation leaned on:
Then with a signal predicate, against the same app:
Same click. Better evidence. The only difference is that somebody spent ten minutes emitting a signal.

Three things to add

Everything lives in src/reticle-dev.ts, which reticle init creates for you and which is dev-only, it self-guards on import.meta.env.DEV and is a no-op in a production build.

1. Register your store

Pass the store, not () => store.getState(). The store form wires subscribe, so every mutation emits a state diff. The getter form is read-only and silently produces empty stateDiffs, which reads as “nothing changed” and actually means “I was never watching”.
That single line is what turns this up in a verdict:
A mock that returns 200 without touching state gets caught precisely there, and nowhere else.
Register your server-state cache too. “The screen is plausible and the network is silent” is the normal failure with query caches: a mutation forgets to invalidate, the UI keeps rendering a number that was true a minute ago, and nothing fires for anyone to notice. The cache’s freshness metadata is the only witness.

2. Emit signals

A signal is your app saying “this specific thing succeeded”. Define the names in one place so they cannot drift:
Then call emit(Sig.AUTH_GRANTED, { email }) where the thing actually succeeds. Not where you think it succeeds. In the success branch, after the state update, not in the click handler. Emit the failures too. auth:denied is as valuable as auth:granted, for a reason the next section demonstrates.

3. Advertise the surface

This is what reticle_capabilities returns. The app telling an agent what it considers testable, before the agent has looked at a single element. It is the cheapest and most truthful first call on an unfamiliar codebase.

Why emitting failure signals pays off

Here is a real failed login, from an app that emits both signals:
Read observed again:
signal ‘auth:granted’ never fired — signals seen in this window: auth:denied
Without the failure signal, that line would have stopped at “never fired”, and the agent would be guessing between a network problem, a broken handler and a wrong password. With it, the app has named its own outcome: the credentials were rejected. The 401 headline confirms it, stateDiffs: [] proves nothing was written, and the whole diagnosis arrives in one response.
capsule.firstDivergence and blastRadius are saved to disk automatically on a failure, so the evidence survives the agent’s context window. Look for capsuleSaved in the response.

When Reticle refuses to call it a pass

This is the one that surprises people, so here it is in full. A successful login, on a backgrounded tab:
The signal fired. State changed. verdict.pass is true. And verified is unknown, because the page never went quiet inside the observation window, so Reticle cannot promise it saw the whole story. That is the design working as intended. A tool that reports “pass” whenever it happens to catch a matching event, without knowing whether it saw everything, is how you get a confident wrong answer.
The usual cause is a throttled tab. A backgrounded browser tab has its timers suppressed, which suppresses the quiescence detection Reticle uses to decide the page has settled. Focus the tab, or drive a dedicated context. The response tells you: look for session.throttled and the accompanying warning.
Note the last field. When Reticle cannot tell what happened, it invites a bug report and calls the ambiguity its own defect rather than yours. If you hit an unknown you believe is wrong, that is worth sending.

Testids, briefly

Role and text queries survive refactors well. Testids survive redesigns, translations, and the day marketing rewrites every button label. Add them to the elements your important flows actually touch. Not to everything, which is a chore nobody finishes.

Keeping it honest

Signals rot the way every convention rots: someone adds a mutation, forgets the signal, and every verdict for that flow quietly drops from signal to presence. Nothing goes red. The tests pass. The evidence just gets weaker, invisibly.
One rule. State changed, so a signal must fire, makes the signal layer self-enforcing. It is the only mechanism we have found that survives contact with a deadline.

Start with one flow

You do not need to describe the whole app, and trying to is the slow path that gets abandoned halfway.
  1. Pick your most important flow. The one that would be embarrassing to break.
  2. Register the store it reads.
  3. Emit a signal where it succeeds, and one where it fails.
  4. Add testids to the elements it touches.
  5. Drive it, and check honesty.grade says signal.
Then stop. Add more when a flow you actually replay needs it.

Prove it

Use your new signals as predicates and watch the grade climb.

Lock it in

Turn the instrumented flow into a spec that runs on every pull request.
Last modified on August 14, 2026