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.
$ 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.
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.
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.
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.
--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.
Raw Chrome DevTools Protocol client, launcher with a pinned binary, and BrowserSession — one page plus every out-of-process frame and popup it spawns.
Accessibility-tree snapshots with versioned refs, cross-frame splicing, token-budget compaction, and semantic locators that survive reloads.
Record → validate → replay. Observational and programmatic recorders, static condition checking, actuation, extraction, and repair proposals.
Workflows, run history, heal history retained across runs, and the hash-chained audit log.
The PageDocument schema — 38 block kinds, typed inline text, form fields — plus validation and live-page capture.
The component kit. Server-rendered HTML, one function per block kind, design tokens, light and dark, nothing external.
Region recovery: a deterministic labeler, a Claude-backed one, and the harness that scores them against each other.
The fullspectrum command, the HTTP API, the MCP server, the mirror, and the browsing shell.
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.
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"
}]
}
<section data-region="pricing">
<div class="fs-callout fs-callout--danger">
Card expires in 3 days
</div>
</section>
/* the tone became a token, not a hex code —
which is what makes a redesign one edit */
Three rules the format enforces
{type, props} formats always end up rendering nothing for content they don't understand.
tone: "danger"), never appearance (color: "#b91c1c").
html field, anywhere
Inline formatting is a typed tree, so captured content — untrusted by definition — cannot carry markup or script into the renderer.
$ 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.
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.
- 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
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.
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.