Developers

A browser you can
compile against.

Eighteen TypeScript packages, from a raw CDP client up. Record a session into an artifact, validate it statically, replay it against a pinned Chrome — and get a diff to review when the page moves under you.

~/your-project
$ pnpm install && pnpm build
$ pnpm fs doctor
chrome    151.0.7922.71  (pinned ✓)
resolved  ./.cache/chrome/mac_arm-151…

$ pnpm fs record checkout --url https://shop.test/
recording… press Enter to stop
compiled 7 steps · static validation passed

$ pnpm fs replay checkout
  ✓ s1 navigate      (412ms)
  ✓ s2 click "Cart"  (96ms)
  ✓ s3 type  email   (61ms)
  …
✓ completed in 1180ms — zero model calls

The mechanism

There is a compile step. That's the whole idea.

Agent frameworks read the page and decide what to do, every time, forever. FullSpectrum decides once — while a human is watching — and then executes a plan. The difference shows up as cost, as latency, and as the absence of an injection surface.

01 / RECORD

Semantic identity, captured in the handler

Role, accessible name, ordinal, neighbouring labels and any test id are computed in the page while the element is still the element you clicked — not resolved afterwards against a snapshot that has already moved on.

02 / COMPILE

Conditions checked before Chrome launches

Every step declares a pre-condition and a post-condition. The plan is valid only if post_i ⊆ pre_{i+1} holds for the whole chain. A workflow that cannot hold together fails at build time.

03 / REPLAY

Resolve, act, verify — or stop

Similarity resolves the target, the action fires over CDP, and the step polls until its post-condition holds. Below threshold, ambiguous, or on an unexpected origin, it fails closed and hands you a repair.

Zero model calls at replay No tokens, no context window, no non-determinism. The hundredth run costs what the second one did.
Extraction 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.
Stale refs are refused, not resolved A ref captured against snapshot v1 is rejected after a re-snapshot, so a replay can never act on a coordinate that no longer means what it meant.
Origin is part of the contract A step declares its host. Content arriving from an unexpected origin stops the run — including for extraction, where it is a provenance failure.
Healing is opt-in, per run --allow-healing exists because a workflow that heals on every run is a workflow nobody has fixed.

Library API

The CLI is a thin wrapper. You can skip it.

Everything the fs command does is available as functions. The CLI adds persistence — the run, its audit chain, and any proposed repairs — and nothing else.

Replay a compiled workflow

replay() returns a report, not an exception. A stop is an escalation with a step id, a reason, and the repairs a human could approve — because a browser run that throws tells you it failed, and one that reports tells you what to do next.

import { BrowserSession, launch } from '@fullspectrum/cdp';
import { Store } from '@fullspectrum/store';
import { renderRepairs, replay } from '@fullspectrum/workflow';

const store = new Store();
const workflow = await store.loadWorkflow('checkout');
const browser = await launch({ headless: true });
const session = await BrowserSession.create(browser.connection);

const report = await replay(browser.connection, session, workflow, {
  allowHealing: false,
  onStep: (r) => console.log(r.step.id, r.outcome.status),
});

if (report.escalation) {
  console.log(report.escalation.stepId, report.escalation.reason);
  console.log(renderRepairs(report.repairs));
}

await browser.close();

Compile one you already know

When the steps aren't a discovery — a harness, a fixture, a smoke test — skip the observational recorder and drive Recorder directly. validate() is the same static check the CLI runs, and it is worth calling before you save.

import { Recorder, validate } from '@fullspectrum/workflow';

const rec = new Recorder(browser.connection, session, 'hn-newest');

await rec.navigate('https://news.ycombinator.com/');
await rec.click({ role: 'link', name: 'new' });
await rec.click({ role: 'link', name: 'comments' });

const workflow = rec.finish();

// post_i ⊆ pre_{i+1}, for every step, before anything is stored
for (const v of validate(workflow)) {
  console.warn(`[${v.stepId}] ${v.kind}: ${v.message}`);
}

await store.saveWorkflow(workflow);

Packages

Take the ones you need.

Nothing here is a framework. @fullspectrum/cdp is usable on its own if all you want is a CDP client with a browser that doesn't change under you.

@fullspectrum/cdp

Raw Chrome DevTools Protocol client, launcher with a pinned binary, and BrowserSession — one page plus every out-of-process frame and popup it spawns.

@fullspectrum/perception

Accessibility-tree snapshots with versioned refs, cross-frame splicing, token-budget compaction, and semantic locators that survive reloads.

@fullspectrum/workflow

Record → validate → replay. Observational and programmatic recorders, static condition checking, actuation, extraction, and repair proposals.

@fullspectrum/store

Workflows, run history, heal history retained across runs, and the hash-chained audit log.

@fullspectrum/document

