Skip to content

Reticle

An MCP server and a dev-only SDK that let a coding agent read and drive a running web or desktop app from the inside, then answer with a verdict and the file and line to fix instead of a screenshot.

Screenshot of Reticle
Editor screenshot, 30 Sep 2026Reticle ↗

What it is

A verification tool that a coding agent calls as MCP tools. In development the app embeds a small SDK that watches the DOM, network, console, routing and framework state over a local WebSocket; the agent looks, acts, observes and then asserts what should be true, and gets back yes, no or unknown with the evidence attached and the source file and line to fix. Nothing is graded from a screenshot: the reads are the accessibility tree, the network log, the console and the store, which is where the failures that never reach the pixels live. It ships as a CLI, an MCP server, build plugins for Vite, Next and Babel, and adapters for Electron, Tauri and command-line subjects; a flow is recorded once and replayed with no model in the loop, and a CI gate refuses to pass an edit to a covered file without a covering verdict.

Who built itThe account divshekhar wrote 2,226 of the repository’s 2,602 commits, which is 86% of everything in it; the next human contributor has 56. The README invites readers to book a call with “the founders”, plural, and the roadmap speaks as “we”. 1,890 commits carry a co-author trailer, and 934 of those name Claude Opus 5 with a one-million-token context, ahead of Claude Opus 4.8 at 661 and Cursor at 32.

How it is put together

The parts · 6

The shape is three moving parts and one contract. A dev-only SDK inside the page instruments the DOM, network, console, routing and framework state and dials a WebSocket; a Node process holds the bridge, the MCP server and the CLI; a .reticle/ directory in the repository holds flows, baselines and run artifacts as reviewable files. The monorepo is split so that each boundary enforces a rule rather than a preference: the browser package may not import a Node API, the server package may not import a DOM API, the contract sits at the bottom of the graph and depends on nothing but a schema library and the protocol, and the code that decides a verdict has no browser, daemon or CLI attached to it — which is what lets the same rules be exercised by a conformance runner and read by somebody auditing the claim. Two consequences run through the rest of the code. Because a verdict is only as good as its provenance, the identity of the thing under test is a first-class value — the instance that dies, the epoch that moves on a hot update — and every observation is scoped to it. And because the interesting failures are the ones nobody witnessed, honesty is treated as a feature with tests: an assertion has to say which tier of evidence it used, and unknown is a result rather than a missing one.

core/ and open-verification/
The bottom of the graph: every constant and schema that crosses the browser, the bridge and the agent lives in core (160 files, about 1 MB), which depends only on zod and on the protocol package. That protocol is its own published thing — a specification of about 65 KB, permanent requirement identifiers, an adjudication vector file, a conformance runner, its own governance, versioning and change process — because the rules that decide a verdict are the part a sceptical reader most wants to audit.
adapters/realm/browser/
The in-page SDK, and the largest adapter at 370 files: observers for DOM, network, console, routing, storage, animation, dialogs, downloads, focus, performance and frames; the action and query layers; accessibility-name computation; ref addressing and source stamps; occlusion and paint context; and an optional presenter the human can watch, annotate and take over.
server/src/
The Node half and, by file count, the centre of the repository: 1,322 files and about 8.9 MB covering the WebSocket bridge, the MCP surface and its proxy, the reticle CLI, the daemon and its lifetime rules, the flow and run stores under .reticle/, the browser pool that leases a tab, the diagnosis of a missing session, and a telemetry contract big enough to have its own 59 KB document and a test that enforces it.
engine/src/
The rules that decide a verdict, kept apart from anything that can reach a browser or a socket: 180 files of contradiction detection, evidence handling, predicate evaluation and windowing, watched by a test that the rules stand alone and another that their coupling to core only shrinks, with a matching neutrality test on the server side.
init/ and adapters/build/
The two halves of an install. init is a build-time codemod with no runtime: it detects the framework and the dev script, plans the edit, patches Vite, Next, Remix, Astro, Nuxt, Angular, CRA or Electron configuration, registers the MCP client, and refuses to call itself finished before a session has connected. The build adapters stamp source locations for Babel, Next and Vite, and the Vite one also injects the connect call.
Desktop realms and the apparatus
Electron with a main-process adapter and an 11 KB preload script, Tauri with per-OS Rust capture modules outside every JavaScript gate, and a command-line realm that supervises a process from outside. Around the product sit its instruments: bench/ with 75 harness files and published scorecards, break/ with hostile environments, conformance/ driving the protocol against implementations, apps/e2e/ with more than forty end-to-end specs, fifteen skill directories written for agents, and 64 documents under docs/.

