Documentation

Record once.
Replay forever.

Everything below describes software that runs on your machine, today, with no account. Install it, record a session, and replay it deterministically against stock Chrome.

On this page

Introduction #

FullSpectrum is a browser runtime for agents. The premise is that most browser automation should not call a model at all.

A session is recorded once into a compiled artifact — semantic locators plus pre- and post-conditions for every step — and every run after that is replayed deterministically against stock Chrome over CDP. There is no model in the loop, which means no token bill, no non-determinism, and no prompt-injection surface where page content could become control flow.

When a locator drifts, replay does not quietly substitute a different element. It stops and emits a reviewable repair: a diff, a confidence score, and the signals behind it, waiting for a human to approve or reject.

Two front doors, one runtime. The same package also renders and captures pages as semantic JSON — see Pages as JSON. Automation and rendering share one locator implementation, which is why a click in the mirror refuses in exactly the places a replay does.

Install #

Node.js 22 or newer, pnpm, and a Chrome binary. The runtime resolves a pinned Chrome for Testing build ahead of any system browser — see the version pin for why.

bash
$ pnpm install
$ pnpm build

# install the pinned browser
$ npx @puppeteer/browsers install chrome@151.0.7922.71 --path ./.cache/chrome

# what browser will be used, and is it the pinned one?
$ pnpm fs doctor

fs doctor is the first command to run and the first one to check when something behaves differently on two machines. It reports which binary was resolved, from where, and whether it matches PINNED_CHROME_VERSION.

Quickstart #

Three commands: record a real session, replay it, then look at what the replay cost. Recording opens a headful browser and watches you work — press Enter in the terminal to stop.

bash
$ pnpm fs record shopping --url https://example.com/
recording… use the browser, then press Enter
compiled 7 steps · 0 warnings

$ pnpm fs replay shopping
✔ shopping — 7/7 steps · 1180ms · 0 model calls

$ pnpm fs stats shopping

Nothing left the machine and nothing called a model. From here, the two things worth reading next are what replay guarantees and what happens when a page changes.

The version pin #

CDP has no meaningful stability guarantee — the “stable 1.3” tag is frozen at Chrome 64 while Chrome ships 151.x — and Chrome ships a new milestone every four weeks with weekly stable refreshes, a two-week major cycle from September 2026. A fleet that follows whatever browser the host happens to have changes behaviour without a commit.

So the pinned Chrome for Testing build is declared in the repo, resolved ahead of any system browser, and reported in every run. Upgrading it is a code change, gated by the regression suite.

bash
$ pnpm regression   # 20 scenarios, offline, against local fixture pages
$ pnpm fs verify    # replay every stored workflow — the upgrade gate

This has already earned its keep. --disable-features=OptimizationHints, a common flag in browser-automation setups, segfaults Chrome for Testing 151 on macOS arm64 within milliseconds of the first Page.enable — while working fine on Chrome 150.

How replay works #

Replay is a program, not a transcript being re-interpreted. Before a run touches the browser, the plan is statically validated: whatever step i+1 requires, step i must guarantee — post_i ⊆ pre_{i+1}. A workflow that cannot possibly hold together fails before Chrome launches.

During the run, every step does the same four things in the same order: verify its pre-condition, resolve its target by similarity, act, then poll until its post-condition holds. It fails closed at every point where it is not sure.

SituationWhat happens
Locator scores below thresholdStep fails. A repair is proposed if one can be found.
Two candidates tie, ordinal goneRefuses to guess — a wrong cached click is worse than a slow one.
Recorded target had no accessible nameRefuses. “The Nth unnamed button” reorders whenever the page does.
Page is not where the workflow expectsStops before acting.
Page is on a different origin than declaredRefuses — including for extract, since reading data off an unexpected origin is a provenance failure. A step may declare its own host, so SSO redirects still replay.
A popup opensFollowed only if the recorded conditions hold there.
A ref from an older snapshot is reusedRefused outright. Stale refs are a safety property, not an inconvenience.

Every stop produces an escalation carrying the reason, the page it happened on, and the repair a human could approve.

Recording #

fs record opens a real, headful browser and watches you work. Clicks, typing, key presses and dropdown choices become steps; a hundred keystrokes become one type step carrying the final value; a click into a text field is dropped, because type focuses its own target.

