The CLI
One binary, thirty command families, and a contract designed so that both a human and an agent can use it without either one guessing: a frozen exit taxonomy, a machine channel that owns stdout, and a schema you can print offline.
The first four commands
squiggle init # wire the SDK into an existing appsquiggle init --mcp # …and register the MCP server in .mcp.jsonsquiggle up # relaunch WITH the agent injected — and prove itsquiggle doctor # is the whole plane actually green?squiggle flow login # drive itinit detects Expo or bare RN, your package manager and your entry file, installs the SDK, plants the import, writes EXPO_PUBLIC_SQUIGGLE_WS_URL so the app dials this machine, scaffolds a first flow at .squiggle/flows/smoke.yaml, and names the adapters your package.json implies. It is idempotent — a second run is a no-op — and it makes conservative edits with a printed-instruction fallback for anything it does not recognise.
Driving the app
- flow · flows · watch · bench
- Run one flow, shard a directory across several simulators, re-run on every file save, or benchmark against the last comparable run. See Writing flows and Speed & delivery.
- record · replay · diff · reproject
- Capture a session, replay it (optionally asserting a flow against the recorded timeline), compare two recordings behaviourally, or re-fold recordings into the store. See Record & replay.
- crawl
- Autonomous exploration — prints a coverage and crash report plus a discovered-flow seed, and exits non-zero if it observed a crash.
- consumer
- The standalone interactive graph watcher, which also asserts that the consumer’s materialised graph equals the app’s.
Off-screen targets fail, unless you ask for a scroll
A tap or type whose target measures outside the viewport fails, naming the rect, the viewport and the clamped point. The alternative was never “nothing happens”: the driver clamps an out-of-bounds point into the viewport — it has to, an out-of-bounds HID event can take the simulator down — and then presses whichever control sits at that edge and reports success.
--auto-scroll scrolls the target into view and delivers once more, marking the step scrolled into view. It is off by default because it moves the screen on a step that never asked to, which is wrong for any flow whose subject is the scroll position — and because the explicit scrollUntilVisible is the better thing to have in a committed flow. It is a comparability key for bench, so auto-scroll runs are never averaged with runs that never scrolled.
A flow that needs it every time should say so in the file rather than depend on the flag being typed: config: { autoScroll: true }, which is honoured by the CLI, Studio and the MCP server alike. See run requirements.
When a flow fails, it shows you the screen
A failing run captures the device screen to .squiggle/runs/<runId>.png and puts it under the failing step — drawn inline in a terminal that has graphics (iTerm2, kitty, WezTerm, Ghostty, VS Code), and named as a path everywhere else. The step list tells you a tap did not find its target; the screen tells you the app was on a login wall, or mid-crash-dialog, which is usually the whole answer.
The path is printed either way, and that is the point rather than a fallback: an image cannot be grepped, copied, or opened in a diff tool, and in a CI log the path is the only half that survives. So the PNG is written whether or not anything can render it, and a failed CI run keeps the frame as an artifact. Passing runs capture nothing. SQUIGGLE_NO_IMAGES=1 keeps the caption and drops the picture.
Reading the data
- perf
- Thirteen subcommands:
sql,top,saved,report,gc,snapshot,diff,export,profile,heap,heapdiff,crashes,symbolicate. See Performance. - product
- Ten:
overview,screens,usage,affinity,releases,users,sessions,retention,friction, and thefunnelfamily. See Product analytics. - history
- Recent runs from any surface with their outcomes and durations, and
history rerun <id>to replay one from the argv recorded with it.
Making the app drivable
- legibility
- Scores how agent-drivable the app is and names the exact source lines to fix: interactive controls with no stable selector, screens addressable only by generated text, duplicated handles, missing roles, hit targets under the 44 pt minimum.
--fixinserts the suggestedtestIDs as a parser-driven byte-offset splice — not a regex — applied back-to-front per file so one insert cannot shift the next one’s offset.--dry-runfirst;--sariflands them as PR annotations on those lines; and--min-scoreis a floor a team raises over time rather than a demand for perfection, because a “must be 100” gate gets disabled in week one. - doctor
- Connectivity, handshake, graph health, route tracking — and injection, named capability by capability, because in-process delivery, direct scroll and settle-gated waits degrade silently rather than failing. It also compares the app’s SDK version against the CLI: a skew is a warning, a protocol mismatch is a failure. And it reports an agent kit whose skills were generated by a different build than the one you are running — a project with no kit installed reports nothing.
--fixrepairs what can be repaired;--bundlewrites a redacted support bundle where every string that leaves is a version, a boolean, an enum, or a path rewritten relative to your project root.
Agents
- agent
- Seventeen subcommands, mostly the offline half — schema, briefings, deny-rail config, KPIs, corpus, gate-only
validate— so code review and CI need no device.agent runexecutes a program against the booted app.agent kitgenerates the harness;agent rulesemits an AGENTS.md fragment generated from the command catalogue, so it cannot describe a CLI that no longer exists. - benchmark
- Score an agent against a versioned suite of mobile-UI journeys, graded by re-observation rather than by a human reading screenshots.
--referenceruns each task’s oracle and its counter-example at zero model turns;--agenthands the prompt to any MCP-speaking command and scores the end state through the same path. - mcp
- The stdio MCP server. See Agents & MCP.
- gate relaunch · gate mcp
- Device-level acceptance gates for the failure classes a unit suite structurally cannot see — because the bug is a process being replaced, or a server launched over real stdio.
The contract
Exit codes are frozen and published, which is what lets an agent or a CI script branch on an integer rather than parse prose:
- 0
- Success. Warnings allowed.
- 1
- The run under test failed — a flow, a suite, a gate, a diff.
- 2
- Usage error, or an expected failure with a human message.
- 3
- Licence required or expired.
- 4
- Timeout — the
--timeoutbudget elapsed. - 5
- Device unavailable, or held by another run.
squiggle explain E_NOT_INJECTED — or squiggle explain 5 — turns any of them into an explanation plus a runnable fix, which is what makes an error in a CI log self-serve. Seven codes are catalogued, including the licence ones: squiggle explain E_LICENSE_EXPIRED works when nothing else does, because explain is one of the commands that never needs a licence.
Output rules
- A machine channel owns stdout
- With
--jsonor--events, stdout carries the payload alone and every human line moves to stderr. That is what makes piping safe. - Status is never colour alone
- Every verdict carries a glyph as well as a hue, with an ASCII fallback chosen by one degradation decision behind
--no-color,--no-unicode,NO_COLORandCI. One rule covers colourblind readers, terminals that mangle Unicode, and log files where colour is stripped. - Truncation announces itself
- Every capped list says what it elided and how to ask for the rest. A silently elided tail reads as “that is all there is”.
- Empty states name the next command
- Rather than printing
0 rows.
squiggle perf sql "select * from spans limit 5" --json | jq '.rows[]'squiggle flow checkout --json | jq '.steps[] | select(.status=="fail")'squiggle product funnel run checkout --jsonConfig and profiles
{ // the app under test, when this repo drives exactly one "bundleId": "com.acme.app", "flowsDir": ".squiggle/flows", "shards": 4, // named environments — --profile ci merges profiles.ci over everything above "profiles": { "ci": { "relay": false, "shards": 4 }, "local": { "relay": true, "udid": "…" } }}The config schema is generated from the config type, so a missing key is a compile error rather than a stale schema nobody notices. Same for the flow schema, which is generated from the parser’s own vocabulary — put a $schema header on your YAML and your editor autocompletes it, and an agent validates a flow before a run instead of failing after one.
squiggle agent schema flow # print the flow schema offlinesquiggle agent schema intent # the intent-program schemasquiggle --schema-json # the whole command tree, as dataA flow can carry its own run settings
Four run settings can live in the flow’s own config: block — scrollMode, fastSettle, stepDelayFloorMs, transitionSettleMs — because for those, the right value is a property of the flow rather than of the machine. A flow that tests scrolling needs the gesture loop’s real physics whether or not the suite was launched with --direct-scroll.
Running one flow, a flag you actually passed wins — a one-off is what a flag is for. Running several (squiggle flows), the flow wins even against that flag, because one flag cannot be right for every flow in a suite. Either way the run says which happened, since a flag silently failing to apply would be worse than not having the feature. --ignore-flow-config turns the block off. Full rules in Flows.
Licensing and housekeeping
squiggle activate --trial # start a trialsquiggle activate <key> # activate a purchased seatsquiggle license # which of the four states you are in, and whysquiggle update --check # channels, and --to <version> to roll backsquiggle uninstall --dry-run # every path we wrote, with sizes, before deletingsquiggle telemetry status # anonymous, on by default — one command turns it offsquiggle completions zsh --install # tab-completion for zsh, bash or fishActivation writes a signed grant to ~/.squiggle/license.json that the CLI, the MCP server and the desktop app all read — one activation, three surfaces. The grant is honoured offline until it expires, so a licence check never adds latency and never fails a run mid-flight. license and activate are never gated: they have to work when nothing else does.
Telemetry is anonymous and on by default — and nothing is ever sent before the one-time first-run notice has been shown on your machine. squiggle telemetry off, DO_NOT_TRACK=1 or SQUIGGLE_TELEMETRY=0 turns it off, and squiggle telemetry prints the exact event that is sent, built by the same code the dispatcher uses. A policy you can print is a fact where a paragraph is a claim.
Every command
Thirty-one top-level commands, seventy-eight leaves. The table below is the whole surface — but it is a copy, and the binary is the original: squiggle --schema-json prints this tree with every flag, type and default, offline and in milliseconds. If the two ever disagree, the binary is right.
Bring-up and health
up- Relaunch the app with the agent dylib injected, and prove it by round-tripping the dylib's Mach-O UUID. Run this first.
doctor- Connectivity and graph-health smoke test, with a named remedy per failure.
--fixapplies the safe ones;--bundlewrites a support archive. init- Wire Squiggle into an existing app — dependencies, the Babel plugin, the entry import, the environment, and a first flow.
consumer- The headless graph consumer in watch mode. Materialises the same graph the app holds and asserts parity; records perf until interrupted.
gate relaunch·gate mcp- The two device-level acceptance gates: that the SRP, AX and push planes all survive the app process being replaced, and that the MCP surface a client actually gets is the one we think we ship.
Driving
flow·flows- Run one semantic flow, or a whole suite sharded across every booted simulator. Both rewrite the file in place when self-healing repairs a selector, so the repair arrives as a reviewable diff.
watch- Re-run a flow on every file change, against the warm session — no reconnect, no relaunch, no cold start between iterations.
bench- Run a flow and compare its speed against the last benched run, with per-phase deltas.
--historyprints the recent trend. crawl- Autonomous, seeded, deny-filtered and bounded exploration. Returns coverage, crashes, and a flow seed for what it discovered.
record·replay·diff- Capture a live session to
.sqrec; replay it with no device attached; and diff two recordings behaviourally — render counts, route timeline, network, spans.
perf — thirteen leaves over one SQLite file
sql·saved·top·report- Read-only SQL over
perf.db; named queries shared with the desktop; the live current-readings strip; and a human summary of a session. snapshot·diff- Freeze a session's shape as a labelled baseline, then compare against it — with a comparability check, so two runs that are not alike say so instead of reporting a meaningless delta.
profile·heap·heapdiff- Hermes CPU profiles and heap snapshots over the Metro inspector, and a growth-by-constructor diff of two snapshots with the top retainers of the biggest grower.
crashes·symbolicate- Ingest native
.ipsreports from DiagnosticReports, and symbolicate stored JS and native stacks. export·gc- Chrome-trace export for
chrome://tracingor Perfetto, and the retention sweep.reprojectre-folds recordings and checks the store still agrees with the canonical log.
product — ten leaves
overview, screens, usage, affinity, releases, users, sessions, retention, friction and funnel — the same readers the desktop Product screen and the product_* tools use, over the same event stream. Funnel definitions live in the project's config sidecar, so one definition serves all three surfaces.
agent — seventeen leaves
run·validate·branch- Run an intent program against the booted app, check one without running it, and explore alternative continuations from a point in the journal.
journal·watch·stats- What the agent did and why, live or after the fact, plus KPIs over the journal — turns, heals, escalations, cost.
briefing·amend·schema·kit·rules- The methodology as artefacts: the mode briefing with this app's learned amendments, the amendments alone so they can be diffed in a PR, the published JSON schemas, the full Claude Code harness, and an
AGENTS.mdfragment for the repo. config·eval·library- The deny rail and run budgets; the eval corpus replayed case by case; and the saved flow library — the app's learned verb vocabulary.
export·skill·patterns- The flywheel: the corpus as JSONL training data, with the basis of every reward on the record; a recurring escalation class proposed as a reviewable
SKILL.md, promotable only on a measured benchmark win and revocable afterwards; and learned handlers exported or imported as a bundle, filtered field by field in both directions.
benchmark — the scored suite
list, run --reference, run --agent <cmd> and history. The reference run is the CI-shaped one — every task's oracle plus its counter-example, at zero model turns — and results ratchet within one agent on one suite version, because a reference score and an agent score are not comparable and neither are two different task sets. See Agents & MCP.
The rest
legibility scores how agent-drivable the app is and names the source lines to fix. mcp serves the tool surface over stdio. history lists recent runs and how to re-run one. explain decodes an error or exit code. license, activate, update, uninstall, licenses, telemetry and completions are the productization set above.