Choices, and what they beat

  • Structured reads rather than screenshots over taking a screenshot and looking at it

    A screenshot is about 1,365 image tokens per look, it is slow and non-deterministic, and it is blind to everything that is not visual — the request that failed, the console error, the route that did not change. The architecture document argues the reads should be the accessibility tree, the network log, the console and framework state instead, which is cheaper and sees the bugs pixels cannot carry.

  • Assert the consequence, not the appearance over asserting that an element matching a selector is present

    Called the weakest possible oracle: a wrong element, or one healed to the wrong element, satisfies it and the regression ships anyway. Evidence is therefore graded in tiers and only a consequence grade can buy a yes, and the protocol adds that evidence for a consequence must not come from the channel that performed the action.

  • One wire contract at the bottom of the graph over writing the same string in the browser and in the server

    The two runtimes have to agree exactly on every message, and the stated temptation is to put a literal such as the network-request event name in both places, which “is how drift and silent breakage start”. Every such string and shape lives once as a named constant plus a schema that both sides import, and the server validates every inbound message against it rather than trusting the caller.

  • Advertise a small tool surface over advertising every tool the server can dispatch

    MCP re-sends every tool definition to the model on every turn, so the list is rent rather than a one-off purchase, and a wide surface also makes the model wander. Retired tools were merged into an action argument rather than deleted, and the project measures its own surface while admitting that the figures it once published had gone stale — the same document still heads a section “The default 19” over nineteen names whose first line says nine.

  • Record a flow once and replay it with no model in the loop over re-driving the whole suite with an agent on every run

    Replay re-resolves each element’s durable anchor against the live DOM and re-asserts the declared consequence, so a re-verification costs tens of tokens instead of a full drive — 47 against roughly 120,000 in the published re-run, and 128k against 12.1M over a hundred runs of a four-flow suite. Self-healing rebinds a drifted anchor only if the consequence still fires, which is what keeps it from healing to the wrong element.

  • A new guard test needs a named incident over adding a guard because something could drift

    Written as a rule with its own reasoning: cheap guards accumulate, they rarely fail in a way that teaches anything new, and “this could drift” is not a defect. Two of the three defects CI found in one release were in code that already had a guard pointed at it, and both guards passed, because they had asserted that a path existed and that a bundler was found — existence was never the question.

  • Open rules, a source-available server over one licence across the whole repository

    The protocol, the wire contract, the engine and the SDK are Apache-2.0, and the server and CLI are under the Functional Source License, which each version leaves two years after its release. The stated reason is that the part deciding a verdict is the part a sceptical reader most wants to audit, so it is the part with no licence in the way, while enterprise features sit behind a key that is free for development and evaluation and activates offline.

Read fromdocs/architecture.md (about 12 KB) for the moving parts and the four design decisions, CLAUDE.md (about 19 KB) for the monorepo layout, the service boundaries and the non-negotiable rules, open-verification/SPEC.md for the protocol, docs/tools/overview.mdx for the surface and its measured cost, the README for the licence split and the safety properties, and the complete directory tree with file sizes.

Build log