The semantic identity of each target — role, accessible name, ordinal among identical controls, neighbouring labels, any data-testid — is computed in the page, in the event handler, while the element is still the element that was clicked. Resolving it afterwards against a snapshot loses a race it cannot win: by then the click has navigated, closed the menu, or re-rendered the row.

Variables

Bind a typed literal to a variable so replay can vary it:

bash
$ pnpm fs record login --url https://example.com/login --var email=me@example.com
$ pnpm fs replay login --var email=someone@else.test

There is also a programmatic Recorder for when you already know the steps — see the programmatic API.

Replaying #

bash
$ pnpm fs replay shopping
$ pnpm fs replay shopping --var email=someone@else.test
$ pnpm fs replay shopping --json            # machine-readable result
$ pnpm fs replay shopping --allow-healing   # opt in to relocation, off by default

Healing is off by default during replay. A workflow that heals on every run is a workflow nobody has fixed, so the default is to stop and tell you. --allow-healing is for the cases where you have decided a drifting page is acceptable and you would rather the run complete.

Replay everything before a browser upgrade. fs verify replays every stored workflow and is the gate for changing PINNED_CHROME_VERSION.

Repairs #

Deterministic replay with repair-on-break is the incumbent enterprise RPA architecture and has been generally available for over a year. What nobody ships is a heal you can review: UiPath states plainly that “healing information from a previous run is not retained in the next run,” and Microsoft concedes its portal “doesn't currently surface additional self-healing details in the action logs.”

Here, a repair is a build-time artifact — something you read, diff, and approve.

bash
$ pnpm fs repairs shopping
1 pending

  #4  click "Add to cart" → "Add to bag"
      confidence 0.91 · role button · same ordinal · same ancestors

$ pnpm fs repair apply shopping 1   # shows the diff, validates, then asks
$ pnpm fs repair reject shopping 1  # the history is append-only

Relocation uses similarity, not a model. HybridSimilo relocates 98.8% of elements with broken locators with no model at all, and similarity is free, deterministic, and not a prompt-injection surface. Heal history is retained across runs, so a repair you rejected last month does not come back next month wearing a new id.

Extraction #

Schema-constrained, in-page, deterministic:

{
  "kind": "extract",
  "into": "products",
  "extract": {
    "rows": "tr.item",
    "fields": [
      { "name": "name",  "selector": ".name" },
      { "name": "price", "selector": ".price", "type": "number" },
      { "name": "url",   "selector": "a.link", "attribute": "href" }
    ],
    "limit": 200
  }
}

Extracted text returns data, never control flow. A value can fill a declared hole in a later step; it can never become a step, a URL, a tool name or a file path. That boundary is what makes model-free replay injection-proof rather than merely injection-resistant.

Rows, per-field matches and string lengths are all bounded. An extractor pointed at a hostile page is otherwise an unbounded read.

Audit log #

Every run writes an append-only, hash-chained log with per-step page provenance — which origin's content was on screen when each action was taken. That is the question an incident actually asks.

bash
$ pnpm fs audit shopping run_01HQ…
chain verified · 7 entries · no gaps

  1  goto      https://example.com/          origin example.com
  2  click     "Search"                      origin example.com
  3  type      "wireless keyboard"           origin example.com
  …

The chain is tamper-evident, not signed: each entry commits to the previous one, so a line cannot be altered or removed without breaking every hash after it. Signing needs a key the writer holds and the verifier trusts, which is a deployment decision rather than a library one.

Measurement #

Every cache hit-rate claim in this space is self-reported and unaudited. No independent production hit-rate measurement exists for any workflow cache, which makes instrumenting it both the only way to know the bet is paying off and an asset in itself.

pnpm fs stats shopping
shopping
  runs            27 (26 ok)
  steps           189
  clean replay    98.4%  (no heal, no model)
  healed          0.5%
  failed          1
  mean / p95 run  3120ms / 4980ms
  model calls     0
  heals           2 proposed, 1 applied, 1 pending

Those are illustrative numbers, not measured ones. The point of the command is that yours will be real. Nothing on this page reports a benchmark we ran on your behalf.

PageDocument #

FullSpectrum also renders sites from a semantic JSON document instead of HTML. HTML tells you here is a div. PageDocument tells you here is the pricing section:

{ "kind": "section", "region": "pricing",
  "blocks": [ { "kind": "table", "columns": […], "rows": […] } ] }

