Skip to main content

Flows, the recorder & self-healing — record once, run forever

Reticle turns an interactive run into a git-checked, replayable program stored under .reticle/. Flows are anchored on meaning (testid + signal), not volatile element refs or coordinates, so they survive refactors — and when an anchor does drift, Reticle tells you why and can repair it. This is what makes Reticle “the project’s living test suite a human seeds and an agent maintains.”
All of this is on-disk and human-readable, so flows are reviewed in PRs and diffed like code.

The .reticle/ directory

When you record or save a contract, Reticle writes a git-checked workspace next to your app:
The server resolves .reticle/ from the working directory it runs in (your project root). A fresh agent can read .reticle/contract.json to learn the testable surface without grepping your source.

The contract — advertise the testable surface

In your app, declare what’s testable (see Step 6 in Getting Started):
Then persist it to disk so it’s committed and any agent can read it:

Create a flow

(a) Agent-recorded — the agent drives, then saves:
(b) Human-recorded (the recorder toolbar) — with the presenter on (present: true), the floating panel hosts a recorder: a human clicks the golden path in the page and Reticle captures each interaction as a semantic-anchored step (testid, else role+name), then persists it via reticle_flow_save_recorded. The agent then runs and maintains it. (First cut: structured annotations only — see below; free natural-language annotations are future work.)

What a flow file looks like

Each step binds to a semantic anchor, never a eXX ref: a testid/signal when available, else an auto-derived component anchor (component name + source file:line) for an element with no testid — so the flow stays stable with zero hand-added testids. Only when none of those resolve is a step kept degraded: true (a last-resort “add a testid here” marker) rather than silently dropped.

Run a flow

Watch it replay on the page. When the presenter is on (present: true), a replay isn’t silent — each step drives the real page, so the synthetic cursor flies to the element, the focus ring lands, and the activity log streams the journey live. You (or a teammate) literally watch the saved journey re-walk itself on your app, then see the verdict land. It’s the fastest way to see that a flow still works — not just read a green checkmark. reticle_flow_replay returns a status:
  • ok — every anchor resolved and every expect held.
  • drift — an anchor missed (a testid was renamed, or a signal never fired). The result is legible: { step, anchor, drift: { reasonKind: "testid_not_found", nearest: "send-message" } } — never a blind failure. (This is the “whose fault is it” principle.)
  • error — the flow file is missing/invalid, or a resolved action failed. Runtime failures include the failed step and a top-level error envelope.
A testid-preserving refactor (you moved markup but kept the testids) still replays green. A step whose element has no testid is anchored on its component + source location ({ kind: "component", component, source: { file, line } }) — an auto-derived stable anchor, so a flow records cleanly with zero hand-added testids and replay re-resolves it via reticle_query by:'component'.

The decision envelope — what to do next, not just pass/fail

On a drift or error, the replay result carries a decision an agent can act on directly:
This is the feedback a human reviewer used to give — made machine-actionable, so the agent decides its next move without one.

Verify the whole suite in one call

reticle_flow_verify replays every saved flow (or a named subset) deterministically — no LLM per flow — and returns one consolidated verdict. This is the regression check to run after any change:
Passing flows are counted; only failures carry detail (token-cheap). Build → reticle_flow_verify → fix from each failure’s nextAction → repeat — the autonomous regression loop.

Self-healing — the agent maintains the flow

When a testid is renamed, the flow drifts. reticle_flow_heal proposes — and optionally applies — the nearest-match rebind, so flows don’t rot:
With apply: false the flow file is never modified — you get the proposed diff to review. With apply: true Reticle rewrites the drifted anchor(s) to the confident nearest match and a subsequent replay passes. A drift with no confident nearest match leaves the file untouched.

Annotations (structured)

reticle_annotate attaches a structured annotation that compiles into the flow, so replay is a checked re-run, not a blind macro:
  • assert-signal / assert-visible → a step expect predicate (the invariant).
  • mark-dynamic → a flow.dynamic[] entry — replay asserts the region’s presence but not its words (the LLM-output case: assert caption:generated, ignore the caption text).
  • success-stateflow.success (the golden end condition). Pass signal/testid, or statePath (+ store, equals) to make the golden condition a store-truth assertion — the app’s own source of truth, which no DOM read can reach (e.g. statePath: "deployments.0.status", equals: "live" fails the flow if a deploy only looks shipped on screen). State assertions are graded as consequences, so they satisfy the business-outcome oracle.

Flows are your test suite

.reticle/ flows can be executed as CI specs — replayed with their expect/success predicates, skipping dynamic regions — via @reticlehq/test’s flowsAsSpecs. See Testing with Reticle.

Tool reference

Flow name must be a single safe path segment (no /, \, .., or leading dot).