6 stages
  1. 01

    A repository on 2026-06-11, a working bridge three days later

    The repository was created on 2026-06-11 and the first commit landed on 2026-06-13 with the message “M0: project scaffold + working bridge round-trip”: a scaffold and a socket that answered, in one step. By 2026-09-30 it held 2,602 commits, the newest a fix titled “a daemon skew is told to the connection that announced it” (issue #1136). The monthly shape is 395 in the creation month, 466 in July, 1,252 in August and 489 in September. The twenty releases listed run from v2.1.0 on 2026-07-18 to v3.4.0 on 2026-09-30, about one every four days and four of them in the last twelve; the tag list also carries a v3.0.0 the release list does not, so the move from 2.14.0 to 3.1.0 skipped a number. The last day is worth reading in order: between 18:16 and 18:20 the maintainer filed thirty issues, each naming a file and a “Done when” clause, v3.4.0 was tagged at 18:25, and commits kept arriving until 18:42. Two of them were claimed from outside within the hour, under a repository that also carries issue templates, code owners, a DCO check, a governance document, a code of conduct and the hacktoberfest topic. A changelog of 706,454 bytes is assembled by a script, and tests keep the published packages and the Rust crate in version lockstep while treating an additive contract as something other than skew.

  2. 02

    The sensor sits inside the page, not beside it

    The organising choice is in the README: Playwright, DevTools and browser agents stand outside the browser looking in, which is right for a site you do not own and wrong for the app you are building, where the bugs that matter never reach the pixels. So the app carries the sensor. In development it calls connect and opens a WebSocket to the bridge at ws://localhost:4400/reticle by default, sending a HELLO with a session id, the protocol version and, if configured, a pairing token; each tab generates its own id, so two tabs never collide. The SDK then installs a MutationObserver for DOM changes, wrappers around fetch and XMLHttpRequest, a console hook and a history hook for routing, plus the registries an app opts into by name, and pushes all of it into a bounded ring buffer, so recent history is always available and memory is capped. The agent calls an MCP tool, the server turns it into a command over that socket, and the SDK runs it in the real page and streams structured events back — look, act, read state and navigate. The seam between the two runtimes is a package rather than a convention: every string and shape that crosses the browser, the bridge and the agent lives once in @reticlehq/core, and the server parses every inbound socket message against it, closing the connection on malformed input instead of letting it reach logic.

  3. 03

    The same protocol over Electron, Tauri and a command line

    The desktop work is where the repository stops being a browser tool. Electron gets an adapter that lives in the main process, with an IPC observer and main-process capture in a preload script of about 11 KB, which is how the boundary between a renderer and its privileged process becomes evidence; the README names that boundary as the thing a browser-only tool cannot see. Tauri gets a Rust capture backend — a crate outside every JavaScript gate, compiled by dedicated CI jobs, with one capture module per operating system — plus a desktop contract file generated by a script and checked by a test. A third realm handles a command-line subject: it spawns and watches the process, puts no code inside it, and renders its own terminal HUD. All three answer to the same protocol package, whose subject vocabulary is deliberately open — web, desktop, mobile, service, game, device, or anything else — on the stated ground that a protocol which must be revised to admit a new kind of computer has an expiry date. The price of that generality is written down beside it: the instance must change when the thing it names is replaced, a navigation invalidates evidence totally while a hot update does so only partially, and both have to be reportable.

  4. 04

    The file and line come from the build, and the SDK never ships

    A verdict that names src/checkout/PayButton.tsx and a line is only worth what the mapping is worth, and the mapping is made at build time: three plugins stamp data-reticle-source into the markup — a Babel plugin, a Next adapter that keeps SWC and wraps the config, and the Vite plugin, which also injects the connect call, discovers the dev server port and writes a pairing token. @reticlehq/react maps a DOM node to a component to a source location, and it is optional by design: the project states that the core works without the source-mapping half. The same build step is why the SDK never ships — it applies to the dev server only, a production build replaces it with an inert stub, and a runtime guard refuses to connect when the build reports NODE_ENV=production. That guard also produced a piece of wrong advice the maintainers filed against themselves: issue #1267 records an agent pointed at a deployed URL getting sdk_never_dialled and the hint to run reticle init in the directory, which cannot help, because the stub is deliberate. The other boundaries are properties rather than promises: the bridge binds 127.0.0.1, an app pairs with an owner-only token at ~/.reticle/pairing-token, a non-loopback bind without that token is refused, and passwords, tokens, API keys and card numbers are replaced with [REDACTED] before they reach the agent.

  5. 05

    Yes, no, unknown — and the blind spots are published too

    The verdict has three values, and the third is the point: unknown means the evidence could not decide, and the README states that a verdict is never a quiet pass. Evidence is graded in tiers — a signal the app emits is strongest because a wrong element cannot fake it, network plus route plus state is next, DOM or text presence is the weak fallback the tool nudges away from. The protocol turns that into numbered requirements: evidence for a consequence must not come from the channel that performed the action, a disagreement is not a fault unless one of the two channels is independent, and a claim reading a channel nobody declared must be unknown rather than a failure, because “nothing was watching” and “it did not happen” produce identical empty evidence. The same reflex produces the small print: a backgrounded tab warns that timer, rAF and pointer gestures may silently no-op, an evicted buffer reports held and dropped counts, warning that a negative result may be a false negative, and the limits sit beside the strengths — IndexedDB, Web Workers, closed shadow roots and cross-origin iframes are named as unseen, and races around a single action are partial. The measurement behind the grading: on a deterministic corpus a locator that resolves to the wrong element satisfies a presence check but never a consequence check, and that difference is “1 false green in 88” against 29.

  6. 06

    Nine tools advertised, forty-five in the table

    The advertised surface is a budget decision rather than a leftover, because MCP re-sends every tool definition to the model on every turn: the tool list is rent, not a menu you pay for once. The table holds 45 tools, the agent is shown nine, and only two of them can produce a verdict — a drive that ends without reticle_act_and_wait or reticle_assert has no result. Most of the rest were merged rather than removed outright. A measurement off a fresh daemon on 2026-09-16 puts the default surface at nine tools and 17,663 bytes against thirty tools and 126,954 bytes with everything advertised. Two tools were promoted out of the cold tail after a measured mistake: reachable only through a generic invoke tool, they left a login form driven one call at a time, because “a tool an agent must already know about is a tool that never gets called”. Trimming further has a floor: an eight-tool cut measurably dropped real-agent accuracy, and the note calls that reading dated evidence rather than proof. Replay is the other half of the budget, and the gate sits on top of it: a recorded flow re-runs with no model, 47 tokens against roughly 120,000 to re-drive it, and the gate exits non-zero unless a passing artifact covers every flow an edit has touched, which the README calls the one check nobody can satisfy by reasoning about their own diff.

Adjacent records

All records →