Three rules the format enforces

  • Blocks are a closed union of 38 kinds. An unknown kind is a validation error, not a blank rectangle. Every builder that starts with an open {type, props} ends up rendering nothing for content it does not understand — which is indistinguishable from content that is genuinely empty.
  • No styling in the document. Blocks carry intent (tone: "danger"), never appearance (color: "#b91c1c"). That is what makes a redesign a change to a token block rather than a find-and-replace across every page ever authored.
  • There is no html field anywhere. Inline formatting is a typed tree, so captured content — untrusted by definition — cannot carry markup or script into the renderer.

Render #

bash
$ pnpm fs render examples/site.json --out site.html
$ pnpm fs validate page.json   # what is wrong, and where

examples/site.json is a full landing page — nav, hero, metrics, cards, callout, tabs, pricing table, quote, FAQ accordion and a form — with no HTML written to produce it. The component kit is server-rendered, one function per block kind, with design tokens, light and dark themes, and nothing external.

Capture #

bash
$ pnpm fs capture https://example.com --out page.json

Capture recovers structure, not intent. Sections, headings, tables, forms, navigation, lists and links survive; the rich authoring blocks do not, because once a cardGrid has been rendered to HTML there is no signal that a div of cards ever was one. Round-tripping examples/site.json through render → capture returns sections, headings, tables, a quote, an accordion and the form — with metricRow, cardGrid, callout and tabs arriving as their structural equivalents.

It is honest about the cost, too. Hacker News captures to ~17k tokens with all 197 links and hrefs intact, against ~3.3k for the compacted accessibility tree, which keeps none of them. The document format is richer and renderable, not cheaper — the accessibility tree remains the right channel for perception, and PageDocument the right one for rendering and editing.

Mirror #

fs mirror <url> serves a real page rendered from its own JSON. A click posts back the locator the document carries — never a selector, never coordinates — and the server resolves it against a fresh snapshot through the same findByLocator path replay uses.

pnpm fs mirror https://example.com
click through the JSON  →  {"ok": true, "confidence": 0.9}   page really moved
ambiguous target        →  refused: no accessible name and 31 candidates
                            are indistinguishable

The rendered page contains no script, style or markup from the origin, so nothing on the site can overdraw the UI or reach the automation.

Browse #

fs browse is the mirror plus the things that make it a browser: an address bar, a history to move through, and links that go somewhere.

bash
$ pnpm dev                              # reloads the view when the server restarts
$ pnpm fs browse news.ycombinator.com
$ pnpm fs browse --port 7373 --headful  # watch the real Chrome alongside it

Everything the shell forwards back is typed:

You doIt sendsResolved by
Click a linkits raw hrefnew URL(href, currentPage) — server-side
Click a controlthe locator the document carriesfindByLocator, which refuses when ambiguous
Type in a fieldthe locator and the valuefindByLocator → Input.insertText
Submit a formthe submit control, or Enter on a fieldfindByLocator → click, or focus + Enter
Back / forwarda deltaPage.getNavigationHistory — Chrome's own history

It refuses in the same places replay does, and for the same reasons:

refusals
javascript: / data: / file: in the address bar  → refused by scheme, never
                                                   silently searched for
mailto: / javascript: link                      → refused, not followed
locator matching nothing, or two things equally → refused: "best candidate
                                                   scored 0.42 < 0.75"
a locator missing fields, arriving over HTTP    → refused, not a 500
DNS or connection failure                       → the browser's own error page

Not in this version: tabs, downloads, cookie and profile management, and per-keystroke typing — a value goes to the page when a field is left, not on every key, so fields with key-level validation will not see individual keystrokes.

Region labels #

Capture gets structure; a labeler puts the meaning back. region: "pricing" is the difference between a model inferring what part of a page it is looking at and simply knowing.

bash
$ pnpm label-eval            # rules only — free, instant
$ pnpm label-eval --claude   # adds the model, and scores it against the rules

capture-only (no labeling)   0.0%   ← the floor, by construction
heuristic                   94.7%   (18/19 sections, 1ms, $0)

The model has to beat free. A deterministic labeler ships alongside the Claude one precisely so the comparison is forced, and --claude refuses to declare a winner if the model failed to run. The model's authority is deliberately tiny: it receives a flattened summary of sections and returns a region label per index. It cannot add, remove, reorder or rewrite a block.

