Skip to main content

Reticle — agent cheat-sheet

One screen to get fluent. Reticle is the proof layer for AI agents — no screenshots, no vision model, evidence not prose. Everything below returns structured data. Full guide: usage.md.

The core loop: look → act → observe → assert

Only reticle_act_and_wait and reticle_assert produce a verdict. Everything else moves or reads the app and proves nothing, so a drive ending without one of those two has no result however many tools it used. reticle_act is the tool agents reach for by habit and it catches nothing; act_and_wait is where defects actually surface. And verified: "unknown" is not a pass — it means Reticle drove the app and could not tell what happened. Report it as unknown.
reticle_act returns a since cursor — pass it to reticle_observe({ since }) to scope the window. Elements are addressed by stable refs (e7) from snapshot/query; they re-resolve across re-renders. assert/wait_for are auto-scoped to your last act. By default they only count events buffered since the most recent act, so a stale signal from a previous step can’t fake a pass — pass an explicit since to override. Clicks run the code, not pixels: reticle_act click fires the full pointer sequence on the element (no coordinate gesture for the HUD to intercept), reports occluded:true when something covers the target, and stays synthetic even with CDP configured (use args:{ native:true } for a trusted native click). Never sleep — wait deterministically. Fixed sleeps are the #1 cause of flaky agent tests. Instead:
  • reticle_act_and_wait({ ref, action }) with no until waits for the page to settle (network + structural DOM idle; ambient count-up/spinner churn is ignored so an animated page still settles) before returning — the one-call replacement for “click then sleep 500ms”.
  • Need to wait without acting? reticle_wait_for({ predicate: { kind: "settled", quietMs } }).
  • Waiting for a specific outcome? Pass that consequence as the predicate ({ signal } / { net }), or allOf it with { kind: "settled" } to wait for both the event and the page going quiet.
A predicate that does not parse produces NO verdict — not a failing one. Nothing runs, so the drive ends with no result at all, which is strictly worse than a failure. The shapes below are the ones agents reach for most; the first column now works, but knowing the second saves you the round trip: If a predicate is still rejected, the error names the fields that kind accepts — read it rather than guessing again, and note state spells its selector path while route spells its pathname. Assert a consequence, not just presence. { signal } / { net } prove the feature actually did something; { element } / { text } only prove something is on screen — which a stale render or a locator healed to the wrong element can fake. A passing presence-only reticle_assert returns advice nudging you to a consequence; heed it on anything that matters.

The 4-layer cross-check — never trust a green the state contradicts

A claim is real only when the layers agree. Check more than the UI:
Rule: a passing UI assert that the store, network, or signal contradicts is a false green.
Session health is universal. Every live-session tool result carries a session block (throttled, focused, lastSeenMs); when throttled:true it also adds a warning + recommendation (refocus, or reticle drive). A throttled/backgrounded tab can silently no-op timers/rAF/pointer gestures — if you see session.throttled, distrust a green and refocus first.
Store reads (reticle_state) are the reliable path; the DOM can lie (optimistic UI, stale render).
Truncation and coverage are declared, never silent. A big reticle_state read carries truncation when the transport caps dropped items — its presence means “this is NOT the whole value”, so an absence assertion over a truncated read proves nothing; scope with path/depth instead. A reticle_assert verdict carries coverage when part of the page was unobservable (cross-origin iframe, closed shadow root) — a green then means “nothing failed in the part I could see”, not “the page is correct”. Both fields are omitted when everything was fine, so their presence is the warning. Charts report their own geometry faults. Any element descriptor containing a broken plot carries chart: [{ kind, tag, attr, sample }]non-finite-coordinates (a zero-range scale divided by zero; always a bug), empty-geometry (mounted but no data reached it), degenerate-geometry (every point identical). No extra call and no flag: query the chart as you would anyway. A healthy chart adds nothing. This matters because a chart is the one widget whose correctness is geometry — the store can be right while the polyline is blank, and neither a state read nor a screenshot catches it. Canvas charts are pixels, not DOM: read their data with canvasChartData(canvasEl, window) instead. Reads never go silently empty. A zero-result read returns a hint, not a bare []: reticle_query{ route, presentTestids, knownEmptyState }; reticle_network{ totalInWindow, present[] } (what DID fire); reticle_console{ totalInWindow, byLevel } (so “0 errors” ≠ “silent page”); reticle_state lists storeNames when a store isn’t found. Read the hint before assuming “not there.” Scope big stores with reticle_state({ store, path:"a.b.0", depth }) instead of paying for the whole tree; a wrong path returns { found:false, availableKeys } so it’s self-correcting.

Core tool set