The PageDocument schema — 38 block kinds, typed inline text, form fields — plus validation and live-page capture.

@fullspectrum/render

The component kit. Server-rendered HTML, one function per block kind, design tokens, light and dark, nothing external.

@fullspectrum/label

Region recovery: a deterministic labeler, a Claude-backed one, and the harness that scores them against each other.

@fullspectrum/cli

The fullspectrum command, the HTTP API, the MCP server, the mirror, and the browsing shell.

@fullspectrum/testkit

Workflow tests: fixtures, expectations over a run report, a runner, and JUnit XML. It depends on nothing that opens a browser — the executor is injected, so the same suite runs locally or against a fleet.

@fullspectrum/sdk

The TypeScript client for the HTTP API. Zero dependencies, fetch only. A Python client ships alongside it, standard library only.

The developer surface

A failed workflow is not a failed request.

fullspectrum serve answers 200 with a report saying the run escalated, because the API call worked — the site is what changed. A client that threw on ok === false would make every caller write a try that cannot tell the button moved from the server is down. An escalation is a queue item, so it arrives as data.

Loopback only, bearer token, and any request carrying an Origin header refused outright: loopback is not a boundary, and DNS rebinding makes a hostile page same-origin with the server as far as Chrome is concerned. Recording is not exposed — a person demonstrating a flow needs a window they can see. Drafting is, and a draft cannot run until somebody approves it.

Pages as JSON

One schema, two front doors, one component kit.

The same document format is authored by hand and produced by capturing a live page. HTML tells you here is a div; PageDocument tells you here is the pricing section.

{
  "kind": "section",
  "region": "pricing",
  "blocks": [{
    "kind": "callout",
    "tone": "danger",
    "text": "Card expires in 3 days"
  }]
}

Three rules the format enforces

A closed union of 38 kinds An unknown kind is a validation error, not a blank rectangle. Open {type, props} formats always end up rendering nothing for content they don't understand.
No styling in the document Blocks carry intent (tone: "danger"), never appearance (color: "#b91c1c").
No html field, anywhere Inline formatting is a typed tree, so captured content — untrusted by definition — cannot carry markup or script into the renderer.
bash
$ pnpm fs render examples/site.json --out site.html
$ pnpm fs capture https://example.com --out page.json
$ pnpm fs validate page.json

The browsing shell

A window that never receives the origin's code.

fs browse captures every page to a PageDocument and re-renders it with the component kit. No script, no styles, no markup from the site reaches the shell — so nothing on the page can overdraw the UI or reach the automation.

Links travel by URL, because following an address the document already carries cannot be ambiguous. Controls act through their locator, because clicking is resolution under uncertainty — and is allowed to refuse. History is Chrome's own, not a shadow stack, so back really goes back: bfcache, scroll position, form state, SPA entries and all.

refusals, verbatim
javascript: in the address bar
  → refused by scheme, never silently searched for

a locator matching two things equally
  → refused: best candidate scored 0.42 < 0.75

an element with no accessible name, 31 candidates
  → refused: indistinguishable

a locator missing fields, arriving over HTTP
  → refused, not a 500

DNS or connection failure
  → the browser's own error page, with the real error

MCP

Give an agent the workflows, not the wheel.

fullspectrum mcp exposes the runtime over stdio. An agent can run workflows a human already approved, read the results, and see what needs review.

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.

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

Thirteen 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

In CI

Upgrading Chrome is a code change.

Chrome ships a new milestone every four weeks with weekly stable refreshes — a two-week major cycle from September 2026 — and CDP's “stable 1.3” tag is frozen at Chrome 64. A fleet that follows whatever browser the host happens to have changes behaviour without a commit — so the pin lives in the repo and moves through the same gate everything else does.

your-repo/.github/workflows/verify.yml
- name: Install the pinned browser
  run: npx @puppeteer/browsers install \
         chrome@151.0.7922.71 --path ./.cache/chrome

- name: Unit + regression
  run: |
    pnpm test
    pnpm regression

- name: Replay every stored workflow
  run: pnpm fs verify --require-pinned
1,733 unit tests, no browser The replay engine runs against a scripted browser and the CDP client against a real WebSocket server, so the fail-closed properties are verified without launching Chrome.
20 end-to-end scenarios, offline Observational recording from real input events, cross-origin iframes, popups, shadow DOM, drift-and-repair, late elements, stale-ref refusal, capture fidelity, the mirror's proxy, link-following and history.
fs verify is the upgrade gate It replays everything you have stored. Moving PINNED_CHROME_VERSION without a green run is the one thing the repo is set up to make hard.
Node 22 · pnpm · stock Chrome

Clone it and break it.

If a workflow doesn't replay clean the second time, that's a bug and we want the issue. If it refuses when it shouldn't, that's a more interesting one.