Honest limits on that 94.7%: those pages were authored for this benchmark and the rules were written knowing them. It is a floor and a regression check, not evidence of generalization. A real eval needs captured pages nobody here wrote, labelled by someone who did not write the rules.

CLI commands #

CommandWhat it doesFlags
record <name>Watch a real session and compile it--url --var k=v
replay <name>Replay deterministically--var --allow-healing --json
verify [names…]Replay everything — the Chrome-upgrade gate
listStored workflows
show <name>Steps, variables, validation--json
runs <name>Run history--limit N
stats [name]Clean-replay rate, heal rate, timings
audit <wf> <runId>Page provenance, hash-chain verified
repairs <name>Heals awaiting review
repair apply <name> <id>Apply, with a diff and a confirmation--all --yes
repair reject <name> <id>Record the decision
doctorBrowser resolution and the version pin
mcpExpose the runtime over stdio MCP
capture <url>A live page as JSON--out --html --targets
render <doc.json>JSON as a page--out --wide --no-js
validate <doc.json>Schema problems, with paths
mirror <url>Serve a live, clickable JSON rendering--port
browse [url]The browsing shell--port --search --headful --dev

Global flags: --root <dir> (store location, default $FULLSPECTRUM_HOME or ./.fullspectrum), --chrome <path>, --chrome-version <v>, --require-pinned, --headful.

That table is the daily set, not the whole surface — fs help lists the full command set, including app, export, schedule, serve, drafts, vault, queue, canary and triage.

MCP server #

fullspectrum mcp exposes the runtime to an agent over stdio:

{
  "mcpServers": {
    "fullspectrum": { "command": "pnpm", "args": ["--dir", "/path/to/FullSpectrum", "fs", "mcp"] }
  }
}

Tools: list_workflows, show_workflow, replay_workflow, run_history, workflow_stats, pending_repairs, preview_repair, audit_run, triage_run, recall, remember, list_drafts, browser_status.

Recording is deliberately not exposed, and neither is “browse this page for me.” An agent cannot demonstrate a workflow, because the value of the artifact is that a human decided what the steps are. And the moment a model reads page content and picks the next action, every property that makes replay safe is gone. What an agent gets is the ability to run workflows a human already approved, read the results, and see what needs review.

Packages #

PackageWhat it does
@fullspectrum/cdpRaw CDP client, launcher with a pinned binary, and BrowserSession — one page plus every out-of-process frame and popup it spawns.
@fullspectrum/perceptionAccessibility-tree snapshots with versioned refs, cross-frame splicing, token-budget compaction, and semantic locators that survive reloads.
@fullspectrum/workflowRecord → validate → replay. Observational and programmatic recorders, static condition checking, actuation, extraction, repair proposals.
@fullspectrum/storeWorkflows, run history, heal history retained across runs, and the hash-chained audit log.
@fullspectrum/documentThe PageDocument schema — 38 block kinds, typed inline text, form fields — plus validation and live-page capture.
@fullspectrum/renderThe component kit. Server-rendered HTML, one function per block kind, design tokens, light/dark, nothing external.
@fullspectrum/labelRegion recovery: a deterministic labeler, a Claude-backed one, and the harness that scores them against each other.
@fullspectrum/cliThe fullspectrum command, the MCP server, the mirror, and the browsing shell.

Tests #

bash
$ pnpm test          # 1,733 unit tests, no browser required
$ pnpm regression    # 20 end-to-end scenarios against local fixtures
$ pnpm label-eval    # region-recovery accuracy against authored ground truth
$ pnpm typecheck

The unit suite runs the replay engine against a scripted browser and the CDP client against a real WebSocket server, so the fail-closed properties and the connection's failure modes are verified without launching Chrome. The regression suite covers what only a real browser can: observational recording driven by real input events, cross-origin iframes, popups, shadow DOM, drift-and-repair, polling for late elements, stale-ref refusal, capture fidelity, the mirror's action proxy, and the browser's link-following and history.

Reading further: docs/ARCHITECTURE.md in the repository carries the full design, the three-phase plan, the security boundaries and the sourced research behind every claim on this page.

Ten minutes, no account.

Clone the repo, run fs doctor, record one workflow. If it doesn't replay clean the second time, we want the issue.