The tools advertised DIRECTLY — what you’ll use 90% of the time. Everything else is one reticle_run away (see Token note below). reticle_sessions · reticle_navigate · reticle_snapshot · reticle_query · reticle_act · reticle_act_sequence · reticle_act_and_wait · reticle_observe · reticle_network · reticle_console · reticle_wait_for · reticle_assert · reticle_state · reticle_inspect (DOM node → src/App.tsx:104) · reticle_feedback (tell the maintainers what is missing) · reticle_session (hand back: {action:"yield"} the moment you stop driving, {action:"resume"} after a human pause). Frequently useful but NOT core, so reach them through reticle_run({ tool, args }): reticle_domain (learn the app + gaps), reticle_capabilities (the app’s declared testable surface), reticle_baseline {action:"diff"}, reticle_project (run history). Reach past core when… you need to record/replay a journey (reticle_record {action:"start"}/stop, reticle_replay), persist a self-healing golden flow (reticle_flow_save* / reticle_flow_replay / reticle_flow_heal), compile annotations (reticle_annotate), explore autonomously (reticle_explore lists controls; reticle_crawl clicks them all and reports anomalies — destructive), reveal a virtualized off-screen row (reticle_scroll_to — when reticle_query finds nothing because a windowed list hasn’t rendered it yet), visual-check (reticle_screenshot / reticle_visual_diff, pinned with reticle_viewport for reproducible baselines), test error/edge states by stubbing the network (reticle_network_mock — 500 / offline / delay, driven mode), control time for toasts/debounces/auto-dismiss (reticle_clock { freeze | advanceMs | reset }), or work with a human (reticle_session {action:"end"} / reticle_session {action:"resume"} / reticle_session {action:"messages"}, and reticle_session {action:"review"} to drain + fix the bugs the human flagged from the panel).

flows vs baselines vs project.json (the persistence layers)

reticle_project / project.json are the run-history layer. flows answer “does the journey still work?”; baselines answer “did the structure change?”; project.json answers “is this run consistent with prior runs?”.
Visual layer (opt-in). reticle_screenshot saves a PNG baseline to .reticle/visual/<name>.png; reticle_visual_diff perceptually compares the live page to it ({ masks } to ignore volatile regions, { maxRatio } tolerance) → { matched, changedPixels, ratio, region, diffPath }. It answers “does it look right” — complementary to the behavioral layers, never a replacement. Both need a driven browser (reticle drive <url> / RETICLE_CDP_URL); without one they return { ok:false, reason:"no-visual-provider" } (the always-on SDK ships no screenshotter).

Start here

  1. Just ran reticle init / started the dev server? Poll reticle_sessions() until your tab appears — readiness is server-internal now, so the first live call already blocks until the SDK connects.
  2. reticle_sessions — find the connected tab (omit sessionId if there’s only one). An empty list is not a dead end: read the why field. It names which case this is — no app running, an app running that has never dialled this daemon, a project that never went through init, or a tab that closed — and the fix for each. Do not fall back to static reasoning until you have read it.
  3. reticle_domain — learn the app BEFORE testing: the saved flows, what each asserts, and the gaps (declared signals/testids that no flow verifies — untested intent). Tells you what to test and where the real risk is without crawling the whole app. Falls back to reticle_capabilities for the raw testable surface (testids, signals, stores, flows).
  4. Run the loop: look → act → observe → assert, cross-checking the 4 layers on anything that matters.

Token note

  • Keep observation cheap. Prefer reticle_query / scoped or interactive reticle_snapshot / reticle_assert over dumping the full tree. A full verify loop is ~100 tokens; see token-efficiency.md (~73× leaner than full-tree snapshots).
  • Re-look with reticle_snapshot({ diff:true }) after an action — it returns only what changed (mode:delta/unchanged), ~99% fewer tokens than a full re-snapshot and no stale tree to mis-read. Every snapshot/query result carries cost:{ bytes, tokens } — re-scope before reading if it’s large.
  • Cap broad reads. reticle_query takes limit (caps descriptors; reports total/truncated) and count_only (just the match count). reticle_network / reticle_console take limit (most-recent-N, reports droppedOldest) and carry the same cost hint — so a busy page or wide window never floods your context unnoticed.
  • A saved flow tells you if it’s a real test. reticle_flow_save returns assertions.grade (asserted / presence-only / assertion-free); if it’s not asserted, add a consequence (reticle_annotate assert-signal/assert-net or a success-state) so it can’t pass while broken. On replay, an ambiguous heal (two testids tie) is surfaced, never auto-applied — and an apply heal re-replays the rebound flow and refuses to write if the success consequence no longer fires (status:consequence_broken): it heals the locator, never the intent.
  • Predicate schema is not bloated. The recursive predicate DSL used by reticle_assert / reticle_wait_for / reticle_act_and_wait is factored, not inlined: when converted to the JSON Schema MCP sends, the predicate body is emitted once (~2.7k chars ≈ ~685 tokens per tool) and recursion is handled by self-$ref (#/properties/predicate) — no per-recursion duplication. No action needed.
  • One tool surface. Reticle advertises the core set directly (sessions/navigate/snapshot/query/act/act_and_wait/observe/network/console/wait_for/assert/state/inspect/feedback/session — the whole detect loop, plus the file-pointer, the feedback channel and the handback) PLUS two meta-tools that keep every other tool one call away: reticle_tools (discover — no args lists every tool name + summary; names:[…] loads full params on demand) and reticle_run({ tool, args }) (invoke any tool by name; a top-level sessionId is forwarded to the target, so you need not nest it in args). So to record/replay/verify a flow, call reticle_run({ tool:"reticle_flow_verify", sessionId }) (or reticle_tools first to see params). There is nothing to pick. RETICLE_ADVERTISE_ALL_TOOLS=1 (read by the daemon at startup, so restart it) advertises everything with output schemas — a verification switch for suites, not a way to run agents. Sizes are deliberately not quoted here — a count in prose goes stale and has three times already. reticle_tools reports the live surface, and SKILL.md carries the one gated table.