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

Terminal
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 it

init 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 the funnel family. 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. --fix inserts the suggested testIDs 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-run first; --sarif lands them as PR annotations on those lines; and --min-score is 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. --fix repairs what can be repaired; --bundle writes 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 run executes a program against the booted app. agent kit generates the harness; agent rules emits 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. --reference runs each task’s oracle and its counter-example at zero model turns; --agent hands 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 --timeout budget 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 --json or --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_COLOR and CI. 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.
Terminal
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 --json

Config and profiles

.squiggle/config.jsonc
{  // 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.

Terminal
squiggle agent schema flow      # print the flow schema offlinesquiggle agent schema intent    # the intent-program schemasquiggle --schema-json          # the whole command tree, as data

A 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

Terminal
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 fish

Activation 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. --fix applies the safe ones; --bundle writes 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. --history prints 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 .ips reports from DiagnosticReports, and symbolicate stored JS and native stacks.
export · gc
Chrome-trace export for chrome://tracing or Perfetto, and the retention sweep. reproject re-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.md fragment 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.